Skip to content

Commit c375a88

Browse files
committed
Add dedicated scanner tests
1 parent 7d6753c commit c375a88

File tree

3 files changed

+335
-9
lines changed

3 files changed

+335
-9
lines changed

v2/codetags/parser.go

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ package codetags
1919
import (
2020
"bytes"
2121
"fmt"
22-
"strconv"
2322
"strings"
2423
"unicode"
2524
)
@@ -175,12 +174,8 @@ func parseTag(input string, opts parseOpts) (TypedTag, error) {
175174

176175
saveInt := func() error {
177176
s := buf.String()
178-
if _, err := strconv.ParseInt(s, 0, 64); err != nil {
179-
return fmt.Errorf("invalid number %q", s)
180-
} else {
181-
cur.Value = s
182-
cur.Type = ArgTypeInt
183-
}
177+
cur.Value = s
178+
cur.Type = ArgTypeInt
184179
args = append(args, cur)
185180
cur = Arg{}
186181
buf.Reset()

v2/codetags/scanner.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package codetags
1919
import (
2020
"bytes"
2121
"fmt"
22+
"strconv"
2223
"strings"
2324
"unicode"
2425
)
@@ -79,7 +80,7 @@ parseLoop:
7980
buf.WriteRune(s.next())
8081
st = stNumber
8182
default:
82-
break parseLoop
83+
return "", fmt.Errorf("expected number at position %d", s.pos)
8384
}
8485
case stPrefixNumber:
8586
switch {
@@ -110,7 +111,11 @@ parseLoop:
110111
if incomplete {
111112
return "", fmt.Errorf("unterminated number at position %d", s.pos)
112113
}
113-
return buf.String(), nil
114+
numStr := buf.String()
115+
if _, err := strconv.ParseInt(numStr, 0, 64); err != nil {
116+
return "", fmt.Errorf("invalid number %q at position %d", numStr, s.pos)
117+
}
118+
return numStr, nil
114119
}
115120

116121
const (
@@ -182,6 +187,8 @@ parseLoop:
182187
case isIdentBegin(r):
183188
buf.WriteRune(s.next())
184189
st = stInterior
190+
default:
191+
return "", fmt.Errorf("expected identifier at position %d", s.pos)
185192
}
186193
case stInterior:
187194
switch {

v2/codetags/scanner_test.go

Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
/*
2+
Copyright 2025 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package codetags
18+
19+
import (
20+
"testing"
21+
)
22+
23+
func TestScannerBasics(t *testing.T) {
24+
tests := []struct {
25+
name string
26+
input string
27+
want []rune
28+
}{
29+
{
30+
name: "empty",
31+
input: "",
32+
want: []rune{EOF},
33+
},
34+
{
35+
name: "single character",
36+
input: "a",
37+
want: []rune{'a', EOF},
38+
},
39+
{
40+
name: "multiple characters",
41+
input: "abc",
42+
want: []rune{'a', 'b', 'c', EOF},
43+
},
44+
{
45+
name: "unicode characters",
46+
input: "αβγ",
47+
want: []rune{'α', 'β', 'γ', EOF},
48+
},
49+
}
50+
51+
for _, tt := range tests {
52+
t.Run(tt.name, func(t *testing.T) {
53+
s := scanner{buf: []rune(tt.input)}
54+
55+
// next()
56+
for _, want := range tt.want {
57+
got := s.next()
58+
if got != want {
59+
t.Errorf("next() = %v, want %v", got, want)
60+
}
61+
}
62+
63+
// peek()
64+
s = scanner{buf: []rune(tt.input)}
65+
for i, want := range tt.want {
66+
got := s.peek()
67+
if got != want {
68+
t.Errorf("peek() at position %d = %v, want %v", i, got, want)
69+
}
70+
s.next() // Advance scanner
71+
}
72+
73+
// peekN()
74+
if len(tt.input) > 1 {
75+
s = scanner{buf: []rune(tt.input)}
76+
got := s.peekN(1)
77+
want := []rune(tt.input)[1]
78+
if got != want {
79+
t.Errorf("peekN(1) at position 0 = %v, want %v", got, want)
80+
}
81+
}
82+
})
83+
}
84+
}
85+
86+
func TestNextNumber(t *testing.T) {
87+
tests := []struct {
88+
name string
89+
input string
90+
want string
91+
wantErr bool
92+
}{
93+
{
94+
name: "simple integer",
95+
input: "123",
96+
},
97+
{
98+
name: "negative integer",
99+
input: "-123",
100+
},
101+
{
102+
name: "positive integer with plus sign",
103+
input: "+123",
104+
want: "123",
105+
},
106+
{
107+
name: "zero",
108+
input: "0",
109+
},
110+
{
111+
name: "hex number",
112+
input: "0xFF",
113+
},
114+
{
115+
name: "octal number",
116+
input: "0o77",
117+
},
118+
{
119+
name: "binary number",
120+
input: "0b101",
121+
},
122+
{
123+
name: "incomplete hex",
124+
input: "0x",
125+
wantErr: true,
126+
},
127+
{
128+
name: "incomplete octal",
129+
input: "0o",
130+
wantErr: true,
131+
},
132+
{
133+
name: "incomplete binary",
134+
input: "0b",
135+
wantErr: true,
136+
},
137+
{
138+
name: "number followed by non-digit",
139+
input: "123abc",
140+
wantErr: true,
141+
},
142+
}
143+
144+
for _, tt := range tests {
145+
t.Run(tt.name, func(t *testing.T) {
146+
if tt.want == "" {
147+
tt.want = tt.input
148+
}
149+
s := scanner{buf: []rune(tt.input)}
150+
got, err := s.nextNumber()
151+
152+
if (err != nil) != tt.wantErr {
153+
t.Errorf("nextNumber() error = %v, wantErr %v", err, tt.wantErr)
154+
return
155+
}
156+
157+
if !tt.wantErr && got != tt.want {
158+
t.Errorf("nextNumber() = %v, want %v", got, tt.want)
159+
}
160+
})
161+
}
162+
}
163+
164+
func TestNextString(t *testing.T) {
165+
tests := []struct {
166+
name string
167+
input string
168+
want string
169+
wantErr bool
170+
}{
171+
{
172+
name: "double quoted string",
173+
input: `"hello"`,
174+
want: "hello",
175+
},
176+
{
177+
name: "backtick quoted string",
178+
input: "`hello`",
179+
want: "hello",
180+
},
181+
{
182+
name: "empty double quoted string",
183+
input: `""`,
184+
want: "",
185+
},
186+
{
187+
name: "empty backtick quoted string",
188+
input: "``",
189+
want: "",
190+
},
191+
{
192+
name: "string with escaped quote",
193+
input: `"hello \"world\""`,
194+
want: `hello "world"`,
195+
},
196+
{
197+
name: "string with escaped backslash",
198+
input: `"hello \\world"`,
199+
want: `hello \world`,
200+
},
201+
{
202+
name: "unterminated double quoted string",
203+
input: `"hello`,
204+
wantErr: true,
205+
},
206+
{
207+
name: "unterminated backtick quoted string",
208+
input: "`hello",
209+
wantErr: true,
210+
},
211+
{
212+
name: "invalid escape sequence",
213+
input: `"hello \n world"`,
214+
wantErr: true,
215+
},
216+
{
217+
name: "string followed by other content",
218+
input: `"hello" world`,
219+
want: "hello",
220+
},
221+
}
222+
223+
for _, tt := range tests {
224+
t.Run(tt.name, func(t *testing.T) {
225+
s := scanner{buf: []rune(tt.input)}
226+
got, err := s.nextString()
227+
228+
if (err != nil) != tt.wantErr {
229+
t.Errorf("nextString() error = %v, wantErr %v", err, tt.wantErr)
230+
return
231+
}
232+
233+
if !tt.wantErr && got != tt.want {
234+
t.Errorf("nextString() = %v, want %v", got, tt.want)
235+
}
236+
})
237+
}
238+
}
239+
240+
func TestNextIdent(t *testing.T) {
241+
tests := []struct {
242+
name string
243+
input string
244+
isInteriorFn func(rune) bool
245+
want string
246+
wantErr bool
247+
}{
248+
{
249+
name: "simple identifier with isIdentInterior",
250+
input: "abc",
251+
want: "abc",
252+
},
253+
{
254+
name: "identifier with underscore using isIdentInterior",
255+
input: "abc_def",
256+
want: "abc_def",
257+
},
258+
{
259+
name: "identifier with dash using isIdentInterior",
260+
input: "abc-def",
261+
want: "abc-def",
262+
},
263+
{
264+
name: "identifier with dot using isIdentInterior",
265+
input: "abc.def",
266+
want: "abc.def",
267+
},
268+
{
269+
name: "identifier with numbers using isIdentInterior",
270+
input: "abc123",
271+
want: "abc123",
272+
},
273+
{
274+
name: "identifier with colon using isIdentInterior",
275+
input: "abc:def",
276+
want: "abc",
277+
},
278+
{
279+
name: "identifier followed by invalid character",
280+
input: "abc@def",
281+
want: "abc",
282+
},
283+
{
284+
name: "identifier starting with underscore",
285+
input: "_abc",
286+
want: "_abc",
287+
},
288+
{
289+
name: "identifier starting with number",
290+
input: "123abc",
291+
want: "",
292+
wantErr: true,
293+
},
294+
295+
{
296+
name: "identifier with colon using isTagNameInterior",
297+
input: "abc:def",
298+
isInteriorFn: isTagNameInterior,
299+
want: "abc:def",
300+
},
301+
}
302+
303+
for _, tt := range tests {
304+
t.Run(tt.name, func(t *testing.T) {
305+
s := scanner{buf: []rune(tt.input)}
306+
var got string
307+
var err error
308+
309+
if tt.isInteriorFn == nil {
310+
tt.isInteriorFn = isIdentInterior
311+
}
312+
got, err = s.nextIdent(tt.isInteriorFn)
313+
314+
if (err != nil) != tt.wantErr {
315+
t.Errorf("nextIdent() error = %v, wantErr %v", err, tt.wantErr)
316+
return
317+
}
318+
319+
if !tt.wantErr && got != tt.want {
320+
t.Errorf("nextIdent() = %v, want %v", got, tt.want)
321+
}
322+
})
323+
}
324+
}

0 commit comments

Comments
 (0)