| // Copyright 2018 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 packages_test |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "go/ast" |
| "go/parser" |
| "go/token" |
| "go/types" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "reflect" |
| "runtime" |
| "sort" |
| "strings" |
| "sync" |
| "testing" |
| |
| "golang.org/x/tools/go/packages" |
| ) |
| |
| func init() { |
| // Insulate the tests from the users' environment. |
| os.Setenv("GOPACKAGESDRIVER", "off") |
| } |
| |
| // TODO(matloob): remove this once Go 1.12 is released as we will end support |
| // for versions of go list before Go 1.10.4. |
| var usesOldGolist = false |
| |
| // TODO(adonovan): more test cases to write: |
| // |
| // - When the tests fail, make them print a 'cd & load' command |
| // that will allow the maintainer to interact with the failing scenario. |
| // - errors in go-list metadata |
| // - a foo.test package that cannot be built for some reason (e.g. |
| // import error) will result in a JSON blob with no name and a |
| // nonexistent testmain file in GoFiles. Test that we handle this |
| // gracefully. |
| // - test more Flags. |
| // |
| // LoadSyntax & LoadAllSyntax modes: |
| // - Fset may be user-supplied or not. |
| // - Packages.Info is correctly set. |
| // - typechecker configuration is honored |
| // - import cycles are gracefully handled in type checker. |
| // - test typechecking of generated test main and cgo. |
| |
| // The zero-value of Config has LoadFiles mode. |
| func TestLoadZeroConfig(t *testing.T) { |
| initial, err := packages.Load(nil, "hash") |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(initial) != 1 { |
| t.Fatalf("got %s, want [hash]", initial) |
| } |
| hash := initial[0] |
| // Even though the hash package has imports, |
| // they are not reported. |
| got := fmt.Sprintf("name=%s srcs=%v imports=%v", hash.Name, srcs(hash), hash.Imports) |
| want := "name=hash srcs=[hash.go] imports=map[]" |
| if got != want { |
| t.Fatalf("got %s, want %s", got, want) |
| } |
| } |
| |
| func TestLoadImportsGraph(t *testing.T) { |
| tmp, cleanup := makeTree(t, map[string]string{ |
| "src/a/a.go": `package a; const A = 1`, |
| "src/b/b.go": `package b; import ("a"; _ "errors"); var B = a.A`, |
| "src/c/c.go": `package c; import (_ "b"; _ "unsafe")`, |
| "src/c/c2.go": "// +build ignore\n\n" + `package c; import _ "fmt"`, |
| "src/subdir/d/d.go": `package d`, |
| "src/subdir/d/d_test.go": `package d; import _ "math/bits"`, |
| "src/subdir/d/x_test.go": `package d_test; import _ "subdir/d"`, // TODO(adonovan): test bad import here |
| "src/subdir/e/d.go": `package e`, |
| "src/e/e.go": `package main; import _ "b"`, |
| "src/e/e2.go": `package main; import _ "c"`, |
| "src/f/f.go": `package f`, |
| }) |
| defer cleanup() |
| |
| cfg := &packages.Config{ |
| Mode: packages.LoadImports, |
| Env: append(os.Environ(), "GOPATH="+tmp), |
| } |
| initial, err := packages.Load(cfg, "c", "subdir/d", "e") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Check graph topology. |
| graph, all := importGraph(initial) |
| wantGraph := ` |
| a |
| b |
| * c |
| * e |
| errors |
| * subdir/d |
| unsafe |
| b -> a |
| b -> errors |
| c -> b |
| c -> unsafe |
| e -> b |
| e -> c |
| `[1:] |
| |
| if graph != wantGraph { |
| t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantGraph) |
| } |
| |
| cfg.Tests = true |
| initial, err = packages.Load(cfg, "c", "subdir/d", "e") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Check graph topology. |
| graph, all = importGraph(initial) |
| wantGraph = ` |
| a |
| b |
| * c |
| * e |
| errors |
| math/bits |
| * subdir/d |
| * subdir/d [subdir/d.test] |
| * subdir/d.test |
| * subdir/d_test [subdir/d.test] |
| unsafe |
| b -> a |
| b -> errors |
| c -> b |
| c -> unsafe |
| e -> b |
| e -> c |
| subdir/d [subdir/d.test] -> math/bits |
| subdir/d.test -> os (pruned) |
| subdir/d.test -> subdir/d [subdir/d.test] |
| subdir/d.test -> subdir/d_test [subdir/d.test] |
| subdir/d.test -> testing (pruned) |
| subdir/d.test -> testing/internal/testdeps (pruned) |
| subdir/d_test [subdir/d.test] -> subdir/d [subdir/d.test] |
| `[1:] |
| |
| // Legacy go list support does not create test main package. |
| wantOldGraph := ` |
| a |
| b |
| * c |
| * e |
| errors |
| math/bits |
| * subdir/d |
| * subdir/d [subdir/d.test] |
| * subdir/d_test [subdir/d.test] |
| unsafe |
| b -> a |
| b -> errors |
| c -> b |
| c -> unsafe |
| e -> b |
| e -> c |
| subdir/d [subdir/d.test] -> math/bits |
| subdir/d_test [subdir/d.test] -> subdir/d [subdir/d.test] |
| `[1:] |
| |
| if graph != wantGraph && !usesOldGolist { |
| t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantGraph) |
| } else if graph != wantOldGraph && usesOldGolist { |
| t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantOldGraph) |
| } |
| |
| // Check node information: kind, name, srcs. |
| for _, test := range []struct { |
| id string |
| wantName string |
| wantKind string |
| wantSrcs string |
| }{ |
| {"a", "a", "package", "a.go"}, |
| {"b", "b", "package", "b.go"}, |
| {"c", "c", "package", "c.go"}, // c2.go is ignored |
| {"e", "main", "command", "e.go e2.go"}, |
| {"errors", "errors", "package", "errors.go"}, |
| {"subdir/d", "d", "package", "d.go"}, |
| {"subdir/d.test", "main", "command", "0.go"}, |
| {"unsafe", "unsafe", "package", ""}, |
| } { |
| if usesOldGolist && test.id == "subdir/d.test" { |
| // Legacy go list support does not create test main package. |
| continue |
| } |
| p, ok := all[test.id] |
| if !ok { |
| t.Errorf("no package %s", test.id) |
| continue |
| } |
| if p.Name != test.wantName { |
| t.Errorf("%s.Name = %q, want %q", test.id, p.Name, test.wantName) |
| } |
| |
| // kind |
| var kind string |
| if p.Name == "main" { |
| kind += "command" |
| } else { |
| kind += "package" |
| } |
| if kind != test.wantKind { |
| t.Errorf("%s.Kind = %q, want %q", test.id, kind, test.wantKind) |
| } |
| |
| if srcs := strings.Join(srcs(p), " "); srcs != test.wantSrcs { |
| t.Errorf("%s.Srcs = [%s], want [%s]", test.id, srcs, test.wantSrcs) |
| } |
| } |
| |
| // Test an ad-hoc package, analogous to "go run hello.go". |
| if initial, err := packages.Load(cfg, filepath.Join(tmp, "src/c/c.go")); len(initial) == 0 { |
| t.Errorf("failed to obtain metadata for ad-hoc package: %s", err) |
| } else { |
| got := fmt.Sprintf("%s %s", initial[0].ID, srcs(initial[0])) |
| if want := "command-line-arguments [c.go]"; got != want && !usesOldGolist { |
| t.Errorf("oops: got %s, want %s", got, want) |
| } |
| } |
| |
| // Wildcards |
| // See StdlibTest for effective test of "std" wildcard. |
| // TODO(adonovan): test "all" returns everything in the current module. |
| { |
| // "..." (subdirectory) |
| initial, err = packages.Load(cfg, "subdir/...") |
| if err != nil { |
| t.Fatal(err) |
| } |
| graph, all = importGraph(initial) |
| wantGraph = ` |
| math/bits |
| * subdir/d |
| * subdir/d [subdir/d.test] |
| * subdir/d.test |
| * subdir/d_test [subdir/d.test] |
| * subdir/e |
| subdir/d [subdir/d.test] -> math/bits |
| subdir/d.test -> os (pruned) |
| subdir/d.test -> subdir/d [subdir/d.test] |
| subdir/d.test -> subdir/d_test [subdir/d.test] |
| subdir/d.test -> testing (pruned) |
| subdir/d.test -> testing/internal/testdeps (pruned) |
| subdir/d_test [subdir/d.test] -> subdir/d [subdir/d.test] |
| `[1:] |
| |
| // Legacy go list support does not create test main package. |
| wantOldGraph = ` |
| math/bits |
| * subdir/d |
| * subdir/d [subdir/d.test] |
| * subdir/d_test [subdir/d.test] |
| * subdir/e |
| subdir/d [subdir/d.test] -> math/bits |
| subdir/d_test [subdir/d.test] -> subdir/d [subdir/d.test] |
| `[1:] |
| if graph != wantGraph && !usesOldGolist { |
| t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantGraph) |
| } else if graph != wantOldGraph && usesOldGolist { |
| t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantOldGraph) |
| } |
| } |
| } |
| |
| func TestLoadImportsTestVariants(t *testing.T) { |
| if usesOldGolist { |
| t.Skip("not yet supported in pre-Go 1.10.4 golist fallback implementation") |
| } |
| |
| tmp, cleanup := makeTree(t, map[string]string{ |
| "src/a/a.go": `package a; import _ "b"`, |
| "src/b/b.go": `package b`, |
| "src/b/b_test.go": `package b`, |
| "src/b/bx_test.go": `package b_test; import _ "a"`, |
| }) |
| defer cleanup() |
| |
| cfg := &packages.Config{ |
| Mode: packages.LoadImports, |
| Env: append(os.Environ(), "GOPATH="+tmp), |
| Tests: true, |
| } |
| initial, err := packages.Load(cfg, "a", "b") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Check graph topology. |
| graph, _ := importGraph(initial) |
| wantGraph := ` |
| * a |
| a [b.test] |
| * b |
| * b [b.test] |
| * b.test |
| * b_test [b.test] |
| a -> b |
| a [b.test] -> b [b.test] |
| b.test -> b [b.test] |
| b.test -> b_test [b.test] |
| b.test -> os (pruned) |
| b.test -> testing (pruned) |
| b.test -> testing/internal/testdeps (pruned) |
| b_test [b.test] -> a [b.test] |
| `[1:] |
| |
| if graph != wantGraph { |
| t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantGraph) |
| } |
| } |
| |
| func TestLoadImportsC(t *testing.T) { |
| // This test checks that when a package depends on the |
| // test variant of "syscall", "unsafe", or "runtime/cgo", that dependency |
| // is not removed when those packages are added when it imports "C". |
| // |
| // For this test to work, the external test of syscall must have a dependency |
| // on net, and net must import "syscall" and "C". |
| if runtime.GOOS == "windows" { |
| t.Skipf("skipping on windows; packages on windows do not satisfy conditions for test.") |
| } |
| |
| if usesOldGolist { |
| t.Skip("not yet supported in pre-Go 1.10.4 golist fallback implementation") |
| } |
| |
| cfg := &packages.Config{ |
| Mode: packages.LoadImports, |
| Tests: true, |
| } |
| initial, err := packages.Load(cfg, "syscall", "net") |
| if err != nil { |
| t.Fatalf("failed to load imports: %v", err) |
| } |
| |
| _, all := importGraph(initial) |
| |
| for _, test := range []struct { |
| pattern string |
| wantImport string // an import to check for |
| }{ |
| {"net", "syscall:syscall"}, |
| {"net [syscall.test]", "syscall:syscall [syscall.test]"}, |
| {"syscall_test [syscall.test]", "net:net [syscall.test]"}, |
| } { |
| // Test the import paths. |
| pkg := all[test.pattern] |
| if pkg == nil { |
| t.Errorf("package %q not loaded", test.pattern) |
| } |
| if imports := strings.Join(imports(pkg), " "); !strings.Contains(imports, test.wantImport) { |
| t.Errorf("package %q: got \n%s, \nwant to have %s", test.pattern, imports, test.wantImport) |
| } |
| } |
| } |
| |
| func TestVendorImports(t *testing.T) { |
| tmp, cleanup := makeTree(t, map[string]string{ |
| "src/a/a.go": `package a; import _ "b"; import _ "c";`, |
| "src/a/vendor/b/b.go": `package b; import _ "c"`, |
| "src/c/c.go": `package c; import _ "b"`, |
| "src/c/vendor/b/b.go": `package b`, |
| }) |
| defer cleanup() |
| |
| cfg := &packages.Config{ |
| Mode: packages.LoadImports, |
| Env: append(os.Environ(), "GOPATH="+tmp), |
| } |
| initial, err := packages.Load(cfg, "a", "c") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| graph, all := importGraph(initial) |
| wantGraph := ` |
| * a |
| a/vendor/b |
| * c |
| c/vendor/b |
| a -> a/vendor/b |
| a -> c |
| a/vendor/b -> c |
| c -> c/vendor/b |
| `[1:] |
| if graph != wantGraph { |
| t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantGraph) |
| } |
| |
| for _, test := range []struct { |
| pattern string |
| wantImports string |
| }{ |
| {"a", "b:a/vendor/b c:c"}, |
| {"c", "b:c/vendor/b"}, |
| {"a/vendor/b", "c:c"}, |
| {"c/vendor/b", ""}, |
| } { |
| // Test the import paths. |
| pkg := all[test.pattern] |
| if imports := strings.Join(imports(pkg), " "); imports != test.wantImports { |
| t.Errorf("package %q: got %s, want %s", test.pattern, imports, test.wantImports) |
| } |
| } |
| } |
| |
| func imports(p *packages.Package) []string { |
| if p == nil { |
| return nil |
| } |
| keys := make([]string, 0, len(p.Imports)) |
| for k, v := range p.Imports { |
| keys = append(keys, fmt.Sprintf("%s:%s", k, v.ID)) |
| } |
| sort.Strings(keys) |
| return keys |
| } |
| |
| func TestConfigDir(t *testing.T) { |
| tmp, cleanup := makeTree(t, map[string]string{ |
| "src/a/a.go": `package a; const Name = "a" `, |
| "src/a/b/b.go": `package b; const Name = "a/b"`, |
| "src/b/b.go": `package b; const Name = "b"`, |
| }) |
| defer cleanup() |
| |
| for _, test := range []struct { |
| dir string |
| pattern string |
| want string // value of Name constant, or error |
| }{ |
| {"", "a", `"a"`}, |
| {"", "b", `"b"`}, |
| {"", "./a", "packages not found"}, |
| {"", "./b", "packages not found"}, |
| {filepath.Join(tmp, "/src"), "a", `"a"`}, |
| {filepath.Join(tmp, "/src"), "b", `"b"`}, |
| {filepath.Join(tmp, "/src"), "./a", `"a"`}, |
| {filepath.Join(tmp, "/src"), "./b", `"b"`}, |
| {filepath.Join(tmp, "/src/a"), "a", `"a"`}, |
| {filepath.Join(tmp, "/src/a"), "b", `"b"`}, |
| {filepath.Join(tmp, "/src/a"), "./a", "packages not found"}, |
| {filepath.Join(tmp, "/src/a"), "./b", `"a/b"`}, |
| } { |
| cfg := &packages.Config{ |
| Mode: packages.LoadSyntax, // Use LoadSyntax to ensure that files can be opened. |
| Dir: test.dir, |
| Env: append(os.Environ(), "GOPATH="+tmp), |
| } |
| |
| initial, err := packages.Load(cfg, test.pattern) |
| var got string |
| if err != nil { |
| got = err.Error() |
| } else { |
| got = constant(initial[0], "Name").Val().String() |
| } |
| if got != test.want { |
| t.Errorf("dir %q, pattern %q: got %s, want %s", |
| test.dir, test.pattern, got, test.want) |
| } |
| } |
| |
| } |
| |
| func TestConfigFlags(t *testing.T) { |
| // Test satisfying +build line tags, with -tags flag. |
| tmp, cleanup := makeTree(t, map[string]string{ |
| // package a |
| "src/a/a.go": `package a; import _ "a/b"`, |
| "src/a/b.go": `// +build tag |
| |
| package a`, |
| "src/a/c.go": `// +build tag tag2 |
| |
| package a`, |
| "src/a/d.go": `// +build tag,tag2 |
| |
| package a`, |
| // package a/b |
| "src/a/b/a.go": `package b`, |
| "src/a/b/b.go": `// +build tag |
| |
| package b`, |
| }) |
| defer cleanup() |
| |
| for _, test := range []struct { |
| pattern string |
| tags []string |
| wantSrcs string |
| wantImportSrcs string |
| }{ |
| {`a`, []string{}, "a.go", "a.go"}, |
| {`a`, []string{`-tags=tag`}, "a.go b.go c.go", "a.go b.go"}, |
| {`a`, []string{`-tags=tag2`}, "a.go c.go", "a.go"}, |
| {`a`, []string{`-tags=tag tag2`}, "a.go b.go c.go d.go", "a.go b.go"}, |
| } { |
| cfg := &packages.Config{ |
| Mode: packages.LoadImports, |
| Flags: test.tags, |
| Env: append(os.Environ(), "GOPATH="+tmp), |
| } |
| |
| initial, err := packages.Load(cfg, test.pattern) |
| if err != nil { |
| t.Error(err) |
| continue |
| } |
| if len(initial) != 1 { |
| t.Errorf("test tags %v: pattern %s, expected 1 package, got %d packages.", test.tags, test.pattern, len(initial)) |
| continue |
| } |
| pkg := initial[0] |
| if srcs := strings.Join(srcs(pkg), " "); srcs != test.wantSrcs { |
| t.Errorf("test tags %v: srcs of package %s = [%s], want [%s]", test.tags, test.pattern, srcs, test.wantSrcs) |
| } |
| for path, ipkg := range pkg.Imports { |
| if srcs := strings.Join(srcs(ipkg), " "); srcs != test.wantImportSrcs { |
| t.Errorf("build tags %v: srcs of imported package %s = [%s], want [%s]", test.tags, path, srcs, test.wantImportSrcs) |
| } |
| } |
| |
| } |
| } |
| |
| type errCollector struct { |
| mu sync.Mutex |
| errors []error |
| } |
| |
| func (ec *errCollector) add(err error) { |
| ec.mu.Lock() |
| ec.errors = append(ec.errors, err) |
| ec.mu.Unlock() |
| } |
| |
| func TestLoadTypes(t *testing.T) { |
| // In LoadTypes and LoadSyntax modes, the compiler will |
| // fail to generate an export data file for c, because it has |
| // a type error. The loader should fall back loading a and c |
| // from source, but use the export data for b. |
| |
| tmp, cleanup := makeTree(t, map[string]string{ |
| "src/a/a.go": `package a; import "b"; import "c"; const A = "a" + b.B + c.C`, |
| "src/b/b.go": `package b; const B = "b"`, |
| "src/c/c.go": `package c; const C = "c" + 1`, |
| }) |
| defer cleanup() |
| |
| cfg := &packages.Config{ |
| Mode: packages.LoadTypes, |
| Env: append(os.Environ(), "GOPATH="+tmp), |
| Error: func(error) {}, |
| } |
| initial, err := packages.Load(cfg, "a") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| graph, all := importGraph(initial) |
| wantGraph := ` |
| * a |
| b |
| c |
| a -> b |
| a -> c |
| `[1:] |
| if graph != wantGraph { |
| t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantGraph) |
| } |
| |
| for _, test := range []struct { |
| id string |
| wantSyntax bool |
| }{ |
| {"a", true}, // need src, no export data for c |
| {"b", false}, // use export data |
| {"c", true}, // need src, no export data for c |
| } { |
| if usesOldGolist && !test.wantSyntax { |
| // legacy go list always upgrades to LoadAllSyntax, syntax will be filled in. |
| // still check that types information is complete. |
| test.wantSyntax = true |
| } |
| p := all[test.id] |
| if p == nil { |
| t.Errorf("missing package: %s", test.id) |
| continue |
| } |
| if p.Types == nil { |
| t.Errorf("missing types.Package for %s", p) |
| continue |
| } else if !p.Types.Complete() { |
| t.Errorf("incomplete types.Package for %s", p) |
| } |
| if (p.Syntax != nil) != test.wantSyntax { |
| if test.wantSyntax { |
| t.Errorf("missing ast.Files for %s", p) |
| } else { |
| t.Errorf("unexpected ast.Files for for %s", p) |
| } |
| } |
| } |
| } |
| |
| func TestLoadSyntaxOK(t *testing.T) { |
| tmp, cleanup := makeTree(t, map[string]string{ |
| "src/a/a.go": `package a; import "b"; const A = "a" + b.B`, |
| "src/b/b.go": `package b; import "c"; const B = "b" + c.C`, |
| "src/c/c.go": `package c; import "d"; const C = "c" + d.D`, |
| "src/d/d.go": `package d; import "e"; const D = "d" + e.E`, |
| "src/e/e.go": `package e; import "f"; const E = "e" + f.F`, |
| "src/f/f.go": `package f; const F = "f"`, |
| }) |
| defer cleanup() |
| |
| cfg := &packages.Config{ |
| Mode: packages.LoadSyntax, |
| Env: append(os.Environ(), "GOPATH="+tmp), |
| Error: func(error) {}, |
| } |
| initial, err := packages.Load(cfg, "a", "c") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| graph, all := importGraph(initial) |
| wantGraph := ` |
| * a |
| b |
| * c |
| d |
| e |
| f |
| a -> b |
| b -> c |
| c -> d |
| d -> e |
| e -> f |
| `[1:] |
| if graph != wantGraph { |
| t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantGraph) |
| } |
| |
| // TODO(matloob): The legacy go list based support loads everything from source |
| // because it doesn't do a build and the .a files don't exist. |
| // Can we simulate its existence? |
| |
| for _, test := range []struct { |
| id string |
| wantSyntax bool |
| }{ |
| {"a", true}, // source package |
| {"b", true}, // source package |
| {"c", true}, // source package |
| {"d", false}, // export data package |
| {"e", false}, // export data package |
| {"f", false}, // export data package |
| } { |
| if usesOldGolist && !test.wantSyntax { |
| // legacy go list always upgrades to LoadAllSyntax, syntax will be filled in. |
| test.wantSyntax = true |
| } |
| p := all[test.id] |
| if p == nil { |
| t.Errorf("missing package: %s", test.id) |
| continue |
| } |
| if p.Types == nil { |
| t.Errorf("missing types.Package for %s", p) |
| continue |
| } else if !p.Types.Complete() { |
| t.Errorf("incomplete types.Package for %s", p) |
| } |
| if (p.Syntax != nil) != test.wantSyntax { |
| if test.wantSyntax { |
| t.Errorf("missing ast.Files for %s", p) |
| } else { |
| t.Errorf("unexpected ast.Files for for %s", p) |
| } |
| } |
| if p.Errors != nil { |
| t.Errorf("errors in package: %s: %s", p, p.Errors) |
| } |
| } |
| |
| // Check value of constant. |
| aA := constant(all["a"], "A") |
| if got, want := fmt.Sprintf("%v %v", aA, aA.Val()), `const a.A untyped string "abcdef"`; got != want { |
| t.Errorf("a.A: got %s, want %s", got, want) |
| } |
| } |
| |
| func TestLoadSyntaxError(t *testing.T) { |
| // A type error in a lower-level package (e) prevents go list |
| // from producing export data for all packages that depend on it |
| // [a-e]. Only f should be loaded from export data, and the rest |
| // should be IllTyped. |
| tmp, cleanup := makeTree(t, map[string]string{ |
| "src/a/a.go": `package a; import "b"; const A = "a" + b.B`, |
| "src/b/b.go": `package b; import "c"; const B = "b" + c.C`, |
| "src/c/c.go": `package c; import "d"; const C = "c" + d.D`, |
| "src/d/d.go": `package d; import "e"; const D = "d" + e.E`, |
| "src/e/e.go": `package e; import "f"; const E = "e" + f.F + 1`, // type error |
| "src/f/f.go": `package f; const F = "f"`, |
| }) |
| defer cleanup() |
| |
| cfg := &packages.Config{ |
| Mode: packages.LoadSyntax, |
| Env: append(os.Environ(), "GOPATH="+tmp), |
| Error: func(error) {}, |
| } |
| initial, err := packages.Load(cfg, "a", "c") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| all := make(map[string]*packages.Package) |
| var visit func(p *packages.Package) |
| visit = func(p *packages.Package) { |
| if all[p.ID] == nil { |
| all[p.ID] = p |
| for _, imp := range p.Imports { |
| visit(imp) |
| } |
| } |
| } |
| for _, p := range initial { |
| visit(p) |
| } |
| |
| for _, test := range []struct { |
| id string |
| wantSyntax bool |
| wantIllTyped bool |
| }{ |
| {"a", true, true}, |
| {"b", true, true}, |
| {"c", true, true}, |
| {"d", true, true}, |
| {"e", true, true}, |
| {"f", false, false}, |
| } { |
| if usesOldGolist && !test.wantSyntax { |
| // legacy go list always upgrades to LoadAllSyntax, syntax will be filled in. |
| test.wantSyntax = true |
| } |
| p := all[test.id] |
| if p == nil { |
| t.Errorf("missing package: %s", test.id) |
| continue |
| } |
| if p.Types == nil { |
| t.Errorf("missing types.Package for %s", p) |
| continue |
| } else if !p.Types.Complete() { |
| t.Errorf("incomplete types.Package for %s", p) |
| } |
| if (p.Syntax != nil) != test.wantSyntax { |
| if test.wantSyntax { |
| t.Errorf("missing ast.Files for %s", test.id) |
| } else { |
| t.Errorf("unexpected ast.Files for for %s", test.id) |
| } |
| } |
| if p.IllTyped != test.wantIllTyped { |
| t.Errorf("IllTyped was %t for %s", p.IllTyped, test.id) |
| } |
| } |
| |
| // Check value of constant. |
| aA := constant(all["a"], "A") |
| if got, want := aA.String(), `const a.A invalid type`; got != want { |
| t.Errorf("a.A: got %s, want %s", got, want) |
| } |
| } |
| |
| // This function tests use of the ParseFile hook to supply |
| // alternative file contents to the parser and type-checker. |
| func TestLoadAllSyntaxOverlay(t *testing.T) { |
| type M = map[string]string |
| |
| tmp, cleanup := makeTree(t, M{ |
| "src/a/a.go": `package a; import "b"; const A = "a" + b.B`, |
| "src/b/b.go": `package b; import "c"; const B = "b" + c.C`, |
| "src/c/c.go": `package c; const C = "c"`, |
| "src/d/d.go": `package d; const D = "d"`, |
| }) |
| defer cleanup() |
| |
| for i, test := range []struct { |
| overlay M |
| want string // expected value of a.A |
| wantErrs []string |
| }{ |
| {nil, `"abc"`, nil}, // default |
| {M{}, `"abc"`, nil}, // empty overlay |
| {M{filepath.Join(tmp, "src/c/c.go"): `package c; const C = "C"`}, `"abC"`, nil}, |
| {M{filepath.Join(tmp, "src/b/b.go"): `package b; import "c"; const B = "B" + c.C`}, `"aBc"`, nil}, |
| {M{filepath.Join(tmp, "src/b/b.go"): `package b; import "d"; const B = "B" + d.D`}, `unknown`, |
| []string{`could not import d (no metadata for d)`}}, |
| } { |
| var parseFile func(fset *token.FileSet, filename string) (*ast.File, error) |
| if test.overlay != nil { |
| parseFile = func(fset *token.FileSet, filename string) (*ast.File, error) { |
| var src interface{} |
| if content, ok := test.overlay[filename]; ok { |
| src = content |
| } |
| const mode = parser.AllErrors | parser.ParseComments |
| return parser.ParseFile(fset, filename, src, mode) |
| } |
| } |
| var errs errCollector |
| cfg := &packages.Config{ |
| Mode: packages.LoadAllSyntax, |
| Env: append(os.Environ(), "GOPATH="+tmp), |
| Error: errs.add, |
| ParseFile: parseFile, |
| } |
| initial, err := packages.Load(cfg, "a") |
| if err != nil { |
| t.Error(err) |
| continue |
| } |
| |
| // Check value of a.A. |
| a := initial[0] |
| got := constant(a, "A").Val().String() |
| if got != test.want { |
| t.Errorf("%d. a.A: got %s, want %s", i, got, test.want) |
| } |
| |
| if errs := errorMessages(errs.errors); !reflect.DeepEqual(errs, test.wantErrs) { |
| t.Errorf("%d. got errors %s, want %s", i, errs, test.wantErrs) |
| } |
| } |
| } |
| |
| func TestLoadAllSyntaxImportErrors(t *testing.T) { |
| // TODO(matloob): Remove this once go list -e -compiled is fixed. See golang.org/issue/26755 |
| t.Skip("go list -compiled -e fails with non-zero exit status for empty packages") |
| |
| if usesOldGolist { |
| t.Skip("not yet supported in pre-Go 1.10.4 golist fallback implementation") |
| } |
| |
| tmp, cleanup := makeTree(t, map[string]string{ |
| "src/unicycle/unicycle.go": `package unicycle; import _ "unicycle"`, |
| "src/bicycle1/bicycle1.go": `package bicycle1; import _ "bicycle2"`, |
| "src/bicycle2/bicycle2.go": `package bicycle2; import _ "bicycle1"`, |
| "src/bad/bad.go": `not a package declaration`, |
| "src/root/root.go": `package root |
| import ( |
| _ "bicycle1" |
| _ "unicycle" |
| _ "nonesuch" |
| _ "empty" |
| _ "bad" |
| )`, |
| }) |
| defer cleanup() |
| |
| os.Mkdir(filepath.Join(tmp, "src/empty"), 0777) // create an existing but empty package |
| |
| var errs2 errCollector |
| cfg := &packages.Config{ |
| Mode: packages.LoadAllSyntax, |
| Env: append(os.Environ(), "GOPATH="+tmp), |
| Error: errs2.add, |
| } |
| initial, err := packages.Load(cfg, "root") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // Cycle-forming edges are removed from the graph: |
| // bicycle2 -> bicycle1 |
| // unicycle -> unicycle |
| graph, all := importGraph(initial) |
| wantGraph := ` |
| bicycle1 |
| bicycle2 |
| * root |
| unicycle |
| bicycle1 -> bicycle2 |
| root -> bicycle1 |
| root -> unicycle |
| `[1:] |
| if graph != wantGraph { |
| t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantGraph) |
| } |
| for _, test := range []struct { |
| id string |
| wantErrs []string |
| }{ |
| {"bicycle1", nil}, |
| {"bicycle2", []string{ |
| "could not import bicycle1 (import cycle: [root bicycle1 bicycle2])", |
| }}, |
| {"unicycle", []string{ |
| "could not import unicycle (import cycle: [root unicycle])", |
| }}, |
| {"root", []string{ |
| `could not import bad (missing package: "bad")`, |
| `could not import empty (missing package: "empty")`, |
| `could not import nonesuch (missing package: "nonesuch")`, |
| }}, |
| } { |
| p := all[test.id] |
| if p == nil { |
| t.Errorf("missing package: %s", test.id) |
| continue |
| } |
| if p.Types == nil { |
| t.Errorf("missing types.Package for %s", test.id) |
| } |
| if p.Syntax == nil { |
| t.Errorf("missing ast.Files for %s", test.id) |
| } |
| if !p.IllTyped { |
| t.Errorf("IllTyped was false for %s", test.id) |
| } |
| if errs := errorMessages(p.Errors); !reflect.DeepEqual(errs, test.wantErrs) { |
| t.Errorf("in package %s, got errors %s, want %s", p, errs, test.wantErrs) |
| } |
| } |
| } |
| |
| func TestAbsoluteFilenames(t *testing.T) { |
| tmp, cleanup := makeTree(t, map[string]string{ |
| "src/a/a.go": `package a; const A = 1`, |
| "src/b/b.go": `package b; import ("a"; _ "errors"); var B = a.A`, |
| "src/b/vendor/a/a.go": `package a; const A = 1`, |
| "src/c/c.go": `package c; import (_ "b"; _ "unsafe")`, |
| "src/c/c2.go": "// +build ignore\n\n" + `package c; import _ "fmt"`, |
| "src/subdir/d/d.go": `package d`, |
| "src/subdir/e/d.go": `package e`, |
| "src/e/e.go": `package main; import _ "b"`, |
| "src/e/e2.go": `package main; import _ "c"`, |
| "src/f/f.go": `package f`, |
| "src/f/f.s": ``, |
| }) |
| defer cleanup() |
| |
| checkFile := func(filename string) { |
| if !filepath.IsAbs(filename) { |
| t.Errorf("filename is not absolute: %s", filename) |
| } |
| if _, err := os.Stat(filename); err != nil { |
| t.Errorf("stat error, %s: %v", filename, err) |
| } |
| } |
| |
| for _, test := range []struct { |
| pattern string |
| want string |
| }{ |
| // Import paths |
| {"a", "a.go"}, |
| {"b/vendor/a", "a.go"}, |
| {"b", "b.go"}, |
| {"c", "c.go"}, |
| {"subdir/d", "d.go"}, |
| {"subdir/e", "d.go"}, |
| {"e", "e.go e2.go"}, |
| {"f", "f.go f.s"}, |
| // Relative paths |
| {"./a", "a.go"}, |
| {"./b/vendor/a", "a.go"}, |
| {"./b", "b.go"}, |
| {"./c", "c.go"}, |
| {"./subdir/d", "d.go"}, |
| {"./subdir/e", "d.go"}, |
| {"./e", "e.go e2.go"}, |
| {"./f", "f.go f.s"}, |
| } { |
| cfg := &packages.Config{ |
| Mode: packages.LoadFiles, |
| Dir: filepath.Join(tmp, "src"), |
| Env: append(os.Environ(), "GOPATH="+tmp), |
| } |
| pkgs, err := packages.Load(cfg, test.pattern) |
| if err != nil { |
| t.Errorf("pattern %s: %v", test.pattern, err) |
| continue |
| } |
| |
| if got := strings.Join(srcs(pkgs[0]), " "); got != test.want { |
| t.Errorf("in package %s, got %s, want %s", test.pattern, got, test.want) |
| } |
| |
| // Test that files in all packages exist and are absolute paths. |
| _, all := importGraph(pkgs) |
| for _, pkg := range all { |
| for _, filename := range pkg.GoFiles { |
| checkFile(filename) |
| } |
| for _, filename := range pkg.OtherFiles { |
| checkFile(filename) |
| } |
| } |
| } |
| } |
| |
| func TestContains(t *testing.T) { |
| tmp, cleanup := makeTree(t, map[string]string{ |
| "src/a/a.go": `package a; import "b"`, |
| "src/b/b.go": `package b; import "c"`, |
| "src/c/c.go": `package c`, |
| }) |
| defer cleanup() |
| |
| cfg := &packages.Config{ |
| Mode: packages.LoadImports, |
| Dir: tmp, |
| Env: append(os.Environ(), "GOPATH="+tmp), |
| } |
| initial, err := packages.Load(cfg, "contains:src/b/b.go") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| graph, _ := importGraph(initial) |
| wantGraph := ` |
| * b |
| c |
| b -> c |
| `[1:] |
| if graph != wantGraph { |
| t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantGraph) |
| } |
| } |
| |
| // TestContains_FallbackSticks ensures that when there are both contains and non-contains queries |
| // the decision whether to fallback to the pre-1.11 go list sticks across both sets of calls to |
| // go list. |
| func TestContains_FallbackSticks(t *testing.T) { |
| tmp, cleanup := makeTree(t, map[string]string{ |
| "src/a/a.go": `package a; import "b"`, |
| "src/b/b.go": `package b; import "c"`, |
| "src/c/c.go": `package c`, |
| }) |
| defer cleanup() |
| |
| cfg := &packages.Config{ |
| Mode: packages.LoadImports, |
| Dir: tmp, |
| Env: append(os.Environ(), "GOPATH="+tmp), |
| } |
| initial, err := packages.Load(cfg, "a", "contains:src/b/b.go") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| graph, _ := importGraph(initial) |
| wantGraph := ` |
| * a |
| * b |
| c |
| a -> b |
| b -> c |
| `[1:] |
| if graph != wantGraph { |
| t.Errorf("wrong import graph: got <<%s>>, want <<%s>>", graph, wantGraph) |
| } |
| } |
| |
| func TestJSON(t *testing.T) { |
| //TODO: add in some errors |
| tmp, cleanup := makeTree(t, map[string]string{ |
| "src/a/a.go": `package a; const A = 1`, |
| "src/b/b.go": `package b; import "a"; var B = a.A`, |
| "src/c/c.go": `package c; import "b" ; var C = b.B`, |
| "src/d/d.go": `package d; import "b" ; var D = b.B`, |
| }) |
| defer cleanup() |
| |
| cfg := &packages.Config{ |
| Mode: packages.LoadImports, |
| Env: append(os.Environ(), "GOPATH="+tmp), |
| } |
| initial, err := packages.Load(cfg, "c", "d") |
| if err != nil { |
| t.Fatal(err) |
| } |
| buf := &bytes.Buffer{} |
| enc := json.NewEncoder(buf) |
| enc.SetIndent("", "\t") |
| seen := make(map[string]bool) |
| var visit func(*packages.Package) |
| visit = func(pkg *packages.Package) { |
| if seen[pkg.ID] { |
| return |
| } |
| seen[pkg.ID] = true |
| // trim the source lists for stable results |
| pkg.GoFiles = cleanPaths(pkg.GoFiles) |
| pkg.CompiledGoFiles = cleanPaths(pkg.CompiledGoFiles) |
| pkg.OtherFiles = cleanPaths(pkg.OtherFiles) |
| for _, ipkg := range pkg.Imports { |
| visit(ipkg) |
| } |
| if err := enc.Encode(pkg); err != nil { |
| t.Fatal(err) |
| } |
| } |
| for _, pkg := range initial { |
| visit(pkg) |
| } |
| wantJSON := ` |
| { |
| "ID": "a", |
| "Name": "a", |
| "PkgPath": "a", |
| "GoFiles": [ |
| "a.go" |
| ], |
| "CompiledGoFiles": [ |
| "a.go" |
| ] |
| } |
| { |
| "ID": "b", |
| "Name": "b", |
| "PkgPath": "b", |
| "GoFiles": [ |
| "b.go" |
| ], |
| "CompiledGoFiles": [ |
| "b.go" |
| ], |
| "Imports": { |
| "a": "a" |
| } |
| } |
| { |
| "ID": "c", |
| "Name": "c", |
| "PkgPath": "c", |
| "GoFiles": [ |
| "c.go" |
| ], |
| "CompiledGoFiles": [ |
| "c.go" |
| ], |
| "Imports": { |
| "b": "b" |
| } |
| } |
| { |
| "ID": "d", |
| "Name": "d", |
| "PkgPath": "d", |
| "GoFiles": [ |
| "d.go" |
| ], |
| "CompiledGoFiles": [ |
| "d.go" |
| ], |
| "Imports": { |
| "b": "b" |
| } |
| } |
| `[1:] |
| |
| if buf.String() != wantJSON { |
| t.Errorf("wrong JSON: got <<%s>>, want <<%s>>", buf.String(), wantJSON) |
| } |
| // now decode it again |
| var decoded []*packages.Package |
| dec := json.NewDecoder(buf) |
| for dec.More() { |
| p := new(packages.Package) |
| if err := dec.Decode(p); err != nil { |
| t.Fatal(err) |
| } |
| decoded = append(decoded, p) |
| } |
| if len(decoded) != 4 { |
| t.Fatalf("got %d packages, want 4", len(decoded)) |
| } |
| for i, want := range []*packages.Package{{ |
| ID: "a", |
| Name: "a", |
| }, { |
| ID: "b", |
| Name: "b", |
| Imports: map[string]*packages.Package{ |
| "a": &packages.Package{ID: "a"}, |
| }, |
| }, { |
| ID: "c", |
| Name: "c", |
| Imports: map[string]*packages.Package{ |
| "b": &packages.Package{ID: "b"}, |
| }, |
| }, { |
| ID: "d", |
| Name: "d", |
| Imports: map[string]*packages.Package{ |
| "b": &packages.Package{ID: "b"}, |
| }, |
| }} { |
| got := decoded[i] |
| if got.ID != want.ID { |
| t.Errorf("Package %d has ID %q want %q", i, got.ID, want.ID) |
| } |
| if got.Name != want.Name { |
| t.Errorf("Package %q has Name %q want %q", got.ID, got.Name, want.Name) |
| } |
| if len(got.Imports) != len(want.Imports) { |
| t.Errorf("Package %q has %d imports want %d", got.ID, len(got.Imports), len(want.Imports)) |
| continue |
| } |
| for path, ipkg := range got.Imports { |
| if want.Imports[path] == nil { |
| t.Errorf("Package %q has unexpected import %q", got.ID, path) |
| continue |
| } |
| if want.Imports[path].ID != ipkg.ID { |
| t.Errorf("Package %q import %q is %q want %q", got.ID, path, ipkg.ID, want.Imports[path].ID) |
| } |
| } |
| } |
| } |
| |
| func errorMessages(errors []error) []string { |
| var msgs []string |
| for _, err := range errors { |
| msg := err.Error() |
| // Strip off /tmp filename. |
| if i := strings.Index(msg, ": "); i >= 0 { |
| msg = msg[i+len(": "):] |
| } |
| msgs = append(msgs, msg) |
| } |
| sort.Strings(msgs) |
| return msgs |
| } |
| |
| func srcs(p *packages.Package) []string { |
| return cleanPaths(append(p.GoFiles, p.OtherFiles...)) |
| } |
| |
| // cleanPaths attempts to reduce path names to stable forms |
| func cleanPaths(paths []string) []string { |
| result := make([]string, len(paths)) |
| for i, src := range paths { |
| // The default location for cache data is a subdirectory named go-build |
| // in the standard user cache directory for the current operating system. |
| if strings.Contains(filepath.ToSlash(src), "/go-build/") { |
| result[i] = fmt.Sprintf("%d.go", i) // make cache names predictable |
| } else { |
| result[i] = filepath.Base(src) |
| } |
| } |
| return result |
| } |
| |
| // importGraph returns the import graph as a user-friendly string, |
| // and a map containing all packages keyed by ID. |
| func importGraph(initial []*packages.Package) (string, map[string]*packages.Package) { |
| out := new(bytes.Buffer) |
| |
| initialSet := make(map[*packages.Package]bool) |
| for _, p := range initial { |
| initialSet[p] = true |
| } |
| |
| // We can't use packages.All because |
| // we need to prune the traversal. |
| var nodes, edges []string |
| res := make(map[string]*packages.Package) |
| seen := make(map[*packages.Package]bool) |
| var visit func(p *packages.Package) |
| visit = func(p *packages.Package) { |
| if !seen[p] { |
| seen[p] = true |
| if res[p.ID] != nil { |
| panic("duplicate ID: " + p.ID) |
| } |
| res[p.ID] = p |
| |
| star := ' ' // mark initial packages with a star |
| if initialSet[p] { |
| star = '*' |
| } |
| nodes = append(nodes, fmt.Sprintf("%c %s", star, p.ID)) |
| |
| // To avoid a lot of noise, |
| // we prune uninteresting dependencies of testmain packages, |
| // which we identify by this import: |
| isTestMain := p.Imports["testing/internal/testdeps"] != nil |
| |
| for _, imp := range p.Imports { |
| if isTestMain { |
| switch imp.ID { |
| case "os", "testing", "testing/internal/testdeps": |
| edges = append(edges, fmt.Sprintf("%s -> %s (pruned)", p, imp)) |
| continue |
| } |
| } |
| edges = append(edges, fmt.Sprintf("%s -> %s", p, imp)) |
| visit(imp) |
| } |
| } |
| } |
| for _, p := range initial { |
| visit(p) |
| } |
| |
| // Sort, ignoring leading optional star prefix. |
| sort.Slice(nodes, func(i, j int) bool { return nodes[i][2:] < nodes[j][2:] }) |
| for _, node := range nodes { |
| fmt.Fprintf(out, "%s\n", node) |
| } |
| |
| sort.Strings(edges) |
| for _, edge := range edges { |
| fmt.Fprintf(out, " %s\n", edge) |
| } |
| |
| return out.String(), res |
| } |
| |
| const skipCleanup = false // for debugging; don't commit 'true'! |
| |
| // makeTree creates a new temporary directory containing the specified |
| // file tree, and chdirs to it. Call the cleanup function to restore the |
| // cwd and delete the tree. |
| func makeTree(t *testing.T, tree map[string]string) (dir string, cleanup func()) { |
| dir, err := ioutil.TempDir("", "") |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| cleanup = func() { |
| if skipCleanup { |
| t.Logf("Skipping cleanup of temp dir: %s", dir) |
| return |
| } |
| os.RemoveAll(dir) // ignore errors |
| } |
| |
| for name, content := range tree { |
| name := filepath.Join(dir, name) |
| if err := os.MkdirAll(filepath.Dir(name), 0777); err != nil { |
| cleanup() |
| t.Fatal(err) |
| } |
| if err := ioutil.WriteFile(name, []byte(content), 0666); err != nil { |
| cleanup() |
| t.Fatal(err) |
| } |
| } |
| return dir, cleanup |
| } |
| |
| func constant(p *packages.Package, name string) *types.Const { |
| return p.Types.Scope().Lookup(name).(*types.Const) |
| } |