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) } got := read["route-proxy"] if len(got) != 2 || got[0].Paths != nil || got[0].Own || !got[0].Looked.After(got[0].Built) || !got[1].Own || got[1].Paths != nil || got[1].Whole != "a newer build of it failed" { t.Fatalf("after a newer failed build the proxy reads as %+v", got) } }