1
2
3
4
5 package test2json
6
7 import (
8 "bufio"
9 "bytes"
10 "cmd/internal/script"
11 "cmd/internal/script/scripttest"
12 "context"
13 "encoding/json"
14 "errors"
15 "flag"
16 "fmt"
17 "internal/txtar"
18 "io"
19 "io/fs"
20 "os"
21 "path/filepath"
22 "reflect"
23 "regexp"
24 "strings"
25 "testing"
26 "unicode/utf8"
27 )
28
29 var update = flag.Bool("update", false, "rewrite testdata/*.json files")
30
31 func TestGolden(t *testing.T) {
32 ctx := scripttest.ScriptTestContext(t, context.Background())
33 engine, env := scripttest.NewEngine(t, nil)
34 files, err := filepath.Glob("testdata/*.test")
35 if err != nil {
36 t.Fatal(err)
37 }
38 for _, file := range files {
39 name := strings.TrimSuffix(filepath.Base(file), ".test")
40 t.Run(name, func(t *testing.T) {
41 orig, err := os.ReadFile(file)
42 if err != nil {
43 t.Fatal(err)
44 }
45
46
47 srcFile := strings.TrimSuffix(file, ".test") + ".src"
48 if st, err := os.Stat(srcFile); err != nil {
49 if !errors.Is(err, fs.ErrNotExist) {
50 t.Fatal(err)
51 }
52 } else if !st.IsDir() {
53 t.Run("go test", func(t *testing.T) {
54 stdout := runTest(t, ctx, engine, env, srcFile)
55
56 if *update {
57 t.Logf("rewriting %s", file)
58 if err := os.WriteFile(file, []byte(stdout), 0666); err != nil {
59 t.Fatal(err)
60 }
61 orig = []byte(stdout)
62 return
63 }
64
65 diffRaw(t, []byte(stdout), orig)
66 })
67 }
68
69
70
71 var buf bytes.Buffer
72 c := NewConverter(&buf, "", 0)
73 in := append([]byte{}, orig...)
74 for _, line := range bytes.SplitAfter(in, []byte("\n")) {
75 writeAndKill(c, line)
76 }
77 c.Close()
78
79 if *update {
80 js := strings.TrimSuffix(file, ".test") + ".json"
81 t.Logf("rewriting %s", js)
82 if err := os.WriteFile(js, buf.Bytes(), 0666); err != nil {
83 t.Fatal(err)
84 }
85 return
86 }
87
88 want, err := os.ReadFile(strings.TrimSuffix(file, ".test") + ".json")
89 if err != nil {
90 t.Fatal(err)
91 }
92 diffJSON(t, buf.Bytes(), want)
93 if t.Failed() {
94
95 return
96 }
97
98
99 t.Run("bulk", func(t *testing.T) {
100 buf.Reset()
101 c = NewConverter(&buf, "", 0)
102 in = append([]byte{}, orig...)
103 writeAndKill(c, in)
104 c.Close()
105 diffJSON(t, buf.Bytes(), want)
106 })
107
108
109 t.Run("crlf", func(t *testing.T) {
110 buf.Reset()
111 c = NewConverter(&buf, "", 0)
112 in = bytes.ReplaceAll(orig, []byte("\n"), []byte("\r\n"))
113 writeAndKill(c, in)
114 c.Close()
115 diffJSON(t, bytes.ReplaceAll(buf.Bytes(), []byte(`\r\n`), []byte(`\n`)), want)
116 })
117
118
119 t.Run("even2", func(t *testing.T) {
120 buf.Reset()
121 c = NewConverter(&buf, "", 0)
122 in = append([]byte{}, orig...)
123 for i := 0; i < len(in); i += 2 {
124 if i+2 <= len(in) {
125 writeAndKill(c, in[i:i+2])
126 } else {
127 writeAndKill(c, in[i:])
128 }
129 }
130 c.Close()
131 diffJSON(t, buf.Bytes(), want)
132 })
133
134
135 t.Run("odd2", func(t *testing.T) {
136 buf.Reset()
137 c = NewConverter(&buf, "", 0)
138 in = append([]byte{}, orig...)
139 if len(in) > 0 {
140 writeAndKill(c, in[:1])
141 }
142 for i := 1; i < len(in); i += 2 {
143 if i+2 <= len(in) {
144 writeAndKill(c, in[i:i+2])
145 } else {
146 writeAndKill(c, in[i:])
147 }
148 }
149 c.Close()
150 diffJSON(t, buf.Bytes(), want)
151 })
152
153
154
155 for b := 5; b <= 8; b++ {
156 t.Run(fmt.Sprintf("tiny%d", b), func(t *testing.T) {
157 oldIn := inBuffer
158 oldOut := outBuffer
159 defer func() {
160 inBuffer = oldIn
161 outBuffer = oldOut
162 }()
163 inBuffer = 64
164 outBuffer = b
165 buf.Reset()
166 c = NewConverter(&buf, "", 0)
167 in = append([]byte{}, orig...)
168 writeAndKill(c, in)
169 c.Close()
170 diffJSON(t, buf.Bytes(), want)
171 })
172 }
173 })
174 }
175 }
176
177 func runTest(t *testing.T, ctx context.Context, engine *script.Engine, env []string, srcFile string) string {
178 workdir := t.TempDir()
179 s, err := script.NewState(ctx, workdir, env)
180 if err != nil {
181 t.Fatal(err)
182 }
183
184
185 a, err := txtar.ParseFile(srcFile)
186 if err != nil {
187 t.Fatal(err)
188 }
189 scripttest.InitScriptDirs(t, s)
190 if err := s.ExtractFiles(a); err != nil {
191 t.Fatal(err)
192 }
193
194 err, stdout := func() (err error, stdout string) {
195 log := new(strings.Builder)
196
197
198
199 t.Helper()
200 defer func() {
201 t.Helper()
202
203 stdout = s.Stdout()
204 if closeErr := s.CloseAndWait(log); err == nil {
205 err = closeErr
206 }
207
208 if log.Len() > 0 && (testing.Verbose() || err != nil) {
209 t.Log(strings.TrimSuffix(log.String(), "\n"))
210 }
211 }()
212
213 if testing.Verbose() {
214
215 wait, err := script.Env().Run(s)
216 if err != nil {
217 t.Fatal(err)
218 }
219 if wait != nil {
220 stdout, stderr, err := wait(s)
221 if err != nil {
222 t.Fatalf("env: %v\n%s", err, stderr)
223 }
224 if len(stdout) > 0 {
225 s.Logf("%s\n", stdout)
226 }
227 }
228 }
229
230 testScript := bytes.NewReader(a.Comment)
231 err = engine.Execute(s, srcFile, bufio.NewReader(testScript), log)
232 return
233 }()
234 if skip := (scripttest.SkipError{}); errors.As(err, &skip) {
235 t.Skipf("SKIP: %v", skip)
236 } else if err != nil {
237 t.Fatalf("FAIL: %v", err)
238 }
239
240
241 i := strings.LastIndex(stdout, "\n\x16=== NAME")
242 if i >= 0 {
243 stdout = stdout[:i+1]
244 }
245
246 return stdout
247 }
248
249
250
251
252 func writeAndKill(w io.Writer, b []byte) {
253 w.Write(b)
254 for i := range b {
255 b[i] = 'Z'
256 }
257 }
258
259
260
261 func diffJSON(t *testing.T, have, want []byte) {
262 t.Helper()
263 type event map[string]any
264
265
266 parseEvents := func(b []byte) ([]event, []string) {
267 t.Helper()
268 var events []event
269 var lines []string
270 for _, line := range bytes.SplitAfter(b, []byte("\n")) {
271 if len(line) > 0 {
272 line = bytes.TrimSpace(line)
273 var e event
274 err := json.Unmarshal(line, &e)
275 if err != nil {
276 t.Errorf("unmarshal %s: %v", b, err)
277 continue
278 }
279 events = append(events, e)
280 lines = append(lines, string(line))
281 }
282 }
283 return events, lines
284 }
285 haveEvents, haveLines := parseEvents(have)
286 wantEvents, wantLines := parseEvents(want)
287 if t.Failed() {
288 return
289 }
290
291
292
293
294
295 i := 0
296 j := 0
297
298
299
300
301 fail := func() {
302 var buf bytes.Buffer
303 show := func(i int, lines []string) {
304 for k := -2; k < 5; k++ {
305 marker := ""
306 if k == 0 {
307 marker = "» "
308 }
309 if 0 <= i+k && i+k < len(lines) {
310 fmt.Fprintf(&buf, "\t%s%s\n", marker, lines[i+k])
311 }
312 }
313 if i >= len(lines) {
314
315 fmt.Fprintf(&buf, "\t» \n")
316 }
317 }
318 fmt.Fprintf(&buf, "have:\n")
319 show(i, haveLines)
320 fmt.Fprintf(&buf, "want:\n")
321 show(j, wantLines)
322 t.Fatal(buf.String())
323 }
324
325 var outputTest string
326 var wantOutput, haveOutput string
327
328
329 getTest := func(e event) string {
330 s, _ := e["Test"].(string)
331 return s
332 }
333
334
335
336 checkOutput := func() {
337 for i < len(haveEvents) && haveEvents[i]["Action"] == "output" && getTest(haveEvents[i]) == outputTest {
338 haveOutput += haveEvents[i]["Output"].(string)
339 i++
340 }
341 if haveOutput != wantOutput {
342 t.Errorf("output mismatch for Test=%q:\nhave %q\nwant %q", outputTest, haveOutput, wantOutput)
343 fail()
344 }
345 haveOutput = ""
346 wantOutput = ""
347 }
348
349
350 for j = range wantEvents {
351 e := wantEvents[j]
352 if e["Action"] == "output" && getTest(e) == outputTest {
353 wantOutput += e["Output"].(string)
354 continue
355 }
356 checkOutput()
357 if e["Action"] == "output" {
358 outputTest = getTest(e)
359 wantOutput += e["Output"].(string)
360 continue
361 }
362 if i >= len(haveEvents) {
363 t.Errorf("early end of event stream: missing event")
364 fail()
365 }
366 if !reflect.DeepEqual(haveEvents[i], e) {
367 t.Errorf("events out of sync")
368 fail()
369 }
370 i++
371 }
372 checkOutput()
373 if i < len(haveEvents) {
374 t.Errorf("extra events in stream")
375 fail()
376 }
377 }
378
379 var reRuntime = regexp.MustCompile(`\d*\.\d*s`)
380
381 func diffRaw(t *testing.T, have, want []byte) {
382 have = bytes.TrimSpace(have)
383 want = bytes.TrimSpace(want)
384
385
386 have = reRuntime.ReplaceAll(have, []byte("X.XXs"))
387 want = reRuntime.ReplaceAll(want, []byte("X.XXs"))
388
389
390 if bytes.Equal(have, want) {
391 return
392 }
393
394
395 have = escapeNonPrinting(have)
396 want = escapeNonPrinting(want)
397
398
399 var i, nl int
400 for i < len(have) && i < len(want) && have[i] == want[i] {
401 if have[i] == '\n' {
402 nl = i
403 }
404 }
405
406 if nl == 0 {
407 t.Fatalf("\nhave:\n%s\nwant:\n%s", have, want)
408 } else {
409 nl++
410 t.Fatalf("\nhave:\n%s» %s\nwant:\n%s» %s", have[:nl], have[nl:], want[:nl], want[nl:])
411 }
412 }
413
414 func escapeNonPrinting(buf []byte) []byte {
415 for i := 0; i < len(buf); i++ {
416 c := buf[i]
417 if 0x20 <= c && c < 0x7F || c > 0x7F || c == '\n' {
418 continue
419 }
420 escaped := fmt.Sprintf(`\x%02x`, c)
421 buf = append(buf[:i+len(escaped)], buf[i+1:]...)
422 for j := 0; j < len(escaped); j++ {
423 buf[i+j] = escaped[j]
424 }
425 }
426 return buf
427 }
428
429 func TestTrimUTF8(t *testing.T) {
430 s := "hello α ☺ 😂 world"
431 b := []byte(s)
432 for i := 0; i < len(s); i++ {
433 j := trimUTF8(b[:i])
434 u := string([]rune(s[:j])) + string([]rune(s[j:]))
435 if u != s {
436 t.Errorf("trimUTF8(%q) = %d (-%d), not at boundary (split: %q %q)", s[:i], j, i-j, s[:j], s[j:])
437 }
438 if utf8.FullRune(b[j:i]) {
439 t.Errorf("trimUTF8(%q) = %d (-%d), too early (missed: %q)", s[:j], j, i-j, s[j:i])
440 }
441 }
442 }
443
View as plain text