github.1485827954.workers.dev/ethereum/go-ethereum@v1.14.3/cmd/evm/t8n_test.go (about)

     1  // Copyright 2021 The go-ethereum Authors
     2  // This file is part of go-ethereum.
     3  //
     4  // go-ethereum is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // go-ethereum is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU General Public License
    15  // along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package main
    18  
    19  import (
    20  	"bufio"
    21  	"encoding/json"
    22  	"fmt"
    23  	"io"
    24  	"os"
    25  	"path/filepath"
    26  	"reflect"
    27  	"strings"
    28  	"testing"
    29  
    30  	"github.com/ethereum/go-ethereum/cmd/evm/internal/t8ntool"
    31  	"github.com/ethereum/go-ethereum/internal/cmdtest"
    32  	"github.com/ethereum/go-ethereum/internal/reexec"
    33  )
    34  
    35  func TestMain(m *testing.M) {
    36  	// Run the app if we've been exec'd as "ethkey-test" in runEthkey.
    37  	reexec.Register("evm-test", func() {
    38  		if err := app.Run(os.Args); err != nil {
    39  			fmt.Fprintln(os.Stderr, err)
    40  			os.Exit(1)
    41  		}
    42  		os.Exit(0)
    43  	})
    44  	// check if we have been reexec'd
    45  	if reexec.Init() {
    46  		return
    47  	}
    48  	os.Exit(m.Run())
    49  }
    50  
    51  type testT8n struct {
    52  	*cmdtest.TestCmd
    53  }
    54  
    55  type t8nInput struct {
    56  	inAlloc  string
    57  	inTxs    string
    58  	inEnv    string
    59  	stFork   string
    60  	stReward string
    61  }
    62  
    63  func (args *t8nInput) get(base string) []string {
    64  	var out []string
    65  	if opt := args.inAlloc; opt != "" {
    66  		out = append(out, "--input.alloc")
    67  		out = append(out, fmt.Sprintf("%v/%v", base, opt))
    68  	}
    69  	if opt := args.inTxs; opt != "" {
    70  		out = append(out, "--input.txs")
    71  		out = append(out, fmt.Sprintf("%v/%v", base, opt))
    72  	}
    73  	if opt := args.inEnv; opt != "" {
    74  		out = append(out, "--input.env")
    75  		out = append(out, fmt.Sprintf("%v/%v", base, opt))
    76  	}
    77  	if opt := args.stFork; opt != "" {
    78  		out = append(out, "--state.fork", opt)
    79  	}
    80  	if opt := args.stReward; opt != "" {
    81  		out = append(out, "--state.reward", opt)
    82  	}
    83  	return out
    84  }
    85  
    86  type t8nOutput struct {
    87  	alloc  bool
    88  	result bool
    89  	body   bool
    90  }
    91  
    92  func (args *t8nOutput) get() (out []string) {
    93  	if args.body {
    94  		out = append(out, "--output.body", "stdout")
    95  	} else {
    96  		out = append(out, "--output.body", "") // empty means ignore
    97  	}
    98  	if args.result {
    99  		out = append(out, "--output.result", "stdout")
   100  	} else {
   101  		out = append(out, "--output.result", "")
   102  	}
   103  	if args.alloc {
   104  		out = append(out, "--output.alloc", "stdout")
   105  	} else {
   106  		out = append(out, "--output.alloc", "")
   107  	}
   108  	return out
   109  }
   110  
   111  func TestT8n(t *testing.T) {
   112  	t.Parallel()
   113  	tt := new(testT8n)
   114  	tt.TestCmd = cmdtest.NewTestCmd(t, tt)
   115  	for i, tc := range []struct {
   116  		base        string
   117  		input       t8nInput
   118  		output      t8nOutput
   119  		expExitCode int
   120  		expOut      string
   121  	}{
   122  		{ // Test exit (3) on bad config
   123  			base: "./testdata/1",
   124  			input: t8nInput{
   125  				"alloc.json", "txs.json", "env.json", "Frontier+1346", "",
   126  			},
   127  			output:      t8nOutput{alloc: true, result: true},
   128  			expExitCode: 3,
   129  		},
   130  		{
   131  			base: "./testdata/1",
   132  			input: t8nInput{
   133  				"alloc.json", "txs.json", "env.json", "Byzantium", "",
   134  			},
   135  			output: t8nOutput{alloc: true, result: true},
   136  			expOut: "exp.json",
   137  		},
   138  		{ // blockhash test
   139  			base: "./testdata/3",
   140  			input: t8nInput{
   141  				"alloc.json", "txs.json", "env.json", "Berlin", "",
   142  			},
   143  			output: t8nOutput{alloc: true, result: true},
   144  			expOut: "exp.json",
   145  		},
   146  		{ // missing blockhash test
   147  			base: "./testdata/4",
   148  			input: t8nInput{
   149  				"alloc.json", "txs.json", "env.json", "Berlin", "",
   150  			},
   151  			output:      t8nOutput{alloc: true, result: true},
   152  			expExitCode: 4,
   153  		},
   154  		{ // Uncle test
   155  			base: "./testdata/5",
   156  			input: t8nInput{
   157  				"alloc.json", "txs.json", "env.json", "Byzantium", "0x80",
   158  			},
   159  			output: t8nOutput{alloc: true, result: true},
   160  			expOut: "exp.json",
   161  		},
   162  		{ // Sign json transactions
   163  			base: "./testdata/13",
   164  			input: t8nInput{
   165  				"alloc.json", "txs.json", "env.json", "London", "",
   166  			},
   167  			output: t8nOutput{body: true},
   168  			expOut: "exp.json",
   169  		},
   170  		{ // Already signed transactions
   171  			base: "./testdata/13",
   172  			input: t8nInput{
   173  				"alloc.json", "signed_txs.rlp", "env.json", "London", "",
   174  			},
   175  			output: t8nOutput{result: true},
   176  			expOut: "exp2.json",
   177  		},
   178  		{ // Difficulty calculation - no uncles
   179  			base: "./testdata/14",
   180  			input: t8nInput{
   181  				"alloc.json", "txs.json", "env.json", "London", "",
   182  			},
   183  			output: t8nOutput{result: true},
   184  			expOut: "exp.json",
   185  		},
   186  		{ // Difficulty calculation - with uncles
   187  			base: "./testdata/14",
   188  			input: t8nInput{
   189  				"alloc.json", "txs.json", "env.uncles.json", "London", "",
   190  			},
   191  			output: t8nOutput{result: true},
   192  			expOut: "exp2.json",
   193  		},
   194  		{ // Difficulty calculation - with ommers + Berlin
   195  			base: "./testdata/14",
   196  			input: t8nInput{
   197  				"alloc.json", "txs.json", "env.uncles.json", "Berlin", "",
   198  			},
   199  			output: t8nOutput{result: true},
   200  			expOut: "exp_berlin.json",
   201  		},
   202  		{ // Difficulty calculation on arrow glacier
   203  			base: "./testdata/19",
   204  			input: t8nInput{
   205  				"alloc.json", "txs.json", "env.json", "London", "",
   206  			},
   207  			output: t8nOutput{result: true},
   208  			expOut: "exp_london.json",
   209  		},
   210  		{ // Difficulty calculation on arrow glacier
   211  			base: "./testdata/19",
   212  			input: t8nInput{
   213  				"alloc.json", "txs.json", "env.json", "ArrowGlacier", "",
   214  			},
   215  			output: t8nOutput{result: true},
   216  			expOut: "exp_arrowglacier.json",
   217  		},
   218  		{ // Difficulty calculation on gray glacier
   219  			base: "./testdata/19",
   220  			input: t8nInput{
   221  				"alloc.json", "txs.json", "env.json", "GrayGlacier", "",
   222  			},
   223  			output: t8nOutput{result: true},
   224  			expOut: "exp_grayglacier.json",
   225  		},
   226  		{ // Sign unprotected (pre-EIP155) transaction
   227  			base: "./testdata/23",
   228  			input: t8nInput{
   229  				"alloc.json", "txs.json", "env.json", "Berlin", "",
   230  			},
   231  			output: t8nOutput{result: true},
   232  			expOut: "exp.json",
   233  		},
   234  		{ // Test post-merge transition
   235  			base: "./testdata/24",
   236  			input: t8nInput{
   237  				"alloc.json", "txs.json", "env.json", "Merge", "",
   238  			},
   239  			output: t8nOutput{alloc: true, result: true},
   240  			expOut: "exp.json",
   241  		},
   242  		{ // Test post-merge transition where input is missing random
   243  			base: "./testdata/24",
   244  			input: t8nInput{
   245  				"alloc.json", "txs.json", "env-missingrandom.json", "Merge", "",
   246  			},
   247  			output:      t8nOutput{alloc: false, result: false},
   248  			expExitCode: 3,
   249  		},
   250  		{ // Test base fee calculation
   251  			base: "./testdata/25",
   252  			input: t8nInput{
   253  				"alloc.json", "txs.json", "env.json", "Merge", "",
   254  			},
   255  			output: t8nOutput{alloc: true, result: true},
   256  			expOut: "exp.json",
   257  		},
   258  		{ // Test withdrawals transition
   259  			base: "./testdata/26",
   260  			input: t8nInput{
   261  				"alloc.json", "txs.json", "env.json", "Shanghai", "",
   262  			},
   263  			output: t8nOutput{alloc: true, result: true},
   264  			expOut: "exp.json",
   265  		},
   266  		{ // Cancun tests
   267  			base: "./testdata/28",
   268  			input: t8nInput{
   269  				"alloc.json", "txs.rlp", "env.json", "Cancun", "",
   270  			},
   271  			output: t8nOutput{alloc: true, result: true},
   272  			expOut: "exp.json",
   273  		},
   274  		{ // More cancun tests
   275  			base: "./testdata/29",
   276  			input: t8nInput{
   277  				"alloc.json", "txs.json", "env.json", "Cancun", "",
   278  			},
   279  			output: t8nOutput{alloc: true, result: true},
   280  			expOut: "exp.json",
   281  		},
   282  		{ // More cancun test, plus example of rlp-transaction that cannot be decoded properly
   283  			base: "./testdata/30",
   284  			input: t8nInput{
   285  				"alloc.json", "txs_more.rlp", "env.json", "Cancun", "",
   286  			},
   287  			output: t8nOutput{alloc: true, result: true},
   288  			expOut: "exp.json",
   289  		},
   290  	} {
   291  		args := []string{"t8n"}
   292  		args = append(args, tc.output.get()...)
   293  		args = append(args, tc.input.get(tc.base)...)
   294  		var qArgs []string // quoted args for debugging purposes
   295  		for _, arg := range args {
   296  			if len(arg) == 0 {
   297  				qArgs = append(qArgs, `""`)
   298  			} else {
   299  				qArgs = append(qArgs, arg)
   300  			}
   301  		}
   302  		tt.Logf("args: %v\n", strings.Join(qArgs, " "))
   303  		tt.Run("evm-test", args...)
   304  		// Compare the expected output, if provided
   305  		if tc.expOut != "" {
   306  			file := fmt.Sprintf("%v/%v", tc.base, tc.expOut)
   307  			want, err := os.ReadFile(file)
   308  			if err != nil {
   309  				t.Fatalf("test %d: could not read expected output: %v", i, err)
   310  			}
   311  			have := tt.Output()
   312  			ok, err := cmpJson(have, want)
   313  			switch {
   314  			case err != nil:
   315  				t.Fatalf("test %d, file %v: json parsing failed: %v", i, file, err)
   316  			case !ok:
   317  				t.Fatalf("test %d, file %v: output wrong, have \n%v\nwant\n%v\n", i, file, string(have), string(want))
   318  			}
   319  		}
   320  		tt.WaitExit()
   321  		if have, want := tt.ExitStatus(), tc.expExitCode; have != want {
   322  			t.Fatalf("test %d: wrong exit code, have %d, want %d", i, have, want)
   323  		}
   324  	}
   325  }
   326  
   327  func lineIterator(path string) func() (string, error) {
   328  	data, err := os.ReadFile(path)
   329  	if err != nil {
   330  		return func() (string, error) { return err.Error(), err }
   331  	}
   332  	scanner := bufio.NewScanner(strings.NewReader(string(data)))
   333  	return func() (string, error) {
   334  		if scanner.Scan() {
   335  			return scanner.Text(), nil
   336  		}
   337  		if err := scanner.Err(); err != nil {
   338  			return "", err
   339  		}
   340  		return "", io.EOF // scanner gobbles io.EOF, but we want it
   341  	}
   342  }
   343  
   344  // TestT8nTracing is a test that checks the tracing-output from t8n.
   345  func TestT8nTracing(t *testing.T) {
   346  	t.Parallel()
   347  	tt := new(testT8n)
   348  	tt.TestCmd = cmdtest.NewTestCmd(t, tt)
   349  	for i, tc := range []struct {
   350  		base           string
   351  		input          t8nInput
   352  		expExitCode    int
   353  		extraArgs      []string
   354  		expectedTraces []string
   355  	}{
   356  		{
   357  			base: "./testdata/31",
   358  			input: t8nInput{
   359  				"alloc.json", "txs.json", "env.json", "Cancun", "",
   360  			},
   361  			extraArgs:      []string{"--trace"},
   362  			expectedTraces: []string{"trace-0-0x88f5fbd1524731a81e49f637aa847543268a5aaf2a6b32a69d2c6d978c45dcfb.jsonl"},
   363  		},
   364  		{
   365  			base: "./testdata/31",
   366  			input: t8nInput{
   367  				"alloc.json", "txs.json", "env.json", "Cancun", "",
   368  			},
   369  			extraArgs: []string{"--trace.tracer", `
   370  { 
   371  	result: function(){ 
   372  		return "hello world"
   373  	}, 
   374  	fault: function(){} 
   375  }`},
   376  			expectedTraces: []string{"trace-0-0x88f5fbd1524731a81e49f637aa847543268a5aaf2a6b32a69d2c6d978c45dcfb.json"},
   377  		},
   378  		{
   379  			base: "./testdata/32",
   380  			input: t8nInput{
   381  				"alloc.json", "txs.json", "env.json", "Merge", "",
   382  			},
   383  			extraArgs:      []string{"--trace", "--trace.callframes"},
   384  			expectedTraces: []string{"trace-0-0x47806361c0fa084be3caa18afe8c48156747c01dbdfc1ee11b5aecdbe4fcf23e.jsonl"},
   385  		},
   386  	} {
   387  		args := []string{"t8n"}
   388  		args = append(args, tc.input.get(tc.base)...)
   389  		// Place the output somewhere we can find it
   390  		outdir := t.TempDir()
   391  		args = append(args, "--output.basedir", outdir)
   392  		args = append(args, tc.extraArgs...)
   393  
   394  		var qArgs []string // quoted args for debugging purposes
   395  		for _, arg := range args {
   396  			if len(arg) == 0 {
   397  				qArgs = append(qArgs, `""`)
   398  			} else {
   399  				qArgs = append(qArgs, arg)
   400  			}
   401  		}
   402  		tt.Logf("args: %v\n", strings.Join(qArgs, " "))
   403  		tt.Run("evm-test", args...)
   404  		t.Log(string(tt.Output()))
   405  
   406  		// Compare the expected traces
   407  		for _, traceFile := range tc.expectedTraces {
   408  			haveFn := lineIterator(filepath.Join(outdir, traceFile))
   409  			wantFn := lineIterator(filepath.Join(tc.base, traceFile))
   410  
   411  			for line := 0; ; line++ {
   412  				want, wErr := wantFn()
   413  				have, hErr := haveFn()
   414  				if want != have {
   415  					t.Fatalf("test %d, trace %v, line %d\nwant: %v\nhave: %v\n",
   416  						i, traceFile, line, want, have)
   417  				}
   418  				if wErr != nil && hErr != nil {
   419  					break
   420  				}
   421  				if wErr != nil {
   422  					t.Fatal(wErr)
   423  				}
   424  				if hErr != nil {
   425  					t.Fatal(hErr)
   426  				}
   427  				t.Logf("%v\n", want)
   428  			}
   429  		}
   430  		if have, want := tt.ExitStatus(), tc.expExitCode; have != want {
   431  			t.Fatalf("test %d: wrong exit code, have %d, want %d", i, have, want)
   432  		}
   433  	}
   434  }
   435  
   436  type t9nInput struct {
   437  	inTxs  string
   438  	stFork string
   439  }
   440  
   441  func (args *t9nInput) get(base string) []string {
   442  	var out []string
   443  	if opt := args.inTxs; opt != "" {
   444  		out = append(out, "--input.txs")
   445  		out = append(out, fmt.Sprintf("%v/%v", base, opt))
   446  	}
   447  	if opt := args.stFork; opt != "" {
   448  		out = append(out, "--state.fork", opt)
   449  	}
   450  	return out
   451  }
   452  
   453  func TestT9n(t *testing.T) {
   454  	t.Parallel()
   455  	tt := new(testT8n)
   456  	tt.TestCmd = cmdtest.NewTestCmd(t, tt)
   457  	for i, tc := range []struct {
   458  		base        string
   459  		input       t9nInput
   460  		expExitCode int
   461  		expOut      string
   462  	}{
   463  		{ // London txs on homestead
   464  			base: "./testdata/15",
   465  			input: t9nInput{
   466  				inTxs:  "signed_txs.rlp",
   467  				stFork: "Homestead",
   468  			},
   469  			expOut: "exp.json",
   470  		},
   471  		{ // London txs on London
   472  			base: "./testdata/15",
   473  			input: t9nInput{
   474  				inTxs:  "signed_txs.rlp",
   475  				stFork: "London",
   476  			},
   477  			expOut: "exp2.json",
   478  		},
   479  		{ // An RLP list (a blockheader really)
   480  			base: "./testdata/15",
   481  			input: t9nInput{
   482  				inTxs:  "blockheader.rlp",
   483  				stFork: "London",
   484  			},
   485  			expOut: "exp3.json",
   486  		},
   487  		{ // Transactions with too low gas
   488  			base: "./testdata/16",
   489  			input: t9nInput{
   490  				inTxs:  "signed_txs.rlp",
   491  				stFork: "London",
   492  			},
   493  			expOut: "exp.json",
   494  		},
   495  		{ // Transactions with value exceeding 256 bits
   496  			base: "./testdata/17",
   497  			input: t9nInput{
   498  				inTxs:  "signed_txs.rlp",
   499  				stFork: "London",
   500  			},
   501  			expOut: "exp.json",
   502  		},
   503  		{ // Invalid RLP
   504  			base: "./testdata/18",
   505  			input: t9nInput{
   506  				inTxs:  "invalid.rlp",
   507  				stFork: "London",
   508  			},
   509  			expExitCode: t8ntool.ErrorIO,
   510  		},
   511  	} {
   512  		args := []string{"t9n"}
   513  		args = append(args, tc.input.get(tc.base)...)
   514  
   515  		tt.Run("evm-test", args...)
   516  		tt.Logf("args:\n go run . %v\n", strings.Join(args, " "))
   517  		// Compare the expected output, if provided
   518  		if tc.expOut != "" {
   519  			want, err := os.ReadFile(fmt.Sprintf("%v/%v", tc.base, tc.expOut))
   520  			if err != nil {
   521  				t.Fatalf("test %d: could not read expected output: %v", i, err)
   522  			}
   523  			have := tt.Output()
   524  			ok, err := cmpJson(have, want)
   525  			switch {
   526  			case err != nil:
   527  				t.Logf(string(have))
   528  				t.Fatalf("test %d, json parsing failed: %v", i, err)
   529  			case !ok:
   530  				t.Fatalf("test %d: output wrong, have \n%v\nwant\n%v\n", i, string(have), string(want))
   531  			}
   532  		}
   533  		tt.WaitExit()
   534  		if have, want := tt.ExitStatus(), tc.expExitCode; have != want {
   535  			t.Fatalf("test %d: wrong exit code, have %d, want %d", i, have, want)
   536  		}
   537  	}
   538  }
   539  
   540  type b11rInput struct {
   541  	inEnv         string
   542  	inOmmersRlp   string
   543  	inWithdrawals string
   544  	inTxsRlp      string
   545  	inClique      string
   546  	ethash        bool
   547  	ethashMode    string
   548  	ethashDir     string
   549  }
   550  
   551  func (args *b11rInput) get(base string) []string {
   552  	var out []string
   553  	if opt := args.inEnv; opt != "" {
   554  		out = append(out, "--input.header")
   555  		out = append(out, fmt.Sprintf("%v/%v", base, opt))
   556  	}
   557  	if opt := args.inOmmersRlp; opt != "" {
   558  		out = append(out, "--input.ommers")
   559  		out = append(out, fmt.Sprintf("%v/%v", base, opt))
   560  	}
   561  	if opt := args.inWithdrawals; opt != "" {
   562  		out = append(out, "--input.withdrawals")
   563  		out = append(out, fmt.Sprintf("%v/%v", base, opt))
   564  	}
   565  	if opt := args.inTxsRlp; opt != "" {
   566  		out = append(out, "--input.txs")
   567  		out = append(out, fmt.Sprintf("%v/%v", base, opt))
   568  	}
   569  	if opt := args.inClique; opt != "" {
   570  		out = append(out, "--seal.clique")
   571  		out = append(out, fmt.Sprintf("%v/%v", base, opt))
   572  	}
   573  	if args.ethash {
   574  		out = append(out, "--seal.ethash")
   575  	}
   576  	if opt := args.ethashMode; opt != "" {
   577  		out = append(out, "--seal.ethash.mode")
   578  		out = append(out, fmt.Sprintf("%v/%v", base, opt))
   579  	}
   580  	if opt := args.ethashDir; opt != "" {
   581  		out = append(out, "--seal.ethash.dir")
   582  		out = append(out, fmt.Sprintf("%v/%v", base, opt))
   583  	}
   584  	out = append(out, "--output.block")
   585  	out = append(out, "stdout")
   586  	return out
   587  }
   588  
   589  func TestB11r(t *testing.T) {
   590  	t.Parallel()
   591  	tt := new(testT8n)
   592  	tt.TestCmd = cmdtest.NewTestCmd(t, tt)
   593  	for i, tc := range []struct {
   594  		base        string
   595  		input       b11rInput
   596  		expExitCode int
   597  		expOut      string
   598  	}{
   599  		{ // unsealed block
   600  			base: "./testdata/20",
   601  			input: b11rInput{
   602  				inEnv:       "header.json",
   603  				inOmmersRlp: "ommers.json",
   604  				inTxsRlp:    "txs.rlp",
   605  			},
   606  			expOut: "exp.json",
   607  		},
   608  		{ // ethash test seal
   609  			base: "./testdata/21",
   610  			input: b11rInput{
   611  				inEnv:       "header.json",
   612  				inOmmersRlp: "ommers.json",
   613  				inTxsRlp:    "txs.rlp",
   614  			},
   615  			expOut: "exp.json",
   616  		},
   617  		{ // clique test seal
   618  			base: "./testdata/21",
   619  			input: b11rInput{
   620  				inEnv:       "header.json",
   621  				inOmmersRlp: "ommers.json",
   622  				inTxsRlp:    "txs.rlp",
   623  				inClique:    "clique.json",
   624  			},
   625  			expOut: "exp-clique.json",
   626  		},
   627  		{ // block with ommers
   628  			base: "./testdata/22",
   629  			input: b11rInput{
   630  				inEnv:       "header.json",
   631  				inOmmersRlp: "ommers.json",
   632  				inTxsRlp:    "txs.rlp",
   633  			},
   634  			expOut: "exp.json",
   635  		},
   636  		{ // block with withdrawals
   637  			base: "./testdata/27",
   638  			input: b11rInput{
   639  				inEnv:         "header.json",
   640  				inOmmersRlp:   "ommers.json",
   641  				inWithdrawals: "withdrawals.json",
   642  				inTxsRlp:      "txs.rlp",
   643  			},
   644  			expOut: "exp.json",
   645  		},
   646  	} {
   647  		args := []string{"b11r"}
   648  		args = append(args, tc.input.get(tc.base)...)
   649  
   650  		tt.Run("evm-test", args...)
   651  		tt.Logf("args:\n go run . %v\n", strings.Join(args, " "))
   652  		// Compare the expected output, if provided
   653  		if tc.expOut != "" {
   654  			want, err := os.ReadFile(fmt.Sprintf("%v/%v", tc.base, tc.expOut))
   655  			if err != nil {
   656  				t.Fatalf("test %d: could not read expected output: %v", i, err)
   657  			}
   658  			have := tt.Output()
   659  			ok, err := cmpJson(have, want)
   660  			switch {
   661  			case err != nil:
   662  				t.Logf(string(have))
   663  				t.Fatalf("test %d, json parsing failed: %v", i, err)
   664  			case !ok:
   665  				t.Fatalf("test %d: output wrong, have \n%v\nwant\n%v\n", i, string(have), string(want))
   666  			}
   667  		}
   668  		tt.WaitExit()
   669  		if have, want := tt.ExitStatus(), tc.expExitCode; have != want {
   670  			t.Fatalf("test %d: wrong exit code, have %d, want %d", i, have, want)
   671  		}
   672  	}
   673  }
   674  
   675  // cmpJson compares the JSON in two byte slices.
   676  func cmpJson(a, b []byte) (bool, error) {
   677  	var j, j2 interface{}
   678  	if err := json.Unmarshal(a, &j); err != nil {
   679  		return false, err
   680  	}
   681  	if err := json.Unmarshal(b, &j2); err != nil {
   682  		return false, err
   683  	}
   684  	return reflect.DeepEqual(j2, j), nil
   685  }