A hand build of an older trunk commit said a closure lacking what was imported since, and the planner would have mapped the next merge onto it. C and assembly beside Go may include files below their directory, and a go.mod made above a package moves it out of its module: each is now held.
203 lines
9.1 KiB
Go
203 lines
9.1 KiB
Go
package builder
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// What a build says it was made from, as files (novox/hq ADR 0267), and what a build compiling a Go
|
|
// program is handed.
|
|
|
|
// onTrunk answers the trunk's questions as a clone of a commit on main would, or off it.
|
|
type onTrunk struct {
|
|
*recorded
|
|
off bool
|
|
// behind is a commit on the trunk that is not its head: an older commit built by hand.
|
|
behind bool
|
|
}
|
|
|
|
func (o onTrunk) run(ctx context.Context, dir, name string, args ...string) (string, error) {
|
|
if name == "git" && len(args) > 0 && args[0] == "symbolic-ref" {
|
|
return "origin/main\n", nil
|
|
}
|
|
if name == "git" && len(args) > 1 && args[0] == "rev-parse" && args[1] == "origin/main" && o.behind {
|
|
return "feedfacefeedfacefeedfacefeedfacefeedface\n", nil
|
|
}
|
|
if name == "git" && len(args) > 0 && args[0] == "merge-base" && o.off {
|
|
return "", os.ErrNotExist
|
|
}
|
|
return compiling{o.recorded}.run(ctx, dir, name, args...)
|
|
}
|
|
|
|
// aSharedRepository is a repository holding two programs that share a package, as the controller's does.
|
|
func aSharedRepository(readme, shared string) map[string]string {
|
|
return map[string]string{
|
|
"go.mod": "module example.com/ctl\n\ngo 1.22\n",
|
|
"go.sum": "",
|
|
"README.md": readme,
|
|
"cmd/ctl/main.go": "package main\n\nimport _ \"example.com/ctl/internal/shared\"\n\nfunc main() {}\n",
|
|
"proxy/main.go": "package main\n\nimport _ \"example.com/ctl/internal/shared\"\n\nfunc main() {}\n",
|
|
"internal/shared/s.go": "package shared\n\nconst S = " + shared + "\n",
|
|
"internal/only/o.go": "package only\n",
|
|
}
|
|
}
|
|
|
|
const aProxy = `{"module":"route-proxy","version":"1",
|
|
"build":{"artifacts":[
|
|
{"name":"server","kind":"image","from":"Dockerfile","compiles":"proxy",
|
|
"context":{"repository":"https://forge.invalid/ctl.git","ref":"main"}},
|
|
{"name":"trust","kind":"upstream","from":"alpine@sha256:` + "3333333333333333333333333333333333333333333333333333333333333333" + `"}]}}`
|
|
|
|
func buildTheProxy(t *testing.T, context_ map[string]string, off bool, behind ...bool) (Result, *recorded, string) {
|
|
t.Helper()
|
|
r := &recorded{
|
|
contents: map[string]string{"modules/route-proxy/" + ManifestName: aProxy, "modules/route-proxy/Dockerfile": "FROM scratch\nCOPY . .\n", "modules/route-proxy/README.md": "x"},
|
|
secondary: map[string]map[string]string{"https://forge.invalid/ctl.git": context_},
|
|
}
|
|
workspace := t.TempDir()
|
|
got, err := Build(context.Background(), onTrunk{r, off, len(behind) > 0 && behind[0]}.run, r,
|
|
"https://forge.invalid/catalogue.git", "modules/route-proxy", "", workspace, nil, Npmrc{}, GitCredential{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return got, r, workspace
|
|
}
|
|
|
|
func TestAnImageCompilingGoIsHandedItsBuildSourceAndSaysIt(t *testing.T) {
|
|
got, r, workspace := buildTheProxy(t, aSharedRepository("one", "1"), false)
|
|
|
|
// Built in the narrowed tree, which holds the program's closure and nothing else.
|
|
at := ""
|
|
for i, line := range r.ran {
|
|
if strings.HasPrefix(line, "docker build ") {
|
|
at = r.dirs[i]
|
|
}
|
|
}
|
|
if filepath.Base(at) != "narrow-server" {
|
|
t.Fatalf("docker build ran in %q, not the narrowed build source", at)
|
|
}
|
|
for file, want := range map[string]bool{"proxy/main.go": true, "internal/shared/s.go": true, "go.mod": true,
|
|
"cmd/ctl/main.go": false, "internal/only/o.go": false, "README.md": false} {
|
|
_, err := os.Stat(filepath.Join(workspace, "narrow-server", filepath.FromSlash(file)))
|
|
if (err == nil) != want {
|
|
t.Errorf("%s handed to the recipe: %v, wanted %v", file, err == nil, want)
|
|
}
|
|
}
|
|
|
|
// Said per repository: its own, the manifest and the recipe; the context's, the closure.
|
|
if len(got.Sources) != 2 {
|
|
t.Fatalf("sources %+v", got.Sources)
|
|
}
|
|
own, ctx := got.Sources[0], got.Sources[1]
|
|
if own.Repository != "" || !slices.Equal(own.Paths, []string{"modules/route-proxy/Dockerfile", "modules/route-proxy/module.json"}) {
|
|
t.Errorf("its own build source: %+v", own)
|
|
}
|
|
if ctx.Repository != "https://forge.invalid/ctl.git" || ctx.Ref != "main" {
|
|
t.Errorf("the context's build source names %q at %q", ctx.Repository, ctx.Ref)
|
|
}
|
|
for file, want := range map[string]bool{"proxy/main.go": true, "internal/shared/s.go": true, "go.mod": true,
|
|
"cmd/ctl/main.go": false, "README.md": false} {
|
|
if SourceHolds(ctx.Paths, file) != want {
|
|
t.Errorf("the context's build source holds %s: %v, wanted %v (%v)", file, !want, want, ctx.Paths)
|
|
}
|
|
}
|
|
|
|
// **The fingerprint is over the build source** (rule 5): a change outside it is one build, inside it another.
|
|
readme, _, _ := buildTheProxy(t, aSharedRepository("two", "1"), false)
|
|
if readme.Source != got.Source {
|
|
t.Errorf("a README of the context changed the fingerprint: %s %s", got.Source, readme.Source)
|
|
}
|
|
shared, _, _ := buildTheProxy(t, aSharedRepository("one", "2"), false)
|
|
if shared.Source == got.Source {
|
|
t.Error("a change to the program's closure kept its fingerprint")
|
|
}
|
|
}
|
|
|
|
// A build off the trunk, or of a trunk commit that is not its head, says no build source: the planner maps
|
|
// a merge onto the trunk head's, and an older commit's closure lacks what was imported since.
|
|
func TestABuildOffTheTrunksHeadSaysNoBuildSource(t *testing.T) {
|
|
got, _, _ := buildTheProxy(t, aSharedRepository("one", "1"), true)
|
|
if len(got.Sources) != 0 {
|
|
t.Fatalf("a build off the trunk said %+v", got.Sources)
|
|
}
|
|
got, _, _ = buildTheProxy(t, aSharedRepository("one", "1"), false, true)
|
|
if len(got.Sources) != 0 {
|
|
t.Fatalf("a build of an older trunk commit said %+v", got.Sources)
|
|
}
|
|
}
|
|
|
|
// An archive of the module's whole directory holds every file of it.
|
|
func TestAnArchiveOfTheWholeDirectoryHoldsIt(t *testing.T) {
|
|
manifest := `{"module":"look","version":"1","build":{"artifacts":[{"name":"all","kind":"archive","from":"."}]},
|
|
"resources":[{"id":"files","type":"archive","path":"/opt/look","artifact":"all"}]}`
|
|
r := &recorded{contents: map[string]string{"modules/look/" + ManifestName: manifest, "modules/look/a/b.css": "x"}}
|
|
got, err := Build(context.Background(), onTrunk{recorded: r}.run, r,
|
|
"https://forge.invalid/catalogue.git", "modules/look", "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Sources) != 1 || !SourceHolds(got.Sources[0].Paths, "modules/look/a/b.css") ||
|
|
SourceHolds(got.Sources[0].Paths, "modules/other/x") {
|
|
t.Fatalf("sources %+v", got.Sources)
|
|
}
|
|
}
|
|
|
|
// A recipe reading past its build source fails, naming what it could not find — in the real docker build;
|
|
// here, the file is simply not in the tree it is handed, which is what makes that so.
|
|
func TestAnImageCompilingANonexistentPackageFails(t *testing.T) {
|
|
r := &recorded{
|
|
contents: map[string]string{ManifestName: strings.Replace(aProxy, `"compiles":"proxy"`, `"compiles":"nowhere"`, 1),
|
|
"Dockerfile": "FROM scratch\n"},
|
|
secondary: map[string]map[string]string{"https://forge.invalid/ctl.git": aSharedRepository("one", "1")},
|
|
}
|
|
_, err := Build(context.Background(), onTrunk{recorded: r}.run, r,
|
|
"https://forge.invalid/catalogue.git", "", "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil)
|
|
if err == nil || !strings.Contains(err.Error(), "nowhere") {
|
|
t.Fatalf("a package that is not there built: %v", err)
|
|
}
|
|
}
|
|
|
|
// A Go bundle of a module built from its repository's root says its import closure, and the manifest;
|
|
// an image that compiles nothing it was told of leaves the module's source whole, and says none.
|
|
func TestAGoBundleSaysItsClosureAndAnUntoldImageNothing(t *testing.T) {
|
|
files := aSharedRepository("one", "1")
|
|
manifest := `{"module":"ctl","version":"1","build":{"artifacts":[
|
|
{"name":"controller","kind":"bundle","language":"go","system":"arch","from":"cmd/ctl","binary":"ctl"}]},
|
|
"resources":[{"id":"controller","type":"process","name":"ctl","artifact":"controller","run":["./ctl"]}]}`
|
|
r := &recorded{contents: map[string]string{ManifestName: manifest}}
|
|
for k, v := range files {
|
|
r.contents[k] = v
|
|
}
|
|
held := map[string]string{"mesh-tools-go/build": "registry.invalid/mesh-tools-go/build@sha256:" + strings.Repeat("b", 64)}
|
|
got, err := Build(context.Background(), onTrunk{recorded: r}.run, r,
|
|
"https://forge.invalid/ctl.git", "", "", t.TempDir(), held, Npmrc{}, GitCredential{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Sources) != 1 || got.Sources[0].Repository != "" {
|
|
t.Fatalf("sources %+v", got.Sources)
|
|
}
|
|
for file, want := range map[string]bool{"cmd/ctl/main.go": true, "internal/shared/s.go": true, ManifestName: true,
|
|
"go.sum": true, "proxy/main.go": false, "README.md": false, "internal/only/o.go": false} {
|
|
if SourceHolds(got.Sources[0].Paths, file) != want {
|
|
t.Errorf("%s: held %v, wanted %v (%v)", file, !want, want, got.Sources[0].Paths)
|
|
}
|
|
}
|
|
|
|
untold := `{"module":"ctl","version":"1","build":{"artifacts":[
|
|
{"name":"server","kind":"image","from":"Dockerfile"}]}}`
|
|
r = &recorded{contents: map[string]string{ManifestName: untold, "Dockerfile": "FROM scratch\n"}}
|
|
got, err = Build(context.Background(), onTrunk{recorded: r}.run, r,
|
|
"https://forge.invalid/ctl.git", "", "", t.TempDir(), nil, Npmrc{}, GitCredential{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got.Sources) != 0 {
|
|
t.Fatalf("an image compiling nothing it was told of said a build source: %+v", got.Sources)
|
|
}
|
|
}
|