• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2017 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package test2json
6
7import (
8	"bytes"
9	"encoding/json"
10	"flag"
11	"fmt"
12	"io"
13	"os"
14	"path/filepath"
15	"reflect"
16	"strings"
17	"testing"
18	"unicode/utf8"
19)
20
21var update = flag.Bool("update", false, "rewrite testdata/*.json files")
22
23func TestGolden(t *testing.T) {
24	files, err := filepath.Glob("testdata/*.test")
25	if err != nil {
26		t.Fatal(err)
27	}
28	for _, file := range files {
29		name := strings.TrimSuffix(filepath.Base(file), ".test")
30		t.Run(name, func(t *testing.T) {
31			orig, err := os.ReadFile(file)
32			if err != nil {
33				t.Fatal(err)
34			}
35
36			// Test one line written to c at a time.
37			// Assume that's the most likely to be handled correctly.
38			var buf bytes.Buffer
39			c := NewConverter(&buf, "", 0)
40			in := append([]byte{}, orig...)
41			for _, line := range bytes.SplitAfter(in, []byte("\n")) {
42				writeAndKill(c, line)
43			}
44			c.Close()
45
46			if *update {
47				js := strings.TrimSuffix(file, ".test") + ".json"
48				t.Logf("rewriting %s", js)
49				if err := os.WriteFile(js, buf.Bytes(), 0666); err != nil {
50					t.Fatal(err)
51				}
52				return
53			}
54
55			want, err := os.ReadFile(strings.TrimSuffix(file, ".test") + ".json")
56			if err != nil {
57				t.Fatal(err)
58			}
59			diffJSON(t, buf.Bytes(), want)
60			if t.Failed() {
61				// If the line-at-a-time conversion fails, no point testing boundary conditions.
62				return
63			}
64
65			// Write entire input in bulk.
66			t.Run("bulk", func(t *testing.T) {
67				buf.Reset()
68				c = NewConverter(&buf, "", 0)
69				in = append([]byte{}, orig...)
70				writeAndKill(c, in)
71				c.Close()
72				diffJSON(t, buf.Bytes(), want)
73			})
74
75			// In bulk again with \r\n.
76			t.Run("crlf", func(t *testing.T) {
77				buf.Reset()
78				c = NewConverter(&buf, "", 0)
79				in = bytes.ReplaceAll(orig, []byte("\n"), []byte("\r\n"))
80				writeAndKill(c, in)
81				c.Close()
82				diffJSON(t, bytes.ReplaceAll(buf.Bytes(), []byte(`\r\n`), []byte(`\n`)), want)
83			})
84
85			// Write 2 bytes at a time on even boundaries.
86			t.Run("even2", func(t *testing.T) {
87				buf.Reset()
88				c = NewConverter(&buf, "", 0)
89				in = append([]byte{}, orig...)
90				for i := 0; i < len(in); i += 2 {
91					if i+2 <= len(in) {
92						writeAndKill(c, in[i:i+2])
93					} else {
94						writeAndKill(c, in[i:])
95					}
96				}
97				c.Close()
98				diffJSON(t, buf.Bytes(), want)
99			})
100
101			// Write 2 bytes at a time on odd boundaries.
102			t.Run("odd2", func(t *testing.T) {
103				buf.Reset()
104				c = NewConverter(&buf, "", 0)
105				in = append([]byte{}, orig...)
106				if len(in) > 0 {
107					writeAndKill(c, in[:1])
108				}
109				for i := 1; i < len(in); i += 2 {
110					if i+2 <= len(in) {
111						writeAndKill(c, in[i:i+2])
112					} else {
113						writeAndKill(c, in[i:])
114					}
115				}
116				c.Close()
117				diffJSON(t, buf.Bytes(), want)
118			})
119
120			// Test with very small output buffers, to check that
121			// UTF8 sequences are not broken up.
122			for b := 5; b <= 8; b++ {
123				t.Run(fmt.Sprintf("tiny%d", b), func(t *testing.T) {
124					oldIn := inBuffer
125					oldOut := outBuffer
126					defer func() {
127						inBuffer = oldIn
128						outBuffer = oldOut
129					}()
130					inBuffer = 64
131					outBuffer = b
132					buf.Reset()
133					c = NewConverter(&buf, "", 0)
134					in = append([]byte{}, orig...)
135					writeAndKill(c, in)
136					c.Close()
137					diffJSON(t, buf.Bytes(), want)
138				})
139			}
140		})
141	}
142}
143
144// writeAndKill writes b to w and then fills b with Zs.
145// The filling makes sure that if w is holding onto b for
146// future use, that future use will have obviously wrong data.
147func writeAndKill(w io.Writer, b []byte) {
148	w.Write(b)
149	for i := range b {
150		b[i] = 'Z'
151	}
152}
153
154// diffJSON diffs the stream we have against the stream we want
155// and fails the test with a useful message if they don't match.
156func diffJSON(t *testing.T, have, want []byte) {
157	t.Helper()
158	type event map[string]any
159
160	// Parse into events, one per line.
161	parseEvents := func(b []byte) ([]event, []string) {
162		t.Helper()
163		var events []event
164		var lines []string
165		for _, line := range bytes.SplitAfter(b, []byte("\n")) {
166			if len(line) > 0 {
167				line = bytes.TrimSpace(line)
168				var e event
169				err := json.Unmarshal(line, &e)
170				if err != nil {
171					t.Errorf("unmarshal %s: %v", b, err)
172					continue
173				}
174				events = append(events, e)
175				lines = append(lines, string(line))
176			}
177		}
178		return events, lines
179	}
180	haveEvents, haveLines := parseEvents(have)
181	wantEvents, wantLines := parseEvents(want)
182	if t.Failed() {
183		return
184	}
185
186	// Make sure the events we have match the events we want.
187	// At each step we're matching haveEvents[i] against wantEvents[j].
188	// i and j can move independently due to choices about exactly
189	// how to break up text in "output" events.
190	i := 0
191	j := 0
192
193	// Fail reports a failure at the current i,j and stops the test.
194	// It shows the events around the current positions,
195	// with the current positions marked.
196	fail := func() {
197		var buf bytes.Buffer
198		show := func(i int, lines []string) {
199			for k := -2; k < 5; k++ {
200				marker := ""
201				if k == 0 {
202					marker = "» "
203				}
204				if 0 <= i+k && i+k < len(lines) {
205					fmt.Fprintf(&buf, "\t%s%s\n", marker, lines[i+k])
206				}
207			}
208			if i >= len(lines) {
209				// show marker after end of input
210				fmt.Fprintf(&buf, "\t» \n")
211			}
212		}
213		fmt.Fprintf(&buf, "have:\n")
214		show(i, haveLines)
215		fmt.Fprintf(&buf, "want:\n")
216		show(j, wantLines)
217		t.Fatal(buf.String())
218	}
219
220	var outputTest string             // current "Test" key in "output" events
221	var wantOutput, haveOutput string // collected "Output" of those events
222
223	// getTest returns the "Test" setting, or "" if it is missing.
224	getTest := func(e event) string {
225		s, _ := e["Test"].(string)
226		return s
227	}
228
229	// checkOutput collects output from the haveEvents for the current outputTest
230	// and then checks that the collected output matches the wanted output.
231	checkOutput := func() {
232		for i < len(haveEvents) && haveEvents[i]["Action"] == "output" && getTest(haveEvents[i]) == outputTest {
233			haveOutput += haveEvents[i]["Output"].(string)
234			i++
235		}
236		if haveOutput != wantOutput {
237			t.Errorf("output mismatch for Test=%q:\nhave %q\nwant %q", outputTest, haveOutput, wantOutput)
238			fail()
239		}
240		haveOutput = ""
241		wantOutput = ""
242	}
243
244	// Walk through wantEvents matching against haveEvents.
245	for j = range wantEvents {
246		e := wantEvents[j]
247		if e["Action"] == "output" && getTest(e) == outputTest {
248			wantOutput += e["Output"].(string)
249			continue
250		}
251		checkOutput()
252		if e["Action"] == "output" {
253			outputTest = getTest(e)
254			wantOutput += e["Output"].(string)
255			continue
256		}
257		if i >= len(haveEvents) {
258			t.Errorf("early end of event stream: missing event")
259			fail()
260		}
261		if !reflect.DeepEqual(haveEvents[i], e) {
262			t.Errorf("events out of sync")
263			fail()
264		}
265		i++
266	}
267	checkOutput()
268	if i < len(haveEvents) {
269		t.Errorf("extra events in stream")
270		fail()
271	}
272}
273
274func TestTrimUTF8(t *testing.T) {
275	s := "hello α ☺ �� world" // α is 2-byte, ☺ is 3-byte, �� is 4-byte
276	b := []byte(s)
277	for i := 0; i < len(s); i++ {
278		j := trimUTF8(b[:i])
279		u := string([]rune(s[:j])) + string([]rune(s[j:]))
280		if u != s {
281			t.Errorf("trimUTF8(%q) = %d (-%d), not at boundary (split: %q %q)", s[:i], j, i-j, s[:j], s[j:])
282		}
283		if utf8.FullRune(b[j:i]) {
284			t.Errorf("trimUTF8(%q) = %d (-%d), too early (missed: %q)", s[:j], j, i-j, s[j:i])
285		}
286	}
287}
288