go/packages/internal/drivertest: a package for a fake go/packages driver
Add a new drivertest package that implements a fake go/packages driver,
which simply wraps a call to a (non-driver) go/packages.Load. This will
be used for writing gopls tests in GOPACKAGESDRIVER mode.
The test for this new package turned up an asymmetric in Package JSON
serialization: the IgnoredFiles field was not being set while
unmarshalling. As you might imagine, this was initially very confusing.
Fixes golang/go#67615
Change-Id: Ia400650947ade5984fa342cdafccfd4e80e9a4dd
Reviewed-on: https://go-review.googlesource.com/c/tools/+/589135
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Commit-Queue: Alan Donovan <adonovan@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Auto-Submit: Alan Donovan <adonovan@google.com>
diff --git a/go/analysis/passes/loopclosure/loopclosure_test.go b/go/analysis/passes/loopclosure/loopclosure_test.go
index 8c17949..03b8107 100644
--- a/go/analysis/passes/loopclosure/loopclosure_test.go
+++ b/go/analysis/passes/loopclosure/loopclosure_test.go
@@ -27,12 +27,12 @@
testenv.NeedsGo1Point(t, 22)
txtar := filepath.Join(analysistest.TestData(), "src", "versions", "go22.txtar")
- dir := testfiles.ExtractTxtarToTmp(t, txtar)
+ dir := testfiles.ExtractTxtarFileToTmp(t, txtar)
analysistest.Run(t, dir, loopclosure.Analyzer, "golang.org/fake/versions")
}
func TestVersions18(t *testing.T) {
txtar := filepath.Join(analysistest.TestData(), "src", "versions", "go18.txtar")
- dir := testfiles.ExtractTxtarToTmp(t, txtar)
+ dir := testfiles.ExtractTxtarFileToTmp(t, txtar)
analysistest.Run(t, dir, loopclosure.Analyzer, "golang.org/fake/versions")
}
diff --git a/go/analysis/passes/stdversion/stdversion_test.go b/go/analysis/passes/stdversion/stdversion_test.go
index d6a2e45..7b2f72d 100644
--- a/go/analysis/passes/stdversion/stdversion_test.go
+++ b/go/analysis/passes/stdversion/stdversion_test.go
@@ -19,7 +19,7 @@
// itself requires the go1.22 implementation of versions.FileVersions.
testenv.NeedsGo1Point(t, 22)
- dir := testfiles.ExtractTxtarToTmp(t, filepath.Join(analysistest.TestData(), "test.txtar"))
+ dir := testfiles.ExtractTxtarFileToTmp(t, filepath.Join(analysistest.TestData(), "test.txtar"))
analysistest.Run(t, dir, stdversion.Analyzer,
"example.com/a",
"example.com/sub",
diff --git a/go/packages/packages.go b/go/packages/packages.go
index 4eca751..ec4ade6 100644
--- a/go/packages/packages.go
+++ b/go/packages/packages.go
@@ -643,6 +643,7 @@
OtherFiles: flat.OtherFiles,
EmbedFiles: flat.EmbedFiles,
EmbedPatterns: flat.EmbedPatterns,
+ IgnoredFiles: flat.IgnoredFiles,
ExportFile: flat.ExportFile,
}
if len(flat.Imports) > 0 {
diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go
index 07b4a3c..062a221 100644
--- a/go/ssa/builder_test.go
+++ b/go/ssa/builder_test.go
@@ -173,7 +173,7 @@
func TestNoIndirectCreatePackage(t *testing.T) {
testenv.NeedsGoBuild(t) // for go/packages
- dir := testfiles.ExtractTxtarToTmp(t, filepath.Join(analysistest.TestData(), "indirect.txtar"))
+ dir := testfiles.ExtractTxtarFileToTmp(t, filepath.Join(analysistest.TestData(), "indirect.txtar"))
pkgs, err := loadPackages(dir, "testdata/a")
if err != nil {
t.Fatal(err)
diff --git a/internal/drivertest/driver.go b/internal/drivertest/driver.go
new file mode 100644
index 0000000..1a63cab
--- /dev/null
+++ b/internal/drivertest/driver.go
@@ -0,0 +1,91 @@
+// Copyright 2024 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.
+
+// The drivertest package provides a fake implementation of the go/packages
+// driver protocol that delegates to the go list driver. It may be used to test
+// programs such as gopls that specialize behavior when a go/packages driver is
+// in use.
+//
+// The driver is run as a child of the current process, by calling [RunIfChild]
+// at process start, and running go/packages with the environment variables set
+// by [Env].
+package drivertest
+
+import (
+ "encoding/json"
+ "flag"
+ "log"
+ "os"
+ "testing"
+
+ "golang.org/x/tools/go/packages"
+)
+
+const runAsDriverEnv = "DRIVERTEST_RUN_AS_DRIVER"
+
+// RunIfChild runs the current process as a go/packages driver, if configured
+// to do so by the current environment (see [Env]).
+//
+// Otherwise, RunIfChild is a no op.
+func RunIfChild() {
+ if os.Getenv(runAsDriverEnv) != "" {
+ main()
+ os.Exit(0)
+ }
+}
+
+// Env returns additional environment variables for use in [packages.Config]
+// to enable the use of drivertest as the driver.
+func Env(t *testing.T) []string {
+ exe, err := os.Executable()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return []string{"GOPACKAGESDRIVER=" + exe, runAsDriverEnv + "=1"}
+}
+
+func main() {
+ flag.Parse()
+
+ dec := json.NewDecoder(os.Stdin)
+ var request packages.DriverRequest
+ if err := dec.Decode(&request); err != nil {
+ log.Fatalf("decoding request: %v", err)
+ }
+
+ config := packages.Config{
+ Mode: request.Mode,
+ Env: append(request.Env, "GOPACKAGESDRIVER=off"), // avoid recursive invocation
+ BuildFlags: request.BuildFlags,
+ Tests: request.Tests,
+ Overlay: request.Overlay,
+ }
+ pkgs, err := packages.Load(&config, flag.Args()...)
+ if err != nil {
+ log.Fatalf("load failed: %v", err)
+ }
+
+ var roots []string
+ for _, pkg := range pkgs {
+ roots = append(roots, pkg.ID)
+ }
+ var allPackages []*packages.Package
+ packages.Visit(pkgs, nil, func(pkg *packages.Package) {
+ newImports := make(map[string]*packages.Package)
+ for path, imp := range pkg.Imports {
+ newImports[path] = &packages.Package{ID: imp.ID}
+ }
+ pkg.Imports = newImports
+ allPackages = append(allPackages, pkg)
+ })
+
+ enc := json.NewEncoder(os.Stdout)
+ response := packages.DriverResponse{
+ Roots: roots,
+ Packages: allPackages,
+ }
+ if err := enc.Encode(response); err != nil {
+ log.Fatalf("encoding response: %v", err)
+ }
+}
diff --git a/internal/drivertest/driver_test.go b/internal/drivertest/driver_test.go
new file mode 100644
index 0000000..b96f684
--- /dev/null
+++ b/internal/drivertest/driver_test.go
@@ -0,0 +1,139 @@
+// Copyright 2024 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 drivertest_test
+
+// This file is both a test of drivertest and an example of how to use it in your own tests.
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "golang.org/x/tools/go/packages"
+ "golang.org/x/tools/internal/diff"
+ "golang.org/x/tools/internal/diff/myers"
+ "golang.org/x/tools/internal/drivertest"
+ "golang.org/x/tools/internal/packagesinternal"
+ "golang.org/x/tools/internal/testfiles"
+ "golang.org/x/tools/txtar"
+)
+
+func TestMain(m *testing.M) {
+ drivertest.RunIfChild()
+
+ os.Exit(m.Run())
+}
+
+func TestDriverConformance(t *testing.T) {
+ const workspace = `
+-- go.mod --
+module example.com/m
+
+go 1.20
+
+-- m.go --
+package m
+
+-- lib/lib.go --
+package lib
+`
+
+ dir := testfiles.ExtractTxtarToTmp(t, txtar.Parse([]byte(workspace)))
+
+ baseConfig := packages.Config{
+ Dir: dir,
+ Mode: packages.NeedName |
+ packages.NeedFiles |
+ packages.NeedCompiledGoFiles |
+ packages.NeedImports |
+ packages.NeedDeps |
+ packages.NeedTypesSizes |
+ packages.NeedModule |
+ packages.NeedEmbedFiles |
+ packages.LoadMode(packagesinternal.DepsErrors) |
+ packages.LoadMode(packagesinternal.ForTest),
+ }
+
+ tests := []struct {
+ name string
+ query string
+ overlay string
+ }{
+ {
+ name: "load all",
+ query: "./...",
+ },
+ {
+ name: "overlays",
+ query: "./...",
+ overlay: `
+-- m.go --
+package m
+
+import . "lib"
+-- a/a.go --
+package a
+`,
+ },
+ {
+ name: "std",
+ query: "std",
+ },
+ {
+ name: "builtin",
+ query: "builtin",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ cfg := baseConfig
+ if test.overlay != "" {
+ cfg.Overlay = make(map[string][]byte)
+ for _, file := range txtar.Parse([]byte(test.overlay)).Files {
+ name := filepath.Join(dir, filepath.FromSlash(file.Name))
+ cfg.Overlay[name] = file.Data
+ }
+ }
+
+ // Compare JSON-encoded packages with and without GOPACKAGESDRIVER.
+ //
+ // Note that this does not guarantee that the go/packages results
+ // themselves are equivalent, only that their encoded JSON is equivalent.
+ // Certain fields such as Module are intentionally omitted from external
+ // drivers, because they don't make sense for an arbitrary build system.
+ var jsons []string
+ for _, env := range [][]string{
+ {"GOPACKAGESDRIVER=off"},
+ drivertest.Env(t),
+ } {
+ cfg.Env = append(os.Environ(), env...)
+ pkgs, err := packages.Load(&cfg, test.query)
+ if err != nil {
+ t.Fatalf("failed to load (env: %v): %v", env, err)
+ }
+ data, err := json.MarshalIndent(pkgs, "", "\t")
+ if err != nil {
+ t.Fatalf("failed to marshal (env: %v): %v", env, err)
+ }
+ jsons = append(jsons, string(data))
+ }
+
+ listJSON := jsons[0]
+ driverJSON := jsons[1]
+
+ // Use the myers package for better line diffs.
+ edits := myers.ComputeEdits(listJSON, driverJSON)
+ d, err := diff.ToUnified("go list", "driver", listJSON, edits, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if d != "" {
+ t.Errorf("mismatching JSON:\n%s", d)
+ }
+ })
+ }
+}
diff --git a/internal/testfiles/testfiles.go b/internal/testfiles/testfiles.go
index ff73955..c8a2bd9 100644
--- a/internal/testfiles/testfiles.go
+++ b/internal/testfiles/testfiles.go
@@ -122,11 +122,11 @@
return nil
}
-// ExtractTxtarToTmp read a txtar archive on a given path,
+// ExtractTxtarFileToTmp read a txtar archive on a given path,
// extracts it to a temporary directory, and returns the
// temporary directory.
-func ExtractTxtarToTmp(t testing.TB, archive string) string {
- ar, err := txtar.ParseFile(archive)
+func ExtractTxtarFileToTmp(t testing.TB, archiveFile string) string {
+ ar, err := txtar.ParseFile(archiveFile)
if err != nil {
t.Fatal(err)
}
@@ -138,3 +138,14 @@
}
return dir
}
+
+// ExtractTxtarToTmp extracts the given archive to a temp directory, and
+// returns that temporary directory.
+func ExtractTxtarToTmp(t testing.TB, ar *txtar.Archive) string {
+ dir := t.TempDir()
+ err := ExtractTxtar(dir, ar)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return dir
+}