Files
mesh-controller/internal/inventory/builds_test.go
T
jochen 150f038ff8 Read a module whole while a plan has overtaken its build source, and plan every view alike (hq ADR 0267, review)
A failed build, or a plan closed before reaching a module, left the
closure its last good build said, and a fix-forward to a newly imported
package would have moved nothing. A missed merge moving only a module
that packages the repository was never caught up, and an older merge
read as history for it through a look that was not its own. The gate,
a pull request's check, the what-if and a delivery's order now read the
same view the merge handler does.
2026-10-10 03:20:36 +02:00

250 lines
8.6 KiB
Go

package inventory
import (
"context"
"reflect"
"strings"
"testing"
"time"
)
// 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},
}
for k := range read["route-proxy"] {
if read["route-proxy"][k].Built.IsZero() {
t.Errorf("no build time on %+v", read["route-proxy"][k])
}
read["route-proxy"][k].Built, read["route-proxy"][k].Looked = time.Time{}, time.Time{}
}
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)
}
}
// A build newer than the newest that worked, and failed, leaves that one's build source stale: its module is
// read whole until a build works again (novox/hq ADR 0267).
func TestAFailedNewerBuildLeavesTheBuildSourceStale(t *testing.T) {
inv := fresh(t)
ctx := context.Background()
worked := aBuild("worked", "route-proxy", "")
worked.Asked = time.Now().Add(-time.Hour)
worked.Read = []ReadRepository{{Repository: "novox/mesh-controller", Ref: "main"}}
worked.Sources = []BuildSource{{Paths: []string{"modules/route-proxy/module.json"}},
{Repository: "novox/mesh-controller", Ref: "main", Paths: []string{"examples/route-proxy/"}}}
failed := aBuild("failed", "route-proxy", "compile error")
failed.Asked = time.Now()
for _, b := range []Build{worked, failed} {
if err := inv.RecordBuild(ctx, b); err != nil {
t.Fatal(err)
}
}
read, err := inv.ReadRepositories(ctx)
if err != nil {
t.Fatal(err)
}
if got := read["route-proxy"]; len(got) != 1 || got[0].Paths != nil || got[0].Own || !got[0].Looked.After(got[0].Built) {
t.Fatalf("after a newer failed build the proxy reads as %+v", got)
}
}