Files
mesh-controller/internal/inventory/builds_test.go
T
jochen 6c616838a5 Move a module on a merge only when its build source holds a changed file (hq ADR 0267, issue 363)
Every merge to the controller's repository planned the controller, the
build seat's holder and the route proxy in three gated tiers, whatever it
changed (issue 338). The planner now maps a merge's files onto the build
source each module's newest trunk build said: a README moves nothing, the
controller's command the controller alone, the proxy's program the proxy
alone. A module with none said, or one an open plan has yet to build, is
read whole as before. Sharing a repository draws no packages edge any more,
and one recorded before neither widens nor orders a plan.
2026-10-10 03:20:36 +02:00

217 lines
7.2 KiB
Go

package inventory
import (
"context"
"reflect"
"strings"
"testing"
)
// A build result was answered to whoever asked and kept nowhere, so "when did this last build",
// "why did it fail" and "which machine built what is running" had no answer at all.
func aBuild(id, module, failed string) Build {
b := Build{
ID: id, Repository: "https://forge.invalid/" + strings.TrimSuffix(module, "?") + ".git",
Module: module, On: "a-build-machine", Failed: failed,
}
if failed == "" {
b.Commit = "c0ffee" + id
b.Made = []Artifact{{Name: "config", Kind: "archive", Reference: "…/blobs/sha256:…"}}
}
return b
}
func TestAFailedBuildIsARowLikeAnyOther(t *testing.T) {
// One that leaves no trace is indistinguishable from a build nobody asked for, and the
// difference is the whole of whether somebody should be looking at something.
inv := fresh(t)
ctx := context.Background()
if err := inv.RecordBuild(ctx, aBuild("1", "", "cannot clone: no such repository")); err != nil {
t.Fatal(err)
}
got, err := inv.Builds(ctx, "", 10)
if err != nil {
t.Fatal(err)
}
if len(got) != 1 {
t.Fatalf("got %d builds", len(got))
}
if got[0].Worked() {
t.Fatal("a failure was recorded as a success")
}
if !strings.Contains(got[0].Failed, "no such repository") {
t.Fatalf("the builder's own words were not kept: %q", got[0].Failed)
}
// And it kept what was asked for, which is the only thing a person can go and look at when
// the build never learned what it was building.
if got[0].Module != "" || got[0].Repository == "" {
t.Fatalf("got %+v", got[0])
}
}
func TestOneResultRecordedTwiceIsOneBuild(t *testing.T) {
// A result can arrive twice: as the answer to whoever asked, and on the exchange when nobody
// was. Two rows would show one build as two, and which is real is not answerable afterwards.
inv := fresh(t)
ctx := context.Background()
for i := 0; i < 2; i++ {
if err := inv.RecordBuild(ctx, aBuild("same", "shell", "")); err != nil {
t.Fatal(err)
}
}
got, err := inv.Builds(ctx, "", 10)
if err != nil {
t.Fatal(err)
}
if len(got) != 1 {
t.Fatalf("one build was recorded %d times", len(got))
}
}
func TestBuildsComeBackNewestFirstAndCanBeAskedPerModule(t *testing.T) {
inv := fresh(t)
ctx := context.Background()
for _, b := range []Build{
aBuild("1", "shell", ""),
aBuild("2", "meshboard", ""),
aBuild("3", "shell", "the tests failed"),
} {
if err := inv.RecordBuild(ctx, b); err != nil {
t.Fatal(err)
}
}
all, err := inv.Builds(ctx, "", 10)
if err != nil {
t.Fatal(err)
}
if len(all) != 3 || all[0].ID != "3" {
t.Fatalf("newest is not first: %v", ids(all))
}
// Per module, because "what has happened to this" is asked when deciding whether to trust it.
shell, err := inv.Builds(ctx, "shell", 10)
if err != nil {
t.Fatal(err)
}
if len(shell) != 2 {
t.Fatalf("shell has %d builds: %v", len(shell), ids(shell))
}
for _, b := range shell {
if b.Module != "shell" {
t.Fatalf("asked for shell and got %q", b.Module)
}
}
}
func TestWhatWasPublishedIsKeptWithTheBuild(t *testing.T) {
// So a digest can be traced back to the build that made it, without keeping the manifest a
// second time in a place that can disagree with the first.
inv := fresh(t)
ctx := context.Background()
if err := inv.RecordBuild(ctx, aBuild("1", "shell", "")); err != nil {
t.Fatal(err)
}
got, _ := inv.Builds(ctx, "", 10)
if len(got[0].Made) != 1 || got[0].Made[0].Kind != "archive" {
t.Fatalf("what was published was not kept: %+v", got[0].Made)
}
}
func TestAskingForMoreThanThereIsIsNotAnError(t *testing.T) {
inv := fresh(t)
got, err := inv.Builds(context.Background(), "", 100)
if err != nil {
t.Fatal(err)
}
if len(got) != 0 {
t.Fatalf("got %d", len(got))
}
}
func ids(builds []Build) []string {
var out []string
for _, b := range builds {
out = append(out, b.ID)
}
return out
}
// What a build read besides its module's own repository comes back for the newest build of each
// module, and only for builds that worked. Nothing recorded is absent rather than empty, which is how
// a build made before the mesh kept this is told from one that read nothing (novox/hq 04-ISSUES/131).
func TestWhatABuildReadComesBackForTheNewestBuildOfEachModule(t *testing.T) {
inv := fresh(t)
ctx := context.Background()
older := aBuild("older", "builder", "")
older.Read = []ReadRepository{{Repository: "novox/mesh-controller", Ref: "release"}}
newer := aBuild("newer", "builder", "")
newer.Read = []ReadRepository{{Repository: "novox/mesh-controller", Ref: "main"}}
plain := aBuild("plain", "gitea", "")
failed := aBuild("failed", "route-proxy", "cannot clone")
failed.Read = []ReadRepository{{Repository: "novox/mesh-controller", Ref: "main"}}
for _, b := range []Build{older, newer, plain, failed} {
if err := inv.RecordBuild(ctx, b); err != nil {
t.Fatal(err)
}
}
read, err := inv.ReadRepositories(ctx)
if err != nil {
t.Fatal(err)
}
if len(read["builder"]) != 1 || read["builder"][0].Ref != "main" {
t.Fatalf("the newest build's reading is %+v", read["builder"])
}
if _, has := read["gitea"]; has {
t.Fatalf("a build that read nothing but its own repository reads as %+v", read["gitea"])
}
if _, has := read["route-proxy"]; has {
t.Fatal("a failed build's reading was kept as what that module reads")
}
}
// **What a build said it was made from comes back laid over what it read** (novox/hq ADR 0267): a context's
// paths on its entry, the module's own as an entry marked Own — and never in the stored `built_contexts`,
// where a controller that predates them would read an own entry as a context of its own repository.
func TestABuildsBuildSourceComesBackOverWhatItRead(t *testing.T) {
inv := fresh(t)
ctx := context.Background()
said := aBuild("said", "route-proxy", "")
said.Read = []ReadRepository{{Repository: "novox/mesh-controller", Ref: "main"}, {Repository: "novox/other"}}
said.Sources = []BuildSource{
{Paths: []string{"modules/route-proxy/Dockerfile", "modules/route-proxy/module.json"}},
{Repository: "novox/mesh-controller", Ref: "main", Paths: []string{"examples/route-proxy/", "go.mod"}},
{Repository: "novox/unread", Paths: []string{"x"}},
}
unsaid := aBuild("unsaid", "build-agent", "")
unsaid.Read = []ReadRepository{{Repository: "novox/mesh-controller", Ref: "main"}}
for _, b := range []Build{said, unsaid} {
if err := inv.RecordBuild(ctx, b); err != nil {
t.Fatal(err)
}
}
read, err := inv.ReadRepositories(ctx)
if err != nil {
t.Fatal(err)
}
want := []ReadRepository{
{Repository: "novox/mesh-controller", Ref: "main", Paths: []string{"examples/route-proxy/", "go.mod"}},
{Repository: "novox/other"},
{Paths: []string{"modules/route-proxy/Dockerfile", "modules/route-proxy/module.json"}, Own: true},
}
if !reflect.DeepEqual(read["route-proxy"], want) {
t.Fatalf("read back %+v\nwanted %+v", read["route-proxy"], want)
}
if got := read["build-agent"]; len(got) != 1 || got[0].Paths != nil || got[0].Own {
t.Fatalf("a build that said no build source reads as %+v", got)
}
var stored string
if err := inv.store.Pool().QueryRow(ctx, `select built_contexts::text from build where id = 'said'`).Scan(&stored); err != nil {
t.Fatal(err)
}
if strings.Contains(stored, "paths") || strings.Contains(stored, "own") {
t.Fatalf("the build source was stored among what the build read: %s", stored)
}
}