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