diff --git a/src/cmd/compile/internal/noder/unified_test.go b/src/cmd/compile/internal/noder/unified_test.go new file mode 100644 index 00000000000..242fa1282f4 --- /dev/null +++ b/src/cmd/compile/internal/noder/unified_test.go @@ -0,0 +1,151 @@ +// Copyright 2021 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. + +package noder_test + +import ( + "encoding/json" + "flag" + exec "internal/execabs" + "os" + "reflect" + "runtime" + "strings" + "testing" +) + +var ( + flagPkgs = flag.String("pkgs", "std", "list of packages to compare") + flagAll = flag.Bool("all", false, "enable testing of all GOOS/GOARCH targets") + flagParallel = flag.Bool("parallel", false, "test GOOS/GOARCH targets in parallel") +) + +// TestUnifiedCompare implements a test similar to running: +// +// $ go build -toolexec="toolstash -cmp" std +// +// The -pkgs flag controls the list of packages tested. +// +// By default, only the native GOOS/GOARCH target is enabled. The -all +// flag enables testing of non-native targets. The -parallel flag +// additionally enables testing of targets in parallel. +// +// Caution: Testing all targets is very resource intensive! On an IBM +// P920 (dual Intel Xeon Gold 6154 CPUs; 36 cores, 192GB RAM), testing +// all targets in parallel takes about 5 minutes. Using the 'go test' +// command's -run flag for subtest matching is recommended for less +// powerful machines. +func TestUnifiedCompare(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode") + } + + targets, err := exec.Command("go", "tool", "dist", "list").Output() + if err != nil { + t.Fatal(err) + } + + for _, target := range strings.Fields(string(targets)) { + t.Run(target, func(t *testing.T) { + parts := strings.Split(target, "/") + goos, goarch := parts[0], parts[1] + + if !(*flagAll || goos == runtime.GOOS && goarch == runtime.GOARCH) { + t.Skip("skipping non-native target (use -all to enable)") + } + if *flagParallel { + t.Parallel() + } + + pkgs1 := loadPackages(t, goos, goarch, "-d=unified=0 -d=inlfuncswithclosures=0") + pkgs2 := loadPackages(t, goos, goarch, "-d=unified=1 -d=inlfuncswithclosures=0") + + if len(pkgs1) != len(pkgs2) { + t.Fatalf("length mismatch: %v != %v", len(pkgs1), len(pkgs2)) + } + + for i := range pkgs1 { + pkg1 := pkgs1[i] + pkg2 := pkgs2[i] + + path := pkg1.ImportPath + if path != pkg2.ImportPath { + t.Fatalf("mismatched paths: %q != %q", path, pkg2.ImportPath) + } + + // Packages that don't have any source files (e.g., packages + // unsafe, embed/internal/embedtest, and cmd/internal/moddeps). + if pkg1.Export == "" && pkg2.Export == "" { + continue + } + + if pkg1.BuildID == pkg2.BuildID { + t.Errorf("package %q: build IDs unexpectedly matched", path) + } + + // Unlike toolstash -cmp, we're comparing the same compiler + // binary against itself, just with different flags. So we + // don't need to worry about skipping over mismatched version + // strings, but we do need to account for differing build IDs. + // + // Fortunately, build IDs are cryptographic 256-bit hashes, + // and cmd/go provides us with them up front. So we can just + // use them as delimeters to split the files, and then check + // that the substrings are all equal. + file1 := strings.Split(readFile(t, pkg1.Export), pkg1.BuildID) + file2 := strings.Split(readFile(t, pkg2.Export), pkg2.BuildID) + if !reflect.DeepEqual(file1, file2) { + t.Errorf("package %q: compile output differs", path) + } + } + }) + } +} + +type pkg struct { + ImportPath string + Export string + BuildID string + Incomplete bool +} + +func loadPackages(t *testing.T, goos, goarch, gcflags string) []pkg { + args := []string{"list", "-e", "-export", "-json", "-gcflags=all=" + gcflags, "--"} + args = append(args, strings.Fields(*flagPkgs)...) + + cmd := exec.Command("go", args...) + cmd.Env = append(os.Environ(), "GOOS="+goos, "GOARCH="+goarch) + cmd.Stderr = os.Stderr + stdout, err := cmd.StdoutPipe() + if err != nil { + t.Fatal(err) + } + if err := cmd.Start(); err != nil { + t.Fatal(err) + } + + var res []pkg + for dec := json.NewDecoder(stdout); dec.More(); { + var pkg pkg + if err := dec.Decode(&pkg); err != nil { + t.Fatal(err) + } + if pkg.Incomplete { + t.Fatalf("incomplete package: %q", pkg.ImportPath) + } + res = append(res, pkg) + } + if err := cmd.Wait(); err != nil { + t.Fatal(err) + } + return res +} + +func readFile(t *testing.T, name string) string { + buf, err := os.ReadFile(name) + if err != nil { + t.Fatal(err) + } + return string(buf) +}