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
+}