internal/testfiles: add a helper package for test files Adds a new package testfiles to help write tests around go files and directories. These are especially helpful when dealing with go.mod files. Adds a new CopyTestFiles function that copies a directory and removes the ".test" file extension, e.g. "go.mod.test" becomes "go.mod". This allows for easily creating analysistests with go.mod files. This change also adds a new TestDir helper to extract to a temporary directory. Consolidates txtar usage around ExtractTxtar, which writes a txtar archive to a directory, and adds a convenience helper TestTxtar that extracts a txtar at a path to a temporary directory. Updates golang/go#61336 Updates golang/go#53063 Updates golang/go#46041 Updates golang/go#37054 Change-Id: I09210f751bbc6ac3f01c34fba22b7e8fa1ddf93f Reviewed-on: https://go-review.googlesource.com/c/tools/+/577995 Reviewed-by: Alan Donovan <adonovan@google.com> Reviewed-by: Robert Findley <rfindley@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/go/analysis/analysistest/analysistest_test.go b/go/analysis/analysistest/analysistest_test.go index 8e4767a..b22e2a1 100644 --- a/go/analysis/analysistest/analysistest_test.go +++ b/go/analysis/analysistest/analysistest_test.go
@@ -20,12 +20,8 @@ ) func init() { - // This test currently requires GOPATH mode. - // Explicitly disabling module mode should suffice, but - // we'll also turn off GOPROXY just for good measure. - if err := os.Setenv("GO111MODULE", "off"); err != nil { - log.Fatal(err) - } + // Run() decides when tests use GOPATH mode or modules. + // We turn off GOPROXY just for good measure. if err := os.Setenv("GOPROXY", "off"); err != nil { log.Fatal(err) }
diff --git a/go/analysis/passes/loopclosure/loopclosure_test.go b/go/analysis/passes/loopclosure/loopclosure_test.go index 683f91e..8c17949 100644 --- a/go/analysis/passes/loopclosure/loopclosure_test.go +++ b/go/analysis/passes/loopclosure/loopclosure_test.go
@@ -5,15 +5,13 @@ package loopclosure_test import ( - "os" "path/filepath" "testing" - "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/analysis/passes/loopclosure" "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/txtar" + "golang.org/x/tools/internal/testfiles" ) func Test(t *testing.T) { @@ -28,37 +26,13 @@ func TestVersions22(t *testing.T) { testenv.NeedsGo1Point(t, 22) - testfile := filepath.Join(analysistest.TestData(), "src", "versions", "go22.txtar") - runTxtarFile(t, testfile, loopclosure.Analyzer, "golang.org/fake/versions") + txtar := filepath.Join(analysistest.TestData(), "src", "versions", "go22.txtar") + dir := testfiles.ExtractTxtarToTmp(t, txtar) + analysistest.Run(t, dir, loopclosure.Analyzer, "golang.org/fake/versions") } func TestVersions18(t *testing.T) { - testfile := filepath.Join(analysistest.TestData(), "src", "versions", "go18.txtar") - runTxtarFile(t, testfile, loopclosure.Analyzer, "golang.org/fake/versions") -} - -// runTxtarFile unpacks a txtar archive to a directory, and runs -// analyzer on the given patterns. -// -// This is compatible with a go.mod file. -// -// TODO(taking): Consider unifying with analysistest. -func runTxtarFile(t *testing.T, path string, analyzer *analysis.Analyzer, patterns ...string) { - ar, err := txtar.ParseFile(path) - if err != nil { - t.Fatal(err) - } - - dir := t.TempDir() - for _, file := range ar.Files { - name, content := file.Name, file.Data - - filename := filepath.Join(dir, name) - os.MkdirAll(filepath.Dir(filename), 0777) // ignore error - if err := os.WriteFile(filename, content, 0666); err != nil { - t.Fatal(err) - } - } - - analysistest.Run(t, dir, analyzer, patterns...) + txtar := filepath.Join(analysistest.TestData(), "src", "versions", "go18.txtar") + dir := testfiles.ExtractTxtarToTmp(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 efee7ba..9186527 100644 --- a/go/analysis/passes/stdversion/stdversion_test.go +++ b/go/analysis/passes/stdversion/stdversion_test.go
@@ -5,15 +5,13 @@ package stdversion_test import ( - "os" "path/filepath" "testing" - "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/analysis/passes/stdversion" "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/txtar" + "golang.org/x/tools/internal/testfiles" ) func Test(t *testing.T) { @@ -21,36 +19,9 @@ // itself requires the go1.22 implementation of versions.FileVersions. testenv.NeedsGo1Point(t, 22) - testfile := filepath.Join(analysistest.TestData(), "test.txtar") - runTxtarFile(t, testfile, stdversion.Analyzer, + dir := testfiles.ExtractTxtarToTmp(t, filepath.Join(analysistest.TestData(), "test.txtar")) + analysistest.Run(t, dir, stdversion.Analyzer, "example.com/a", "example.com/sub", "example.com/old") } - -// runTxtarFile unpacks a txtar archive to a directory, and runs -// analyzer on the given patterns. -// -// This is compatible with a go.mod file. -// -// Plundered from loopclosure_test.go. -// TODO(golang/go#46136): add module support to analysistest. -func runTxtarFile(t *testing.T, path string, analyzer *analysis.Analyzer, patterns ...string) { - ar, err := txtar.ParseFile(path) - if err != nil { - t.Fatal(err) - } - - dir := t.TempDir() - for _, file := range ar.Files { - name, content := file.Name, file.Data - - filename := filepath.Join(dir, name) - os.MkdirAll(filepath.Dir(filename), 0777) // ignore error - if err := os.WriteFile(filename, content, 0666); err != nil { - t.Fatal(err) - } - } - - analysistest.Run(t, dir, analyzer, patterns...) -}
diff --git a/go/analysis/unitchecker/separate_test.go b/go/analysis/unitchecker/separate_test.go index 37e74e4..00c5aec 100644 --- a/go/analysis/unitchecker/separate_test.go +++ b/go/analysis/unitchecker/separate_test.go
@@ -24,6 +24,7 @@ "golang.org/x/tools/go/gcexportdata" "golang.org/x/tools/go/packages" "golang.org/x/tools/internal/testenv" + "golang.org/x/tools/internal/testfiles" "golang.org/x/tools/txtar" ) @@ -82,7 +83,7 @@ // Expand archive into tmp tree. tmpdir := t.TempDir() - if err := extractTxtar(txtar.Parse([]byte(src)), tmpdir); err != nil { + if err := testfiles.ExtractTxtar(tmpdir, txtar.Parse([]byte(src))); err != nil { t.Fatal(err) } @@ -291,19 +292,3 @@ type importerFunc func(path string) (*types.Package, error) func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) } - -// extractTxtar writes each archive file to the corresponding location beneath dir. -// -// TODO(adonovan): move this to txtar package, we need it all the time (#61386). -func extractTxtar(ar *txtar.Archive, dir string) error { - for _, file := range ar.Files { - name := filepath.Join(dir, file.Name) - if err := os.MkdirAll(filepath.Dir(name), 0777); err != nil { - return err - } - if err := os.WriteFile(name, file.Data, 0666); err != nil { - return err - } - } - return nil -}
diff --git a/go/ssa/builder_test.go b/go/ssa/builder_test.go index 607e64f..07b4a3c 100644 --- a/go/ssa/builder_test.go +++ b/go/ssa/builder_test.go
@@ -20,6 +20,7 @@ "strings" "testing" + "golang.org/x/tools/go/analysis/analysistest" "golang.org/x/tools/go/buildutil" "golang.org/x/tools/go/loader" "golang.org/x/tools/go/packages" @@ -27,7 +28,7 @@ "golang.org/x/tools/go/ssa/ssautil" "golang.org/x/tools/internal/aliases" "golang.org/x/tools/internal/testenv" - "golang.org/x/tools/txtar" + "golang.org/x/tools/internal/testfiles" ) func isEmpty(f *ssa.Function) bool { return f.Blocks == nil } @@ -172,38 +173,7 @@ func TestNoIndirectCreatePackage(t *testing.T) { testenv.NeedsGoBuild(t) // for go/packages - src := ` --- go.mod -- -module testdata -go 1.18 - --- a/a.go -- -package a - -import "testdata/b" - -func A() { - var x b.B - x.F() -} - --- b/b.go -- -package b - -import "testdata/c" - -type B struct { c.C } - --- c/c.go -- -package c - -type C int -func (C) F() {} -` - dir := t.TempDir() - if err := extractArchive(dir, src); err != nil { - t.Fatal(err) - } + dir := testfiles.ExtractTxtarToTmp(t, filepath.Join(analysistest.TestData(), "indirect.txtar")) pkgs, err := loadPackages(dir, "testdata/a") if err != nil { t.Fatal(err) @@ -235,27 +205,6 @@ } } -// extractArchive extracts the txtar archive into the specified directory. -func extractArchive(dir, arch string) error { - // TODO(adonovan): publish this a helper (#61386). - extractTxtar := func(ar *txtar.Archive, dir string) error { - for _, file := range ar.Files { - name := filepath.Join(dir, file.Name) - if err := os.MkdirAll(filepath.Dir(name), 0777); err != nil { - return err - } - if err := os.WriteFile(name, file.Data, 0666); err != nil { - return err - } - } - return nil - } - - // Extract archive to temporary tree. - ar := txtar.Parse([]byte(arch)) - return extractTxtar(ar, dir) -} - // loadPackages loads packages from the specified directory, using LoadSyntax. func loadPackages(dir string, patterns ...string) ([]*packages.Package, error) { cfg := &packages.Config{
diff --git a/go/ssa/testdata/indirect.txtar b/go/ssa/testdata/indirect.txtar new file mode 100644 index 0000000..595bd2e --- /dev/null +++ b/go/ssa/testdata/indirect.txtar
@@ -0,0 +1,26 @@ +-- go.mod -- +module testdata +go 1.18 + +-- a/a.go -- +package a + +import "testdata/b" + +func A() { + var x b.B + x.F() +} + +-- b/b.go -- +package b + +import "testdata/c" + +type B struct { c.C } + +-- c/c.go -- +package c + +type C int +func (C) F() {} \ No newline at end of file
diff --git a/internal/testfiles/testdata/somefile.txt b/internal/testfiles/testdata/somefile.txt new file mode 100644 index 0000000..8d9c108 --- /dev/null +++ b/internal/testfiles/testdata/somefile.txt
@@ -0,0 +1 @@ +A file to try to load. \ No newline at end of file
diff --git a/internal/testfiles/testdata/versions/go.mod.test b/internal/testfiles/testdata/versions/go.mod.test new file mode 100644 index 0000000..55f69e1 --- /dev/null +++ b/internal/testfiles/testdata/versions/go.mod.test
@@ -0,0 +1,5 @@ +// File is versions/go.mod after expansion with TestDir() + +module golang.org/fake/versions + +go 1.21
diff --git a/internal/testfiles/testdata/versions/mod.go b/internal/testfiles/testdata/versions/mod.go new file mode 100644 index 0000000..13117fd --- /dev/null +++ b/internal/testfiles/testdata/versions/mod.go
@@ -0,0 +1,3 @@ +// The file will be go1.21 from the go.mod. + +package versions // want "mod.go@go1.21"
diff --git a/internal/testfiles/testdata/versions/post.go b/internal/testfiles/testdata/versions/post.go new file mode 100644 index 0000000..8c1afde --- /dev/null +++ b/internal/testfiles/testdata/versions/post.go
@@ -0,0 +1,3 @@ +//go:build go1.22 + +package versions // want "post.go@go1.22"
diff --git a/internal/testfiles/testdata/versions/pre.go b/internal/testfiles/testdata/versions/pre.go new file mode 100644 index 0000000..2b5ca67 --- /dev/null +++ b/internal/testfiles/testdata/versions/pre.go
@@ -0,0 +1,3 @@ +//go:build go1.20 + +package versions // want "pre.go@go1.20"
diff --git a/internal/testfiles/testdata/versions/sub.test/sub.go b/internal/testfiles/testdata/versions/sub.test/sub.go new file mode 100644 index 0000000..3b2721b --- /dev/null +++ b/internal/testfiles/testdata/versions/sub.test/sub.go
@@ -0,0 +1 @@ +package sub // want "sub.go@go1.21"
diff --git a/internal/testfiles/testfiles.go b/internal/testfiles/testfiles.go new file mode 100644 index 0000000..ff73955 --- /dev/null +++ b/internal/testfiles/testfiles.go
@@ -0,0 +1,140 @@ +// 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 testfiles provides utilities for writing Go tests with files +// in testdata. +package testfiles + +import ( + "io" + "io/fs" + "os" + "path/filepath" + "strings" + "testing" + + "golang.org/x/tools/txtar" +) + +// CopyDirToTmp copies dir to a temporary test directory using +// CopyTestFiles and returns the path to the test directory. +func CopyDirToTmp(t testing.TB, srcdir string) string { + dst := t.TempDir() + if err := CopyFS(dst, os.DirFS(srcdir)); err != nil { + t.Fatal(err) + } + return dst +} + +// CopyFS copies the files and directories in src to a +// destination directory dst. Paths to files and directories +// ending in a ".test" extension have the ".test" extension +// removed. This allows tests to hide files whose names have +// special meaning, such as "go.mod" files or "testdata" directories +// from the go command, or ill-formed Go source files from gofmt. +// +// For example if we copy the directory testdata: +// +// testdata/ +// go.mod.test +// a/a.go +// b/b.go +// +// The resulting files will be: +// +// dst/ +// go.mod +// a/a.go +// b/b.go +func CopyFS(dstdir string, src fs.FS) error { + if err := copyFS(dstdir, src); err != nil { + return err + } + + // Collect ".test" paths in lexical order. + var rename []string + err := fs.WalkDir(os.DirFS(dstdir), ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if strings.HasSuffix(path, ".test") { + rename = append(rename, path) + } + return nil + }) + if err != nil { + return err + } + + // Rename the .test paths in reverse lexical order, e.g. + // in d.test/a.test renames a.test to d.test/a then d.test to d. + for i := len(rename) - 1; i >= 0; i-- { + oldpath := filepath.Join(dstdir, rename[i]) + newpath := strings.TrimSuffix(oldpath, ".test") + if err != os.Rename(oldpath, newpath) { + return err + } + } + return nil +} + +// Copy the files in src to dst. +// Use os.CopyFS when 1.23 can be used in x/tools. +func copyFS(dstdir string, src fs.FS) error { + return fs.WalkDir(src, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + newpath := filepath.Join(dstdir, path) + if d.IsDir() { + return os.MkdirAll(newpath, 0777) + } + r, err := src.Open(path) + if err != nil { + return err + } + defer r.Close() + + w, err := os.Create(newpath) + if err != nil { + return err + } + defer w.Close() + _, err = io.Copy(w, r) + return err + }) +} + +// ExtractTxtar writes each archive file to the corresponding location beneath dir. +// +// TODO(adonovan): move this to txtar package, we need it all the time (#61386). +func ExtractTxtar(dstdir string, ar *txtar.Archive) error { + for _, file := range ar.Files { + name := filepath.Join(dstdir, file.Name) + if err := os.MkdirAll(filepath.Dir(name), 0777); err != nil { + return err + } + if err := os.WriteFile(name, file.Data, 0666); err != nil { + return err + } + } + return nil +} + +// ExtractTxtarToTmp 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) + if err != nil { + t.Fatal(err) + } + + dir := t.TempDir() + err = ExtractTxtar(dir, ar) + if err != nil { + t.Fatal(err) + } + return dir +}
diff --git a/internal/testfiles/testfiles_test.go b/internal/testfiles/testfiles_test.go new file mode 100644 index 0000000..d956326 --- /dev/null +++ b/internal/testfiles/testfiles_test.go
@@ -0,0 +1,54 @@ +// 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 testfiles_test + +import ( + "os" + "path/filepath" + "testing" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/analysistest" + "golang.org/x/tools/internal/testenv" + "golang.org/x/tools/internal/testfiles" + "golang.org/x/tools/internal/versions" +) + +func TestTestDir(t *testing.T) { + testenv.NeedsGo1Point(t, 22) + + // TODO(taking): Expose a helper for this pattern? + // dir must contain a go.mod file to be picked up by Run(). + // So this pattern or Join(TestDir(t, TestData()), "versions") are + // probably what everyone will want. + dir := testfiles.CopyDirToTmp(t, filepath.Join(analysistest.TestData(), "versions")) + + filever := &analysis.Analyzer{ + Name: "filever", + Doc: "reports file go versions", + Run: func(pass *analysis.Pass) (any, error) { + for _, file := range pass.Files { + ver := versions.FileVersion(pass.TypesInfo, file) + name := filepath.Base(pass.Fset.Position(file.Package).Filename) + pass.Reportf(file.Package, "%s@%s", name, ver) + } + return nil, nil + }, + } + analysistest.Run(t, dir, filever, "golang.org/fake/versions", "golang.org/fake/versions/sub") +} + +func TestCopyTestFilesErrors(t *testing.T) { + tmp := t.TempDir() // a real tmp dir + for _, dir := range []string{ + filepath.Join(analysistest.TestData(), "not_there"), // dir does not exist + filepath.Join(analysistest.TestData(), "somefile.txt"), // not a dir + } { + err := testfiles.CopyFS(tmp, os.DirFS(dir)) + if err == nil { + t.Error("Expected an error from CopyTestFiles") + } + } +}