| // Copyright 2013 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // This test fails at Go 1.18 due to infinite recursion in go/types. |
| |
| //go:build go1.19 |
| |
| package interp_test |
| |
| // This test runs the SSA interpreter over sample Go programs. |
| // Because the interpreter requires intrinsics for assembly |
| // functions and many low-level runtime routines, it is inherently |
| // not robust to evolutionary change in the standard library. |
| // Therefore the test cases are restricted to programs that |
| // use a fake standard library in testdata/src containing a tiny |
| // subset of simple functions useful for writing assertions. |
| // |
| // We no longer attempt to interpret any real standard packages such as |
| // fmt or testing, as it proved too fragile. |
| |
| import ( |
| "bytes" |
| "fmt" |
| "go/build" |
| "go/types" |
| "log" |
| "os" |
| "path/filepath" |
| "runtime" |
| "strings" |
| "testing" |
| "time" |
| "unsafe" |
| |
| "golang.org/x/tools/go/loader" |
| "golang.org/x/tools/go/ssa" |
| "golang.org/x/tools/go/ssa/interp" |
| "golang.org/x/tools/go/ssa/ssautil" |
| "golang.org/x/tools/internal/testenv" |
| ) |
| |
| // Each line contains a space-separated list of $GOROOT/test/ |
| // filenames comprising the main package of a program. |
| // They are ordered quickest-first, roughly. |
| // |
| // If a test in this list fails spuriously, remove it. |
| var gorootTestTests = []string{ |
| "235.go", |
| "alias1.go", |
| "func5.go", |
| "func6.go", |
| "func7.go", |
| "func8.go", |
| "helloworld.go", |
| "varinit.go", |
| "escape3.go", |
| "initcomma.go", |
| "cmp.go", |
| "compos.go", |
| "turing.go", |
| "indirect.go", |
| "complit.go", |
| "for.go", |
| "struct0.go", |
| "intcvt.go", |
| "printbig.go", |
| "deferprint.go", |
| "escape.go", |
| "range.go", |
| "const4.go", |
| "float_lit.go", |
| "bigalg.go", |
| "decl.go", |
| "if.go", |
| "named.go", |
| "bigmap.go", |
| "func.go", |
| "reorder2.go", |
| "gc.go", |
| "simassign.go", |
| "iota.go", |
| "nilptr2.go", |
| "utf.go", |
| "method.go", |
| "char_lit.go", |
| "env.go", |
| "int_lit.go", |
| "string_lit.go", |
| "defer.go", |
| "typeswitch.go", |
| "stringrange.go", |
| "reorder.go", |
| "method3.go", |
| "literal.go", |
| "nul1.go", // doesn't actually assert anything (errorcheckoutput) |
| "zerodivide.go", |
| "convert.go", |
| "convT2X.go", |
| "switch.go", |
| "ddd.go", |
| "blank.go", // partly disabled |
| "closedchan.go", |
| "divide.go", |
| "rename.go", |
| "nil.go", |
| "recover1.go", |
| "recover2.go", |
| "recover3.go", |
| "typeswitch1.go", |
| "floatcmp.go", |
| "crlf.go", // doesn't actually assert anything (runoutput) |
| } |
| |
| // These are files in go.tools/go/ssa/interp/testdata/. |
| var testdataTests = []string{ |
| "boundmeth.go", |
| "complit.go", |
| "convert.go", |
| "coverage.go", |
| "deepequal.go", |
| "defer.go", |
| "fieldprom.go", |
| "forvarlifetime_old.go", |
| "ifaceconv.go", |
| "ifaceprom.go", |
| "initorder.go", |
| "methprom.go", |
| "mrvchain.go", |
| "range.go", |
| "recover.go", |
| "reflect.go", |
| "slice2arrayptr.go", |
| "static.go", |
| "width32.go", |
| "rangevarlifetime_old.go", |
| "fixedbugs/issue52342.go", |
| "fixedbugs/issue55115.go", |
| "fixedbugs/issue52835.go", |
| "fixedbugs/issue55086.go", |
| "typeassert.go", |
| "zeros.go", |
| } |
| |
| func init() { |
| // GOROOT/test used to assume that GOOS and GOARCH were explicitly set in the |
| // environment, so do that here for TestGorootTest. |
| os.Setenv("GOOS", runtime.GOOS) |
| os.Setenv("GOARCH", runtime.GOARCH) |
| } |
| |
| func run(t *testing.T, input string, goroot string) { |
| // The recover2 test case is broken on Go 1.14+. See golang/go#34089. |
| // TODO(matloob): Fix this. |
| if filepath.Base(input) == "recover2.go" { |
| t.Skip("The recover2.go test is broken in go1.14+. See golang.org/issue/34089.") |
| } |
| |
| t.Logf("Input: %s\n", input) |
| |
| start := time.Now() |
| |
| ctx := build.Default // copy |
| ctx.GOROOT = goroot |
| ctx.GOOS = runtime.GOOS |
| ctx.GOARCH = runtime.GOARCH |
| if filepath.Base(input) == "width32.go" && unsafe.Sizeof(int(0)) > 4 { |
| t.Skipf("skipping: width32.go checks behavior for a 32-bit int") |
| } |
| |
| gover := "" |
| if p := testenv.Go1Point(); p > 0 { |
| gover = fmt.Sprintf("go1.%d", p) |
| } |
| |
| conf := loader.Config{Build: &ctx, TypeChecker: types.Config{GoVersion: gover}} |
| if _, err := conf.FromArgs([]string{input}, true); err != nil { |
| t.Fatalf("FromArgs(%s) failed: %s", input, err) |
| } |
| |
| conf.Import("runtime") |
| |
| // Print a helpful hint if we don't make it to the end. |
| var hint string |
| defer func() { |
| if hint != "" { |
| fmt.Println("FAIL") |
| fmt.Println(hint) |
| } else { |
| fmt.Println("PASS") |
| } |
| |
| interp.CapturedOutput = nil |
| }() |
| |
| hint = fmt.Sprintf("To dump SSA representation, run:\n%% go build golang.org/x/tools/cmd/ssadump && ./ssadump -test -build=CFP %s\n", input) |
| |
| iprog, err := conf.Load() |
| if err != nil { |
| t.Fatalf("conf.Load(%s) failed: %s", input, err) |
| } |
| |
| bmode := ssa.InstantiateGenerics | ssa.SanityCheckFunctions |
| // bmode |= ssa.PrintFunctions // enable for debugging |
| prog := ssautil.CreateProgram(iprog, bmode) |
| prog.Build() |
| |
| mainPkg := prog.Package(iprog.Created[0].Pkg) |
| if mainPkg == nil { |
| t.Fatalf("not a main package: %s", input) |
| } |
| |
| interp.CapturedOutput = new(bytes.Buffer) |
| |
| sizes := types.SizesFor("gc", ctx.GOARCH) |
| if sizes.Sizeof(types.Typ[types.Int]) < 4 { |
| panic("bogus SizesFor") |
| } |
| hint = fmt.Sprintf("To trace execution, run:\n%% go build golang.org/x/tools/cmd/ssadump && ./ssadump -build=C -test -run --interp=T %s\n", input) |
| var imode interp.Mode // default mode |
| // imode |= interp.DisableRecover // enable for debugging |
| // imode |= interp.EnableTracing // enable for debugging |
| exitCode := interp.Interpret(mainPkg, imode, sizes, input, []string{}) |
| if exitCode != 0 { |
| t.Fatalf("interpreting %s: exit code was %d", input, exitCode) |
| } |
| // $GOROOT/test tests use this convention: |
| if strings.Contains(interp.CapturedOutput.String(), "BUG") { |
| t.Fatalf("interpreting %s: exited zero but output contained 'BUG'", input) |
| } |
| |
| hint = "" // call off the hounds |
| |
| if false { |
| t.Log(input, time.Since(start)) // test profiling |
| } |
| } |
| |
| // makeGoroot copies testdata/src into the "src" directory of a temporary |
| // location to mimic GOROOT/src, and adds a file "runtime/consts.go" containing |
| // declarations for GOOS and GOARCH that match the GOOS and GOARCH of this test. |
| // |
| // It returns the directory that should be used for GOROOT. |
| func makeGoroot(t *testing.T) string { |
| goroot := t.TempDir() |
| src := filepath.Join(goroot, "src") |
| |
| err := filepath.Walk("testdata/src", func(path string, info os.FileInfo, err error) error { |
| if err != nil { |
| return err |
| } |
| |
| rel, err := filepath.Rel("testdata/src", path) |
| if err != nil { |
| return err |
| } |
| targ := filepath.Join(src, rel) |
| |
| if info.IsDir() { |
| return os.Mkdir(targ, info.Mode().Perm()|0700) |
| } |
| |
| b, err := os.ReadFile(path) |
| if err != nil { |
| return err |
| } |
| return os.WriteFile(targ, b, info.Mode().Perm()) |
| }) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| constsGo := fmt.Sprintf(`package runtime |
| const GOOS = %q |
| const GOARCH = %q |
| `, runtime.GOOS, runtime.GOARCH) |
| err = os.WriteFile(filepath.Join(src, "runtime/consts.go"), []byte(constsGo), 0644) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| return goroot |
| } |
| |
| // TestTestdataFiles runs the interpreter on testdata/*.go. |
| func TestTestdataFiles(t *testing.T) { |
| goroot := makeGoroot(t) |
| cwd, err := os.Getwd() |
| if err != nil { |
| log.Fatal(err) |
| } |
| for _, input := range testdataTests { |
| t.Run(input, func(t *testing.T) { |
| run(t, filepath.Join(cwd, "testdata", input), goroot) |
| }) |
| } |
| } |
| |
| // TestGorootTest runs the interpreter on $GOROOT/test/*.go. |
| func TestGorootTest(t *testing.T) { |
| goroot := makeGoroot(t) |
| for _, input := range gorootTestTests { |
| t.Run(input, func(t *testing.T) { |
| run(t, filepath.Join(build.Default.GOROOT, "test", input), goroot) |
| }) |
| } |
| } |
| |
| // TestTypeparamTest runs the interpreter on runnable examples |
| // in $GOROOT/test/typeparam/*.go. |
| |
| func TestTypeparamTest(t *testing.T) { |
| goroot := makeGoroot(t) |
| |
| // Skip known failures for the given reason. |
| // TODO(taking): Address these. |
| skip := map[string]string{ |
| "chans.go": "interp tests do not support runtime.SetFinalizer", |
| "issue23536.go": "unknown reason", |
| "issue48042.go": "interp tests do not handle reflect.Value.SetInt", |
| "issue47716.go": "interp tests do not handle unsafe.Sizeof", |
| "issue50419.go": "interp tests do not handle dispatch to String() correctly", |
| "issue51733.go": "interp does not handle unsafe casts", |
| "ordered.go": "math.NaN() comparisons not being handled correctly", |
| "orderedmap.go": "interp tests do not support runtime.SetFinalizer", |
| "stringer.go": "unknown reason", |
| "issue48317.go": "interp tests do not support encoding/json", |
| "issue48318.go": "interp tests do not support encoding/json", |
| "issue58513.go": "interp tests do not support runtime.Caller", |
| } |
| // Collect all of the .go files in dir that are runnable. |
| dir := filepath.Join(build.Default.GOROOT, "test", "typeparam") |
| list, err := os.ReadDir(dir) |
| if err != nil { |
| t.Fatal(err) |
| } |
| for _, entry := range list { |
| if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") { |
| continue // Consider standalone go files. |
| } |
| t.Run(entry.Name(), func(t *testing.T) { |
| input := filepath.Join(dir, entry.Name()) |
| src, err := os.ReadFile(input) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Only build test files that can be compiled, or compiled and run. |
| if !bytes.HasPrefix(src, []byte("// run")) || bytes.HasPrefix(src, []byte("// rundir")) { |
| t.Logf("Not a `// run` file: %s", entry.Name()) |
| return |
| } |
| |
| if reason := skip[entry.Name()]; reason != "" { |
| t.Skipf("skipping: %s", reason) |
| } |
| |
| run(t, input, goroot) |
| }) |
| } |
| } |