// Package packagestest is an in-memory Go package loader for tests. It type-checks // source provided as a map of path → content, resolving imports against the // in-memory module and a small synthetic subset of the standard library (see // fakeStd), never GOROOT, a subprocess, or the disk. That keeps the fast unit // tests (table-driven, golden, compile-check, fuzz, determinism) hermetic: they // pass even when the test binary is built with -trimpath, which strips the // baked-in GOROOT that the default disk-backed importer would otherwise need. // // File paths encode the package: a file at "example.com/app/x.go" belongs to // the package whose import path is "example.com/app". The package name is taken // from the package clause.
package packagestest import ( ) // fakeStd is the standard-library subset the corpus and plumb's generated output // import, given as synthetic source. Resolving stdlib here rather than through // go/importer keeps loads self-contained (no GOROOT, no disk), so the tests // type-check under -trimpath. The declared surface is exactly what the fixtures // and generated code reference: a wider import fails to resolve like any missing // package, which points at the fixture that needs a new entry added here. var fakeStd = map[string]string{ "database/sql": "package sql\n\ntype DB struct{}\n", "errors": "package errors\n\nfunc Join(errs ...error) error { return nil }\n", "fmt": "package fmt\n\nfunc Sprint(a ...any) string { return \"\" }\n", "io": "package io\n\ntype Reader interface {\n\tRead(p []byte) (n int, err error)\n}\n", "log": "package log\n\ntype Logger struct{}\n", "net/url": "package url\n\ntype URL struct{}\n", // os.File carries a Read method so provider_kinds' assertion that *os.File // satisfies io.Reader type-checks as it does against the real standard library. "os": "package os\n\n" + "type File struct{}\n\n" + "func (f *File) Read(p []byte) (n int, err error) { return 0, nil }\n\n" + "var Stdin *File\n", "strings": "package strings\n\ntype Replacer struct{}\n", } // ErrImportCycle is returned by Load when type-checking hits an import cycle. It // is a sentinel so callers classify the failure with errors.Is rather than // matching message text. (The type-checker also reports the cycle as a per-import // type error, but that loses the error chain.) var ErrImportCycle = errors.New("import cycle") // Loaded is the result of loading an in-memory module. type Loaded struct { Packages []*discover.Package // TypeErrors are non-fatal type-checking errors (e.g. a stale generated // file). Parse errors are returned as the error result instead. TypeErrors []error } // ShuffleFunc reorders a sequence of n elements by swapping pairs. It has the // signature of math/rand/v2's (*Rand).Shuffle, so a *Rand's Shuffle method can // be passed directly. type ShuffleFunc func(n int, swap func(i, j int)) // Load parses and type-checks the given files, grouped into packages by their // directory. The companion LoadShuffled additionally reorders the files, the // load order, and the returned package slice to exercise determinism. func ( map[string]string) (*Loaded, error) { return LoadShuffled(, nil) } // LoadShuffled is Load with an optional reordering of files within packages and // of the packages themselves, used by the determinism test. When shuffle is nil // the deterministic sorted order is used as-is. func ( map[string]string, ShuffleFunc) (*Loaded, error) { := token.NewFileSet() // Group file paths by package import path (their directory). := map[string][]string{} for := range { := path.Dir() [] = append([], ) } := make([]string, 0, len()) for := range { = append(, ) } slices.Sort() applyShuffle(, ) for , := range { slices.Sort() applyShuffle(, ) } // Parse every file. := map[string][]*ast.File{} // import path → files := map[string]string{} for , := range { for , := range [] { const = parser.ParseComments | parser.SkipObjectResolution , := parser.ParseFile(, , [], ) if != nil { return nil, fmt.Errorf("parse %s: %w", , ) } [] = append([], ) [] = .Name.Name } } // Seed the synthetic standard library. These packages resolve on demand // through recChecker.Import, exactly like an intra-module import, but are not // part of the loaded module: they are excluded from pkgPaths, so they are // neither type-checked eagerly nor returned in l.Packages. Seeded after the // module files so module source positions are unaffected by this map's order. for , := range fakeStd { , := parser.ParseFile(, +"/std.go", , parser.SkipObjectResolution) if != nil { panic(fmt.Sprintf("packagestest: fakeStd[%q] does not parse: %v", , )) } [] = []*ast.File{} [] = .Name.Name } := &Loaded{} := &recChecker{ fset: , parsed: , names: , cache: map[string]*types.Package{}, infos: map[string]*types.Info{}, typeErr: &.TypeErrors, } for , := range { , , := .check() if != nil { return nil, } .Packages = append(.Packages, &discover.Package{ PkgPath: , Name: [], Fset: , Syntax: [], Types: , Info: , }) } // An import cycle is a structural load failure (the type-checker also records // it as a per-import type error, but that string loses the chain). Surface it // as the sentinel-wrapped error so callers classify with errors.Is. if .cycleErr != nil { return nil, .cycleErr } slices.SortFunc(.Packages, func(, *discover.Package) int { return strings.Compare(.PkgPath, .PkgPath) }) applyShuffle(.Packages, ) return , nil } // recChecker type-checks the in-memory packages, resolving imports (both // intra-module packages and the synthetic standard library) recursively from // its parsed set. An import outside that set is an unknown package. type recChecker struct { fset *token.FileSet parsed map[string][]*ast.File names map[string]string cache map[string]*types.Package infos map[string]*types.Info inProg map[string]bool typeErr *[]error cycleErr error // first import cycle seen, wrapping ErrImportCycle } func ( *recChecker) ( string) (*types.Package, error) { if , := .parsed[]; { , , := .check() return , } return nil, fmt.Errorf("cannot find package %q", ) } func ( *recChecker) ( error) { *.typeErr = append(*.typeErr, ) } func ( *recChecker) ( string) (*types.Package, *types.Info, error) { if , := .cache[]; { return , .infos[], nil } if .inProg == nil { .inProg = map[string]bool{} } if .inProg[] { if .cycleErr == nil { .cycleErr = fmt.Errorf("import cycle through %s: %w", , ErrImportCycle) } return nil, nil, .cycleErr } .inProg[] = true defer delete(.inProg, ) := &types.Info{ Types: map[ast.Expr]types.TypeAndValue{}, Defs: map[*ast.Ident]types.Object{}, Uses: map[*ast.Ident]types.Object{}, Selections: map[*ast.SelectorExpr]*types.Selection{}, Instances: map[*ast.Ident]types.Instance{}, Implicits: map[ast.Node]types.Object{}, Scopes: map[ast.Node]*types.Scope{}, } := types.Config{ Importer: , Error: .appendError, } , := .Check(, .fset, .parsed[], ) .cache[] = .infos[] = return , , nil } // applyShuffle reorders s in place using the given shuffle function. A nil // shuffle leaves s untouched. func [ ~[], any]( , ShuffleFunc) { if != nil { (len(), func(, int) { [], [] = [], [] }) } }