Source file src/cmd/internal/test2json/test2json_test.go

     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  
     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  			// If there's a corresponding *.src script, execute it
    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  			// Test one line written to c at a time.
    70  			// Assume that's the most likely to be handled correctly.
    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  				// If the line-at-a-time conversion fails, no point testing boundary conditions.
    95  				return
    96  			}
    97  
    98  			// Write entire input in bulk.
    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  			// In bulk again with \r\n.
   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  			// Write 2 bytes at a time on even boundaries.
   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  			// Write 2 bytes at a time on odd boundaries.
   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  			// Test with very small output buffers, to check that
   154  			// UTF8 sequences are not broken up.
   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  	// Unpack archive.
   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  		// Defer writing to the test log in case the script engine panics during execution,
   198  		// but write the log before we write the final "skip" or "FAIL" line.
   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  			// Add the environment to the start of the script log.
   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  	// Remove the output after "=== NAME"
   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  // writeAndKill writes b to w and then fills b with Zs.
   250  // The filling makes sure that if w is holding onto b for
   251  // future use, that future use will have obviously wrong data.
   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  // diffJSON diffs the stream we have against the stream we want
   260  // and fails the test with a useful message if they don't match.
   261  func diffJSON(t *testing.T, have, want []byte) {
   262  	t.Helper()
   263  	type event map[string]any
   264  
   265  	// Parse into events, one per line.
   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  	// Make sure the events we have match the events we want.
   292  	// At each step we're matching haveEvents[i] against wantEvents[j].
   293  	// i and j can move independently due to choices about exactly
   294  	// how to break up text in "output" events.
   295  	i := 0
   296  	j := 0
   297  
   298  	// Fail reports a failure at the current i,j and stops the test.
   299  	// It shows the events around the current positions,
   300  	// with the current positions marked.
   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  				// show marker after end of input
   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             // current "Test" key in "output" events
   326  	var wantOutput, haveOutput string // collected "Output" of those events
   327  
   328  	// getTest returns the "Test" setting, or "" if it is missing.
   329  	getTest := func(e event) string {
   330  		s, _ := e["Test"].(string)
   331  		return s
   332  	}
   333  
   334  	// checkOutput collects output from the haveEvents for the current outputTest
   335  	// and then checks that the collected output matches the wanted output.
   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  	// Walk through wantEvents matching against haveEvents.
   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  	// Replace durations (e.g. 0.01s) with a placeholder
   386  	have = reRuntime.ReplaceAll(have, []byte("X.XXs"))
   387  	want = reRuntime.ReplaceAll(want, []byte("X.XXs"))
   388  
   389  	// Compare
   390  	if bytes.Equal(have, want) {
   391  		return
   392  	}
   393  
   394  	// Escape non-printing characters to make the error more legible
   395  	have = escapeNonPrinting(have)
   396  	want = escapeNonPrinting(want)
   397  
   398  	// Find where the output differs and remember the last newline
   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" // α is 2-byte, ☺ is 3-byte, 😂 is 4-byte
   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