Skip to content

Commit 8913719

Browse files
fredbiclaude
andauthored
test(mangling/ucd): added tests to cover UCD-based generation commands (#42)
Bugs fixed: * generation was referencing a deprecated location * numeral interpretation of NaN/Inf is handled by the mangler, not in the tables, where it yields an incorrect result * the generators recorded the UCD source path as filepath.Rel returned it, so a regen on Windows rewrote "Generated from v15/DerivedName.txt" with a backslash and churned every table; filepath.ToSlash pins it Signed-off-by: Frederic BIDON <fredbi@yahoo.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent d4340dd commit 8913719

15 files changed

Lines changed: 1176 additions & 9 deletions

File tree

‎.golangci.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ linters:
6262
- third_party$
6363
- builtin$
6464
- examples$
65+
rules:
66+
- path: mangling/ucd
67+
linters:
68+
- mnd
6569
formatters:
6670
enable:
6771
- gofmt

‎mangling/go_ident_fallback_test.go‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
package mangling
55

66
import (
7+
"strings"
78
"testing"
9+
"unicode"
10+
"unicode/utf8"
811

912
"github.com/go-openapi/testify/v2/assert"
1013
)
@@ -80,3 +83,24 @@ func TestGoIdentFallback(t *testing.T) {
8083
assert.EqualT(t, "", m.Pascalize("日本")) // elided CJK runes
8184
})
8285
}
86+
87+
// TestGoIdentExtremeNumbers covers the sibling contract: whatever the magnitude of a number in the input, a Go
88+
// identifier producer verbalizes it and never leaves a leading digit behind.
89+
//
90+
// The numbers engine spells an integer too large for int64 digit by digit, and -2^63 the same way.
91+
func TestGoIdentExtremeNumbers(t *testing.T) {
92+
t.Parallel()
93+
94+
g := MakeGoMangler()
95+
for _, in := range []string{
96+
"99999999999999999999.5", // integer part beyond int64
97+
"-9223372036854775808 items", // -2^63, no positive int64 counterpart
98+
"1" + strings.Repeat("0", 400), // beyond float64 range: ParseFloat overflows to +Inf
99+
"1" + strings.Repeat("0", 400) + ".5", // same, with a fractional part
100+
} {
101+
got := g.IdentExported(in)
102+
assert.NotEmptyf(t, got, "IdentExported(%q)", in)
103+
first, _ := utf8.DecodeRuneInString(got)
104+
assert.Truef(t, unicode.IsLetter(first), "IdentExported(%q) starts with %q: %.40q", in, first, got)
105+
}
106+
}

‎mangling/numbers/cardinal.go‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
package numbers
55

6-
import "strconv"
6+
import (
7+
"math"
8+
"strconv"
9+
)
710

811
// maxFullCardinal is the default magnitude up to which a number is spelled out in full.
912
//
@@ -78,6 +81,15 @@ func writeCardinal(b *buf, n int64, o numberOptions) {
7881

7982
if n < 0 {
8083
_, _ = b.WriteString("minus ")
84+
85+
if n == math.MinInt64 {
86+
// -2^63 has no positive int64 counterpart, so negating it would leave n negative and spell nothing at all.
87+
// Spell its digits instead, as [writeSpellDecimal] does for an integer too large for int64.
88+
writeDigitWords(b, "9223372036854775808")
89+
90+
return
91+
}
92+
8193
n = -n
8294
}
8395

‎mangling/numbers/coverage_test.go‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package numbers
55

66
import (
7+
"math"
78
"strings"
89
"testing"
910

@@ -25,6 +26,57 @@ func TestNumberWordsOverflow(t *testing.T) {
2526
assert.EqualT(t, "minus one"+strings.Repeat(" zero", 19), neg)
2627
}
2728

29+
// TestNonFiniteValues covers the wording of NaN and the two infinities, which have no cardinal form.
30+
//
31+
// The generated numeral table holds finite values only (see the gen_numerals command), so this guards every other way
32+
// a value reaches the verbalizer.
33+
func TestNonFiniteValues(t *testing.T) {
34+
t.Parallel()
35+
36+
var o numberOptions
37+
38+
assert.EqualT(t, "not a number", numberWords(math.NaN(), o))
39+
assert.EqualT(t, "infinity", numberWords(math.Inf(1), o))
40+
assert.EqualT(t, "minus infinity", numberWords(math.Inf(-1), o))
41+
42+
// The streaming form agrees with the string form.
43+
for _, x := range []float64{math.NaN(), math.Inf(1), math.Inf(-1)} {
44+
var b buf
45+
writeNumberValue(&b, x, o)
46+
assert.EqualTf(t, numberWords(x, o), string(b.b), "writeNumberValue(%v)", x)
47+
}
48+
}
49+
50+
// TestNumberWordsDecimalOverflow covers a decimal whose integer part is too large for int64.
51+
//
52+
// strconv.ParseInt clamps such an integer to int64 max, so the digits are spelled one by one instead — as
53+
// [TestNumberWordsOverflow] does for a plain integer.
54+
func TestNumberWordsDecimalOverflow(t *testing.T) {
55+
t.Parallel()
56+
57+
m := MakeNumberMangler()
58+
59+
got := m.NumberWords("99999999999999999999.5") // 20 nines, > int64 max
60+
assert.EqualT(t, strings.Repeat("nine ", 20)+"dot five", got)
61+
62+
// Beyond float64 range: strconv.ParseFloat returns +Inf with strconv.ErrRange.
63+
huge := m.NumberWords("1" + strings.Repeat("0", 400) + ".5")
64+
assert.Falsef(t, strings.ContainsAny(huge, "0123456789"), "overflowing decimal left raw digits: %q", huge)
65+
assert.Truef(t, strings.HasSuffix(huge, " dot five"), "overflowing decimal lost its fractional part: %q", huge)
66+
}
67+
68+
// TestCardinalMinInt64 covers -2^63, the one negative int64 with no positive counterpart: negating it leaves it
69+
// negative, so writeCardinal spells its digits instead of emitting "minus " and nothing else.
70+
func TestCardinalMinInt64(t *testing.T) {
71+
t.Parallel()
72+
73+
//nolint:dupword // the repeated words are the digit-by-digit spelling of 9223372036854775808
74+
const want = "minus nine two two three three seven two zero three six eight five four seven seven five eight zero eight"
75+
76+
assert.EqualT(t, want, cardinal(math.MinInt64, numberOptions{}))
77+
assert.EqualT(t, want, MakeNumberMangler().NumberWords("-9223372036854775808"))
78+
}
79+
2880
// TestWithNumberDetectPrecision covers the tolerance knob: a tighter precision stops a loose decimal from matching a
2981
// simple fraction.
3082
func TestWithNumberDetectPrecision(t *testing.T) {

‎mangling/numbers/fraction.go‎

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package numbers
55

66
import (
7+
"errors"
78
"math"
89
"strconv"
910
"strings"
@@ -132,12 +133,12 @@ func writeSpellDecimal(b *buf, s string, o numberOptions) {
132133
}
133134

134135
x, err := strconv.ParseFloat(s, 64)
135-
if err != nil {
136-
_, _ = b.WriteString(s)
136+
if errors.Is(err, strconv.ErrSyntax) {
137+
_, _ = b.WriteString(s) // not a number at all: copy it through untouched
137138

138139
return
139140
}
140-
if x > -1 && x < 1 && x != 0 {
141+
if err == nil && x > -1 && x < 1 && x != 0 {
141142
if writeFraction(b, x, o) {
142143
return
143144
}
@@ -154,16 +155,51 @@ func writeSpellDecimal(b *buf, s string, o numberOptions) {
154155
if intPart == "" {
155156
intPart = "0"
156157
}
157-
intVal, _ := strconv.ParseInt(intPart, 10, 64)
158-
159-
writeCardinal(b, intVal, o)
158+
if intVal, err := strconv.ParseInt(intPart, 10, 64); err == nil {
159+
writeCardinal(b, intVal, o)
160+
} else {
161+
// The integer part overflows int64, so ParseFloat has returned +Inf or -Inf with strconv.ErrRange. Spell the
162+
// digits one by one, as the integer branch above does, so the result never starts with a raw digit.
163+
writeDigitWords(b, intPart)
164+
}
160165
_, _ = b.WriteString(" dot ")
161166
writeDigitWords(b, fracPart)
162167
}
163168

169+
// nonFiniteWords are the words for the three float64 values no cardinal covers.
170+
const (
171+
nanWords = "not a number"
172+
infWords = "infinity"
173+
negativeInfWords = "minus infinity"
174+
)
175+
176+
// nonFinite returns the wording of NaN and the two infinities, and whether x is one of them.
177+
//
178+
// The generated numeral table holds finite values only, so no numeral rune reaches this. It guards the conversion in
179+
// [numberWords] and [writeNumberValue]: int64(+Inf) is platform-defined (-2^63 on amd64), which spells out as "minus"
180+
// and nothing else.
181+
func nonFinite(x float64) (string, bool) {
182+
switch {
183+
case math.IsNaN(x):
184+
return nanWords, true
185+
case math.IsInf(x, 1):
186+
return infWords, true
187+
case math.IsInf(x, -1):
188+
return negativeInfWords, true
189+
default:
190+
return "", false
191+
}
192+
}
193+
164194
// numberWords renders a numeric value as words: cardinal for integral values, fraction/decimal otherwise (the value is
165195
// formatted to its shortest exact decimal string first).
196+
//
197+
// NaN and the infinities have no cardinal form and render as [nanWords], [infWords] and [negativeInfWords].
166198
func numberWords(x float64, o numberOptions) string {
199+
if w, ok := nonFinite(x); ok {
200+
return w
201+
}
202+
167203
if x == math.Trunc(x) {
168204
return cardinal(int64(x), o)
169205
}
@@ -174,6 +210,12 @@ func numberWords(x float64, o numberOptions) string {
174210
// writeNumberValue streams the english wording of a numeric value into b — the streaming form of [numberWords], used
175211
// to verbalize a Unicode numeral rune ('½' → "one half", 'Ⅶ' → "seven").
176212
func writeNumberValue(b *buf, x float64, o numberOptions) {
213+
if w, ok := nonFinite(x); ok {
214+
_, _ = b.WriteString(w)
215+
216+
return
217+
}
218+
177219
if x == math.Trunc(x) {
178220
writeCardinal(b, int64(x), o)
179221

‎mangling/ucd/cmd/gen_asciifold/gen_asciifold.go‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,9 @@ func run(pkg, outFile, ucdDir, buildTag string) error {
150150
if err != nil {
151151
return err
152152
}
153+
// The generated file records this path, so keep it slash-separated: regenerating on Windows must not rewrite
154+
// "v15/DerivedName.txt" as "v15\DerivedName.txt" and churn every table.
155+
inFile = filepath.ToSlash(inFile)
153156

154157
if err := emit(inFile, pkg, outFile, buildTag, out); err != nil {
155158
return err

0 commit comments

Comments
 (0)