319 lines
14 KiB
Go
319 lines
14 KiB
Go
package builder
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// A Go program's build source is its import closure (novox/hq ADR 0267 rule 1): never narrower than what
|
|
// `go build` reads, whatever the system, the tags, the vendoring or the embeds.
|
|
|
|
// aGoTree writes files under a fresh directory and returns it.
|
|
func aGoTree(t *testing.T, files map[string]string) string {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
for name, body := range files {
|
|
full := filepath.Join(root, filepath.FromSlash(name))
|
|
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(full, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
return root
|
|
}
|
|
|
|
// aProgram is a module whose program imports its own packages, a vendored module, a local replacement
|
|
// that is vendored too, an embed and a package only one system builds; beside them, a package nothing
|
|
// imports and one only a test imports.
|
|
var aProgram = map[string]string{
|
|
"go.mod": "module example.com/fix\n\ngo 1.22\n\nrequire (\n\texample.org/dep v1.0.0\n\texample.net/local v0.0.0\n)\n\n" +
|
|
"replace example.net/local => ./third_party/local\n",
|
|
"go.sum": "",
|
|
"vendor/modules.txt": "# example.net/local v0.0.0 => ./third_party/local\n## explicit; go 1.22\nexample.net/local/pkg\n" +
|
|
"# example.org/dep v1.0.0\n## explicit; go 1.22\nexample.org/dep\nexample.org/dep/sub\n# example.net/local => ./third_party/local\n",
|
|
"vendor/example.org/dep/dep.go": "package dep\n\nimport _ \"example.org/dep/sub\"\n",
|
|
"vendor/example.org/dep/sub/sub.go": "package sub\n",
|
|
"vendor/example.org/other/other.go": "package other\n",
|
|
"vendor/example.net/local/pkg/p.go": "package pkg\n",
|
|
"third_party/local/go.mod": "module example.net/local\n\ngo 1.22\n",
|
|
"third_party/local/pkg/p.go": "package pkg\n",
|
|
"cmd/prog/main.go": "package main\n\nimport (\n\t\"fmt\"\n\n\t_ \"example.com/fix/emb\"\n\t\"example.com/fix/lib\"\n" +
|
|
"\t_ \"example.net/local/pkg\"\n\t_ \"example.org/dep\"\n)\n\nfunc main() { fmt.Println(lib.X) }\n",
|
|
"lib/lib.go": "package lib\n\nconst X = 1\n",
|
|
"lib/lib_plan9.go": "//go:build plan9\n\npackage lib\n\nimport _ \"example.com/fix/plan9only\"\n",
|
|
"lib/lib_test.go": "package lib\n\nimport _ \"example.com/fix/testonly\"\n",
|
|
"emb/emb_amd64.s": "",
|
|
"lib/sub/sub.go": "package sub\n",
|
|
"plan9only/p.go": "package plan9only\n",
|
|
"testonly/t.go": "package testonly\n",
|
|
"unrelated/u.go": "package unrelated\n",
|
|
"emb/emb.go": "package emb\n\nimport \"embed\"\n\n//go:embed static/*\nvar Static embed.FS\n\n" +
|
|
"//go:embed \"a b.txt\"\nvar Text string\n",
|
|
"emb/static/index.html": "x",
|
|
"emb/static/deep/style.css": "x",
|
|
"emb/a b.txt": "x",
|
|
"README.md": "x",
|
|
}
|
|
|
|
func TestAGoProgramsBuildSourceIsItsImportClosure(t *testing.T) {
|
|
root := aGoTree(t, aProgram)
|
|
got, err := GoBuildSource(root, "cmd/prog")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, c := range []struct {
|
|
file string
|
|
held bool
|
|
why string
|
|
}{
|
|
{"cmd/prog/main.go", true, "the program itself"},
|
|
{"cmd/prog/helper.go", true, "a file added to the program's package"},
|
|
{"lib/lib.go", true, "a package it imports"},
|
|
{"emb/emb_amd64.s", true, "assembly beside a package's Go"},
|
|
{"lib/lib_plan9.go", true, "a file one system builds"},
|
|
{"plan9only/p.go", true, "what a file one system builds imports"},
|
|
{"emb/static/index.html", true, "an embedded file"},
|
|
{"emb/static/deep/style.css", true, "an embedded file below the pattern's directory"},
|
|
{"emb/a b.txt", true, "an embed named outright, quoted"},
|
|
{"vendor/example.org/dep/dep.go", true, "a vendored package it imports"},
|
|
{"vendor/example.org/dep/sub/sub.go", true, "what a vendored package imports"},
|
|
{"vendor/example.net/local/pkg/p.go", true, "a replaced module, as vendored"},
|
|
{"third_party/local/pkg/p.go", true, "a replaced module, at its directory"},
|
|
{"third_party/local/go.mod", true, "a replaced module's requirements"},
|
|
{"go.mod", true, "the module's requirements"},
|
|
{"go.sum", true, "the module's sums"},
|
|
{"vendor/modules.txt", true, "the vendored modules"},
|
|
{"go.work", true, "a workspace, were one made"},
|
|
{"lib/lib_test.go", false, "a test is not built"},
|
|
{"testonly/t.go", false, "a package only a test imports"},
|
|
{"lib/sub/sub.go", false, "a package below an imported one, not imported"},
|
|
{"unrelated/u.go", false, "a package nothing imports"},
|
|
{"vendor/example.org/other/other.go", false, "a vendored package nothing imports"},
|
|
{"README.md", false, "a file no build reads"},
|
|
} {
|
|
if SourceHolds(got, c.file) != c.held {
|
|
t.Errorf("%s (%s): held %v, wanted %v\n %v", c.file, c.why, !c.held, c.held, got)
|
|
}
|
|
}
|
|
|
|
// **Never narrower than go list -deps**: every package go names, and every file it compiles or embeds,
|
|
// is held. Where no go command is at hand, said, not passed.
|
|
goCmd, err := exec.LookPath("go")
|
|
if err != nil {
|
|
t.Skip("NOT COMPARED: no go command to list the closure with")
|
|
}
|
|
list := exec.Command(goCmd, "list", "-deps", "-f",
|
|
`{{if not .Standard}}{{$d := .Dir}}{{range .GoFiles}}{{$d}}/{{.}}
|
|
{{end}}{{range .SFiles}}{{$d}}/{{.}}
|
|
{{end}}{{range .EmbedFiles}}{{$d}}/{{.}}
|
|
{{end}}{{end}}`, "./cmd/prog")
|
|
list.Dir = root
|
|
list.Env = append(os.Environ(), "GOFLAGS=-mod=vendor", "GOPROXY=off", "GOWORK=off", "GOOS=linux", "GOARCH=amd64")
|
|
out, err := list.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("go list: %v\n%s", err, out)
|
|
}
|
|
real, _ := filepath.EvalSymlinks(root)
|
|
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
rel, err := filepath.Rel(real, line)
|
|
if err != nil || strings.HasPrefix(rel, "..") {
|
|
rel, _ = filepath.Rel(root, line)
|
|
}
|
|
if !SourceHolds(got, filepath.ToSlash(rel)) {
|
|
t.Errorf("go list builds %s, and the build source does not hold it", rel)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A file added to the program's import closure moves its build source with it: the closure read again
|
|
// grows by the package.
|
|
func TestTheBuildSourceGrowsWithAnImport(t *testing.T) {
|
|
files := map[string]string{}
|
|
for k, v := range aProgram {
|
|
files[k] = v
|
|
}
|
|
before, err := GoBuildSource(aGoTree(t, files), "cmd/prog")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if SourceHolds(before, "unrelated/u.go") {
|
|
t.Fatal("held before it was imported")
|
|
}
|
|
files["cmd/prog/more.go"] = "package main\n\nimport _ \"example.com/fix/unrelated\"\n"
|
|
after, err := GoBuildSource(aGoTree(t, files), "cmd/prog")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !SourceHolds(after, "unrelated/u.go") {
|
|
t.Fatalf("an import added did not grow the build source: %v", after)
|
|
}
|
|
}
|
|
|
|
// What cannot be read is refused, so the build source is not narrowed and nothing is missed.
|
|
func TestABuildSourceThatCannotBeReadIsRefused(t *testing.T) {
|
|
for _, c := range []struct {
|
|
what string
|
|
files map[string]string
|
|
pkg string
|
|
says string
|
|
}{
|
|
{"no go.mod", map[string]string{"cmd/p/main.go": "package main\n"}, "cmd/p", "no go.mod"},
|
|
{"a workspace", map[string]string{"go.mod": "module x\n", "go.work": "go 1.22\n", "p/main.go": "package main\n"},
|
|
"p", "go.work"},
|
|
{"a local replacement outside the tree", map[string]string{
|
|
"go.mod": "module x\n\nreplace y => ../y\n", "p/main.go": "package main\n\nimport _ \"y\"\n"}, "p", "outside"},
|
|
{"a cgo header outside the directory", map[string]string{
|
|
"go.mod": "module x\n", "p/main.go": "package main\n\n// #include \"../h/h.h\"\nimport \"C\"\n"}, "p", "cgo"},
|
|
{"an assembly include above its directory", map[string]string{"go.mod": "module x\n", "p/main.go": "package main\n",
|
|
"p/a_amd64.s": "#include \"../h/textflag.h\"\n"}, "p", "outside"},
|
|
{"a file that does not parse", map[string]string{"go.mod": "module x\n", "p/main.go": "package main\n\nimport (\n"},
|
|
"p", "main.go"},
|
|
{"a package that leaves the tree", map[string]string{"go.mod": "module x\n"}, "../elsewhere", "leaves"},
|
|
} {
|
|
_, err := GoBuildSource(aGoTree(t, c.files), c.pkg)
|
|
if err == nil || !strings.Contains(err.Error(), c.says) {
|
|
t.Errorf("%s: %v, wanted a refusal saying %q", c.what, err, c.says)
|
|
}
|
|
}
|
|
// A cgo header in the package's own directory is held already, and is no refusal.
|
|
if _, err := GoBuildSource(aGoTree(t, map[string]string{"go.mod": "module x\n",
|
|
"p/main.go": "package main\n\n// #include \"h.h\"\nimport \"C\"\n"}), "p"); err != nil {
|
|
t.Errorf("a header in the package's directory was refused: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestABuildSourceHoldsWhatItsEntriesSay(t *testing.T) {
|
|
entries := []string{"./", "cmd/p/", "web/**", "go.mod"}
|
|
for file, want := range map[string]bool{
|
|
"main.go": true, "main_test.go": false, "cmd/p/x.go": true, "cmd/p/x_test.go": false, "cmd/p/sub/y.go": false,
|
|
"cmd/px/x.go": false, "web/a/b/c.css": true, "web": true, "webx/a": false, "go.mod": true, "docs/x.md": false,
|
|
"/cmd/p/x.go": true, "cmd/p/../q/x.go": false,
|
|
} {
|
|
if SourceHolds(entries, file) != want {
|
|
t.Errorf("%s: held %v, wanted %v", file, !want, want)
|
|
}
|
|
}
|
|
if !SourceHolds([]string{"**"}, "anything/at/all") {
|
|
t.Error("the whole tree does not hold a file")
|
|
}
|
|
}
|
|
|
|
// **A build handed its build source reads nothing else** (rule 3): the narrowed tree holds the source's
|
|
// files and no others — never .git — and its fingerprint changes with them and only with them.
|
|
func TestANarrowedTreeHoldsTheBuildSourceAndNothingElse(t *testing.T) {
|
|
files := map[string]string{}
|
|
for k, v := range aProgram {
|
|
files[k] = v
|
|
}
|
|
files[".git/HEAD"] = "ref: refs/heads/main\n"
|
|
src := aGoTree(t, files)
|
|
entries, err := GoBuildSource(src, "cmd/prog")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
dst := filepath.Join(t.TempDir(), "narrow")
|
|
one, err := narrowTree(src, dst, entries)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for file, want := range map[string]bool{"cmd/prog/main.go": true, "lib/lib.go": true, "emb/static/deep/style.css": true,
|
|
"lib/lib_test.go": false, "unrelated/u.go": false, "README.md": false, ".git/HEAD": false} {
|
|
_, err := os.Stat(filepath.Join(dst, filepath.FromSlash(file)))
|
|
if (err == nil) != want {
|
|
t.Errorf("%s: copied %v, wanted %v", file, err == nil, want)
|
|
}
|
|
}
|
|
// Outside the build source: one fingerprint.
|
|
files["README.md"] = "changed"
|
|
files["unrelated/u.go"] = "package unrelated\n\nconst Changed = 1\n"
|
|
again, err := narrowTree(aGoTree(t, files), filepath.Join(t.TempDir(), "n"), entries)
|
|
if err != nil || again != one {
|
|
t.Fatalf("a change outside the build source changed its fingerprint: %s %s %v", one, again, err)
|
|
}
|
|
// Inside it: another.
|
|
files["lib/lib.go"] = "package lib\n\nconst X = 2\n"
|
|
moved, err := narrowTree(aGoTree(t, files), filepath.Join(t.TempDir(), "n"), entries)
|
|
if err != nil || moved == one {
|
|
t.Fatalf("a change inside the build source kept its fingerprint: %s %v", moved, err)
|
|
}
|
|
}
|
|
|
|
// This repository's own programs: the route proxy's build source is not the controller's, and neither
|
|
// holds the other's command.
|
|
func TestThisRepositorysProgramsHaveBuildSourcesOfTheirOwn(t *testing.T) {
|
|
root := filepath.Join("..", "..")
|
|
proxy, err := GoBuildSource(root, "examples/route-proxy")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
controller, err := GoBuildSource(root, "cmd/mesh-controller")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
builder, err := GoBuildSource(root, "cmd/mesh-builder")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, c := range []struct {
|
|
entries []string
|
|
name string
|
|
file string
|
|
held bool
|
|
}{
|
|
{proxy, "the route proxy", "examples/route-proxy/main.go", true},
|
|
{proxy, "the route proxy", "internal/broker/broker.go", true},
|
|
{proxy, "the route proxy", "cmd/mesh-controller/main.go", false},
|
|
{proxy, "the route proxy", "internal/conditions/condition.go", false},
|
|
{proxy, "the route proxy", "README.md", false},
|
|
{controller, "the controller", "cmd/mesh-controller/main.go", true},
|
|
{controller, "the controller", "internal/conditions/condition.go", true},
|
|
{controller, "the controller", "internal/inventory/migrations/0001-nodes.sql", true},
|
|
{controller, "the controller", "examples/route-proxy/main.go", false},
|
|
{controller, "the controller", "cmd/mesh-builder/main.go", false},
|
|
{controller, "the controller", "README.md", false},
|
|
{builder, "the build seat's program", "cmd/mesh-builder/main.go", true},
|
|
{builder, "the build seat's program", "internal/conditions/condition.go", false},
|
|
{builder, "the build seat's program", "cmd/mesh-controller/main.go", false},
|
|
} {
|
|
if SourceHolds(c.entries, c.file) != c.held {
|
|
t.Errorf("%s: %s held %v, wanted %v", c.name, c.file, !c.held, c.held)
|
|
}
|
|
}
|
|
}
|
|
|
|
// C or assembly beside Go holds its package's directory whole — an include may name a file below it — and a
|
|
// go.mod made between the module's root and a package is a change.
|
|
func TestNativeSourcesHoldTheirDirectoryWhole(t *testing.T) {
|
|
got, err := GoBuildSource(aGoTree(t, map[string]string{"go.mod": "module x\n", "cmd/p/main.go": "package main\n\nimport _ \"x/lib/asm\"\n",
|
|
"lib/asm/a.go": "package asm\n", "lib/asm/a_amd64.s": "#include \"inc/textflag.h\"\n", "lib/asm/inc/textflag.h": ""}), "cmd/p")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for file, want := range map[string]bool{"lib/asm/inc/textflag.h": true, "lib/asm/a_amd64.s": true, "lib/go.mod": true,
|
|
"lib/asm/go.mod": true, "cmd/go.mod": true, "other/go.mod": false} {
|
|
if SourceHolds(got, file) != want {
|
|
t.Errorf("%s: held %v, wanted %v (%v)", file, !want, want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A cgo file may include from below its directory with nothing native beside it: its directory is held whole.
|
|
func TestACgoFileHoldsItsDirectoryWhole(t *testing.T) {
|
|
got, err := GoBuildSource(aGoTree(t, map[string]string{"go.mod": "module x\n",
|
|
"p/main.go": "package main\n\n// #include \"inc/x.h\"\nimport \"C\"\n", "p/inc/x.h": ""}), "p")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !SourceHolds(got, "p/inc/x.h") {
|
|
t.Fatalf("a header a cgo preamble includes is not held: %v", got)
|
|
}
|
|
}
|