// Copyright 2013 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. // No testdata on Android. // +build !android package loader_test import ( "fmt" "go/build" "go/constant" "go/types" "os" "path/filepath" "reflect" "runtime" "sort" "strings" "sync" "testing" "golang.org/x/tools/go/buildutil" "golang.org/x/tools/go/loader" "golang.org/x/tools/internal/testenv" ) func TestMain(m *testing.M) { testenv.ExitIfSmallMachine() os.Exit(m.Run()) } // TestFromArgs checks that conf.FromArgs populates conf correctly. // It does no I/O. func TestFromArgs(t *testing.T) { type result struct { Err string Rest []string ImportPkgs map[string]bool CreatePkgs []loader.PkgSpec } for _, test := range []struct { args []string tests bool want result }{ // Mix of existing and non-existent packages. { args: []string{"nosuchpkg", "errors"}, want: result{ ImportPkgs: map[string]bool{"errors": false, "nosuchpkg": false}, }, }, // Same, with -test flag. { args: []string{"nosuchpkg", "errors"}, tests: true, want: result{ ImportPkgs: map[string]bool{"errors": true, "nosuchpkg": true}, }, }, // Surplus arguments. { args: []string{"fmt", "errors", "--", "surplus"}, want: result{ Rest: []string{"surplus"}, ImportPkgs: map[string]bool{"errors": false, "fmt": false}, }, }, // Ad hoc package specified as *.go files. { args: []string{"foo.go", "bar.go"}, want: result{CreatePkgs: []loader.PkgSpec{{ Filenames: []string{"foo.go", "bar.go"}, }}}, }, // Mixture of *.go and import paths. { args: []string{"foo.go", "fmt"}, want: result{ Err: "named files must be .go files: fmt", }, }, } { var conf loader.Config rest, err := conf.FromArgs(test.args, test.tests) got := result{ Rest: rest, ImportPkgs: conf.ImportPkgs, CreatePkgs: conf.CreatePkgs, } if err != nil { got.Err = err.Error() } if !reflect.DeepEqual(got, test.want) { t.Errorf("FromArgs(%q) = %+v, want %+v", test.args, got, test.want) } } } func TestLoad_NoInitialPackages(t *testing.T) { var conf loader.Config const wantErr = "no initial packages were loaded" prog, err := conf.Load() if err == nil { t.Errorf("Load succeeded unexpectedly, want %q", wantErr) } else if err.Error() != wantErr { t.Errorf("Load failed with wrong error %q, want %q", err, wantErr) } if prog != nil { t.Errorf("Load unexpectedly returned a Program") } } func TestLoad_MissingInitialPackage(t *testing.T) { var conf loader.Config conf.Import("nosuchpkg") conf.Import("errors") const wantErr = "couldn't load packages due to errors: nosuchpkg" prog, err := conf.Load() if err == nil { t.Errorf("Load succeeded unexpectedly, want %q", wantErr) } else if err.Error() != wantErr { t.Errorf("Load failed with wrong error %q, want %q", err, wantErr) } if prog != nil { t.Errorf("Load unexpectedly returned a Program") } } func TestLoad_MissingInitialPackage_AllowErrors(t *testing.T) { if runtime.Compiler == "gccgo" { t.Skip("gccgo has no standard library test files") } var conf loader.Config conf.AllowErrors = true conf.Import("nosuchpkg") conf.ImportWithTests("errors") prog, err := conf.Load() if err != nil { t.Errorf("Load failed unexpectedly: %v", err) } if prog == nil { t.Fatalf("Load returned a nil Program") } if got, want := created(prog), "errors_test"; got != want { t.Errorf("Created = %s, want %s", got, want) } if got, want := imported(prog), "errors"; got != want { t.Errorf("Imported = %s, want %s", got, want) } } func TestCreateUnnamedPackage(t *testing.T) { var conf loader.Config conf.CreateFromFilenames("") prog, err := conf.Load() if err != nil { t.Fatalf("Load failed: %v", err) } if got, want := fmt.Sprint(prog.InitialPackages()), "[(unnamed)]"; got != want { t.Errorf("InitialPackages = %s, want %s", got, want) } } func TestLoad_MissingFileInCreatedPackage(t *testing.T) { var conf loader.Config conf.CreateFromFilenames("", "missing.go") const wantErr = "couldn't load packages due to errors: (unnamed)" prog, err := conf.Load() if prog != nil { t.Errorf("Load unexpectedly returned a Program") } if err == nil { t.Fatalf("Load succeeded unexpectedly, want %q", wantErr) } if err.Error() != wantErr { t.Fatalf("Load failed with wrong error %q, want %q", err, wantErr) } } func TestLoad_MissingFileInCreatedPackage_AllowErrors(t *testing.T) { conf := loader.Config{AllowErrors: true} conf.CreateFromFilenames("", "missing.go") prog, err := conf.Load() if err != nil { t.Errorf("Load failed: %v", err) } if got, want := fmt.Sprint(prog.InitialPackages()), "[(unnamed)]"; got != want { t.Fatalf("InitialPackages = %s, want %s", got, want) } } func TestLoad_ParseError(t *testing.T) { var conf loader.Config conf.CreateFromFilenames("badpkg", "testdata/badpkgdecl.go") const wantErr = "couldn't load packages due to errors: badpkg" prog, err := conf.Load() if prog != nil { t.Errorf("Load unexpectedly returned a Program") } if err == nil { t.Fatalf("Load succeeded unexpectedly, want %q", wantErr) } if err.Error() != wantErr { t.Fatalf("Load failed with wrong error %q, want %q", err, wantErr) } } func TestLoad_ParseError_AllowErrors(t *testing.T) { var conf loader.Config conf.AllowErrors = true conf.CreateFromFilenames("badpkg", "testdata/badpkgdecl.go") prog, err := conf.Load() if err != nil { t.Errorf("Load failed unexpectedly: %v", err) } if prog == nil { t.Fatalf("Load returned a nil Program") } if got, want := created(prog), "badpkg"; got != want { t.Errorf("Created = %s, want %s", got, want) } badpkg := prog.Created[0] if len(badpkg.Files) != 1 { t.Errorf("badpkg has %d files, want 1", len(badpkg.Files)) } wantErr := filepath.Join("testdata", "badpkgdecl.go") + ":1:34: expected 'package', found 'EOF'" if !hasError(badpkg.Errors, wantErr) { t.Errorf("badpkg.Errors = %v, want %s", badpkg.Errors, wantErr) } } func TestLoad_FromSource_Success(t *testing.T) { var conf loader.Config conf.CreateFromFilenames("P", "testdata/a.go", "testdata/b.go") prog, err := conf.Load() if err != nil { t.Errorf("Load failed unexpectedly: %v", err) } if prog == nil { t.Fatalf("Load returned a nil Program") } if got, want := created(prog), "P"; got != want { t.Errorf("Created = %s, want %s", got, want) } } func TestLoad_FromImports_Success(t *testing.T) { if runtime.Compiler == "gccgo" { t.Skip("gccgo has no standard library test files") } var conf loader.Config conf.ImportWithTests("fmt") conf.ImportWithTests("errors") prog, err := conf.Load() if err != nil { t.Errorf("Load failed unexpectedly: %v", err) } if prog == nil { t.Fatalf("Load returned a nil Program") } if got, want := created(prog), "errors_test fmt_test"; got != want { t.Errorf("Created = %q, want %s", got, want) } if got, want := imported(prog), "errors fmt"; got != want { t.Errorf("Imported = %s, want %s", got, want) } // Check set of transitive packages. // There are >30 and the set may grow over time, so only check a few. want := map[string]bool{ "strings": true, "time": true, "runtime": true, "testing": true, "unicode": true, } for _, path := range all(prog) { delete(want, path) } if len(want) > 0 { t.Errorf("AllPackages is missing these keys: %q", keys(want)) } } func TestLoad_MissingIndirectImport(t *testing.T) { pkgs := map[string]string{ "a": `package a; import _ "b"`, "b": `package b; import _ "c"`, } conf := loader.Config{Build: fakeContext(pkgs)} conf.Import("a") const wantErr = "couldn't load packages due to errors: b" prog, err := conf.Load() if err == nil { t.Errorf("Load succeeded unexpectedly, want %q", wantErr) } else if err.Error() != wantErr { t.Errorf("Load failed with wrong error %q, want %q", err, wantErr) } if prog != nil { t.Errorf("Load unexpectedly returned a Program") } } func TestLoad_BadDependency_AllowErrors(t *testing.T) { for _, test := range []struct { descr string pkgs map[string]string wantPkgs string }{ { descr: "missing dependency", pkgs: map[string]string{ "a": `package a; import _ "b"`, "b": `package b; import _ "c"`, }, wantPkgs: "a b", }, { descr: "bad package decl in dependency", pkgs: map[string]string{ "a": `package a; import _ "b"`, "b": `package b; import _ "c"`, "c": `package`, }, wantPkgs: "a b", }, { descr: "parse error in dependency", pkgs: map[string]string{ "a": `package a; import _ "b"`, "b": `package b; import _ "c"`, "c": `package c; var x = `, }, wantPkgs: "a b c", }, } { conf := loader.Config{ AllowErrors: true, Build: fakeContext(test.pkgs), } conf.Import("a") prog, err := conf.Load() if err != nil { t.Errorf("%s: Load failed unexpectedly: %v", test.descr, err) } if prog == nil { t.Fatalf("%s: Load returned a nil Program", test.descr) } if got, want := imported(prog), "a"; got != want { t.Errorf("%s: Imported = %s, want %s", test.descr, got, want) } if got := all(prog); strings.Join(got, " ") != test.wantPkgs { t.Errorf("%s: AllPackages = %s, want %s", test.descr, got, test.wantPkgs) } } } func TestCwd(t *testing.T) { ctxt := fakeContext(map[string]string{"one/two/three": `package three`}) for _, test := range []struct { cwd, arg, want string }{ {cwd: "/go/src/one", arg: "./two/three", want: "one/two/three"}, {cwd: "/go/src/one", arg: "../one/two/three", want: "one/two/three"}, {cwd: "/go/src/one", arg: "one/two/three", want: "one/two/three"}, {cwd: "/go/src/one/two/three", arg: ".", want: "one/two/three"}, {cwd: "/go/src/one", arg: "two/three", want: ""}, } { conf := loader.Config{ Cwd: test.cwd, Build: ctxt, } conf.Import(test.arg) var got string prog, err := conf.Load() if prog != nil { got = imported(prog) } if got != test.want { t.Errorf("Load(%s) from %s: Imported = %s, want %s", test.arg, test.cwd, got, test.want) if err != nil { t.Errorf("Load failed: %v", err) } } } } func TestLoad_vendor(t *testing.T) { pkgs := map[string]string{ "a": `package a; import _ "x"`, "a/vendor": ``, // mkdir a/vendor "a/vendor/x": `package xa`, "b": `package b; import _ "x"`, "b/vendor": ``, // mkdir b/vendor "b/vendor/x": `package xb`, "c": `package c; import _ "x"`, "x": `package xc`, } conf := loader.Config{Build: fakeContext(pkgs)} conf.Import("a") conf.Import("b") conf.Import("c") prog, err := conf.Load() if err != nil { t.Fatal(err) } // Check that a, b, and c see different versions of x. for _, r := range "abc" { name := string(r) got := prog.Package(name).Pkg.Imports()[0] want := "x" + name if got.Name() != want { t.Errorf("package %s import %q = %s, want %s", name, "x", got.Name(), want) } } } func TestVendorCwd(t *testing.T) { // Test the interaction of cwd and vendor directories. ctxt := fakeContext(map[string]string{ "net": ``, // mkdir net "net/http": `package http; import _ "hpack"`, "vendor": ``, // mkdir vendor "vendor/hpack": `package vendorhpack`, "hpack": `package hpack`, }) for i, test := range []struct { cwd, arg, want string }{ {cwd: "/go/src/net", arg: "http"}, // not found {cwd: "/go/src/net", arg: "./http", want: "net/http vendor/hpack"}, {cwd: "/go/src/net", arg: "hpack", want: "vendor/hpack"}, {cwd: "/go/src/vendor", arg: "hpack", want: "vendor/hpack"}, {cwd: "/go/src/vendor", arg: "./hpack", want: "vendor/hpack"}, } { conf := loader.Config{ Cwd: test.cwd, Build: ctxt, } conf.Import(test.arg) var got string prog, err := conf.Load() if prog != nil { got = strings.Join(all(prog), " ") } if got != test.want { t.Errorf("#%d: Load(%s) from %s: got %s, want %s", i, test.arg, test.cwd, got, test.want) if err != nil { t.Errorf("Load failed: %v", err) } } } } func TestVendorCwdIssue16580(t *testing.T) { // Regression test for Go issue 16580. // Import decls in "created" packages were vendor-resolved // w.r.t. cwd, not the parent directory of the package's files. ctxt := fakeContext(map[string]string{ "a": ``, // mkdir a "a/vendor": ``, // mkdir a/vendor "a/vendor/b": `package b; const X = true`, "b": `package b; const X = false`, }) for _, test := range []struct { filename, cwd string want bool // expected value of b.X; depends on filename, not on cwd }{ {filename: "c.go", cwd: "/go/src", want: false}, {filename: "c.go", cwd: "/go/src/a", want: false}, {filename: "c.go", cwd: "/go/src/a/b", want: false}, {filename: "c.go", cwd: "/go/src/a/vendor/b", want: false}, {filename: "/go/src/a/c.go", cwd: "/go/src", want: true}, {filename: "/go/src/a/c.go", cwd: "/go/src/a", want: true}, {filename: "/go/src/a/c.go", cwd: "/go/src/a/b", want: true}, {filename: "/go/src/a/c.go", cwd: "/go/src/a/vendor/b", want: true}, {filename: "/go/src/c/c.go", cwd: "/go/src", want: false}, {filename: "/go/src/c/c.go", cwd: "/go/src/a", want: false}, {filename: "/go/src/c/c.go", cwd: "/go/src/a/b", want: false}, {filename: "/go/src/c/c.go", cwd: "/go/src/a/vendor/b", want: false}, } { conf := loader.Config{ Cwd: test.cwd, Build: ctxt, } f, err := conf.ParseFile(test.filename, `package dummy; import "b"; const X = b.X`) if err != nil { t.Fatal(f) } conf.CreateFromFiles("dummy", f) prog, err := conf.Load() if err != nil { t.Errorf("%+v: Load failed: %v", test, err) continue } x := constant.BoolVal(prog.Created[0].Pkg.Scope().Lookup("X").(*types.Const).Val()) if x != test.want { t.Errorf("%+v: b.X = %t", test, x) } } // TODO(adonovan): also test imports within XTestGoFiles. } // TODO(adonovan): more Load tests: // // failures: // - to parse package decl of *_test.go files // - to parse package decl of external *_test.go files // - to parse whole of *_test.go files // - to parse whole of external *_test.go files // - to open a *.go file during import scanning // - to import from binary // features: // - InitialPackages // - PackageCreated hook // - TypeCheckFuncBodies hook func TestTransitivelyErrorFreeFlag(t *testing.T) { // Create an minimal custom build.Context // that fakes the following packages: // // a --> b --> c! c has an error // \ d and e are transitively error-free. // e --> d // // Each package [a-e] consists of one file, x.go. pkgs := map[string]string{ "a": `package a; import (_ "b"; _ "e")`, "b": `package b; import _ "c"`, "c": `package c; func f() { _ = int(false) }`, // type error within function body "d": `package d;`, "e": `package e; import _ "d"`, } conf := loader.Config{ AllowErrors: true, Build: fakeContext(pkgs), } conf.Import("a") prog, err := conf.Load() if err != nil { t.Errorf("Load failed: %s", err) } if prog == nil { t.Fatalf("Load returned nil *Program") } for pkg, info := range prog.AllPackages { var wantErr, wantTEF bool switch pkg.Path() { case "a", "b": case "c": wantErr = true case "d", "e": wantTEF = true default: t.Errorf("unexpected package: %q", pkg.Path()) continue } if (info.Errors != nil) != wantErr { if wantErr { t.Errorf("Package %q.Error = nil, want error", pkg.Path()) } else { t.Errorf("Package %q has unexpected Errors: %v", pkg.Path(), info.Errors) } } if info.TransitivelyErrorFree != wantTEF { t.Errorf("Package %q.TransitivelyErrorFree=%t, want %t", pkg.Path(), info.TransitivelyErrorFree, wantTEF) } } } // Test that syntax (scan/parse), type, and loader errors are recorded // (in PackageInfo.Errors) and reported (via Config.TypeChecker.Error). func TestErrorReporting(t *testing.T) { pkgs := map[string]string{ "a": `package a; import (_ "b"; _ "c"); var x int = false`, "b": `package b; 'syntax error!`, } conf := loader.Config{ AllowErrors: true, Build: fakeContext(pkgs), } var mu sync.Mutex var allErrors []error conf.TypeChecker.Error = func(err error) { mu.Lock() allErrors = append(allErrors, err) mu.Unlock() } conf.Import("a") prog, err := conf.Load() if err != nil { t.Errorf("Load failed: %s", err) } if prog == nil { t.Fatalf("Load returned nil *Program") } // TODO(adonovan): test keys of ImportMap. // Check errors recorded in each PackageInfo. for pkg, info := range prog.AllPackages { switch pkg.Path() { case "a": // The match below is unfortunately vague, because in go1.16 the error // message in go/types changed from "cannot convert ..." to "cannot use // ... as ... in assignment". if !hasError(info.Errors, "cannot") { t.Errorf("a.Errors = %v, want bool assignment (type) error", info.Errors) } if !hasError(info.Errors, "could not import c") { t.Errorf("a.Errors = %v, want import (loader) error", info.Errors) } case "b": if !hasError(info.Errors, "rune literal not terminated") { t.Errorf("b.Errors = %v, want unterminated literal (syntax) error", info.Errors) } } } // Check errors reported via error handler. if !hasError(allErrors, "cannot") || !hasError(allErrors, "rune literal not terminated") || !hasError(allErrors, "could not import c") { t.Errorf("allErrors = %v, want syntax, type and loader errors", allErrors) } } func TestCycles(t *testing.T) { for _, test := range []struct { descr string ctxt *build.Context wantErr string }{ { "self-cycle", fakeContext(map[string]string{ "main": `package main; import _ "selfcycle"`, "selfcycle": `package selfcycle; import _ "selfcycle"`, }), `import cycle: selfcycle -> selfcycle`, }, { "three-package cycle", fakeContext(map[string]string{ "main": `package main; import _ "a"`, "a": `package a; import _ "b"`, "b": `package b; import _ "c"`, "c": `package c; import _ "a"`, }), `import cycle: c -> a -> b -> c`, }, { "self-cycle in dependency of test file", buildutil.FakeContext(map[string]map[string]string{ "main": { "main.go": `package main`, "main_test.go": `package main; import _ "a"`, }, "a": { "a.go": `package a; import _ "a"`, }, }), `import cycle: a -> a`, }, // TODO(adonovan): fix: these fail // { // "two-package cycle in dependency of test file", // buildutil.FakeContext(map[string]map[string]string{ // "main": { // "main.go": `package main`, // "main_test.go": `package main; import _ "a"`, // }, // "a": { // "a.go": `package a; import _ "main"`, // }, // }), // `import cycle: main -> a -> main`, // }, // { // "self-cycle in augmented package", // buildutil.FakeContext(map[string]map[string]string{ // "main": { // "main.go": `package main`, // "main_test.go": `package main; import _ "main"`, // }, // }), // `import cycle: main -> main`, // }, } { conf := loader.Config{ AllowErrors: true, Build: test.ctxt, } var mu sync.Mutex var allErrors []error conf.TypeChecker.Error = func(err error) { mu.Lock() allErrors = append(allErrors, err) mu.Unlock() } conf.ImportWithTests("main") prog, err := conf.Load() if err != nil { t.Errorf("%s: Load failed: %s", test.descr, err) } if prog == nil { t.Fatalf("%s: Load returned nil *Program", test.descr) } if !hasError(allErrors, test.wantErr) { t.Errorf("%s: Load() errors = %q, want %q", test.descr, allErrors, test.wantErr) } } // TODO(adonovan): // - Test that in a legal test cycle, none of the symbols // defined by augmentation are visible via import. } // ---- utilities ---- // Simplifying wrapper around buildutil.FakeContext for single-file packages. func fakeContext(pkgs map[string]string) *build.Context { pkgs2 := make(map[string]map[string]string) for path, content := range pkgs { pkgs2[path] = map[string]string{"x.go": content} } return buildutil.FakeContext(pkgs2) } func hasError(errors []error, substr string) bool { for _, err := range errors { if strings.Contains(err.Error(), substr) { return true } } return false } func keys(m map[string]bool) (keys []string) { for key := range m { keys = append(keys, key) } sort.Strings(keys) return } // Returns all loaded packages. func all(prog *loader.Program) []string { var pkgs []string for _, info := range prog.AllPackages { pkgs = append(pkgs, info.Pkg.Path()) } sort.Strings(pkgs) return pkgs } // Returns initially imported packages, as a string. func imported(prog *loader.Program) string { var pkgs []string for _, info := range prog.Imported { pkgs = append(pkgs, info.Pkg.Path()) } sort.Strings(pkgs) return strings.Join(pkgs, " ") } // Returns initially created packages, as a string. func created(prog *loader.Program) string { var pkgs []string for _, info := range prog.Created { pkgs = append(pkgs, info.Pkg.Path()) } return strings.Join(pkgs, " ") } // Load package "io" twice in parallel. // When run with -race, this is a regression test for Go issue 20718, in // which the global "unsafe" package was modified concurrently. func TestLoad1(t *testing.T) { loadIO(t) } func TestLoad2(t *testing.T) { loadIO(t) } func loadIO(t *testing.T) { t.Parallel() conf := &loader.Config{ImportPkgs: map[string]bool{"io": false}} if _, err := conf.Load(); err != nil { t.Fatal(err) } }