package main import ( "context" "errors" "reflect" "strings" "testing" "github.com/novox/mesh-controller/internal/builder" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // One build per change (novox/hq ADR 0350), as the controller asks it and takes it in. // The check of a change is asked to build what a merge of it would move, in the walk's order: a module of the // repository checked from the merge, one standing on it from its own repository as the walk builds it, and one // that reads the repository through a context not at all. A change that moves no module builds nothing. func TestACheckIsAskedToBuildWhatTheMergeWouldMove(t *testing.T) { entries := []inventory.Entry{ {Manifest: catalogue.Manifest{Module: "app"}, Source: inventory.Source{Repository: "novox/shop", Seat: "git", Path: "modules/app"}}, {Manifest: catalogue.Manifest{Module: "web"}, Source: inventory.Source{Repository: "novox/web", Seat: "git", Ref: "main"}}, {Manifest: catalogue.Manifest{Module: "docs"}, Source: inventory.Source{Repository: "novox/docs", Seat: "git"}}, } p := link.PullUpdated{Owner: "novox", Repo: "shop", Number: 7, Base: "main", Commit: "abc"} scope := checkScope{Reach: mergeReach{Packaging: entries[2:], Plan: inventory.Plan{Tiers: [][]string{{"app", "docs"}, {"web"}}}}} clone := func(s inventory.Source) (string, error) { return "http://forge.invalid/" + s.Repository + ".git", nil } got := checkBuildsOf(p, scope, entries, clone) want := []link.CheckBuild{ {Module: "app", Path: "modules/app", Merged: true, Tier: 0}, {Module: "docs", Tier: 0, Skip: "its build reads novox/shop through a context, which a check cannot hand the merge"}, {Module: "web", Repository: "http://forge.invalid/novox/web.git", Ref: "main", Tier: 1}, } if !reflect.DeepEqual(got, want) { t.Fatalf("the check is asked to build\n%+v\nwant\n%+v", got, want) } if got := checkBuildsOf(p, checkScope{}, entries, clone); got != nil { t.Fatalf("a change that moves no module asks for builds: %+v", got) } } // A walk's ask carries its leave to take a check's build, naming the pull requests of the merges it answers; a dry // run never does, and neither does an ask that is not a walk's. // A walk ended lets go of its merges' check builds — only once it ended, after it took what it reuses. func TestAWalkEndedLetsGoOfItsMergesCheckBuilds(t *testing.T) { var forgot []builder.PullRef was := forgetChecks forgetChecks = func(_ context.Context, _ *inventory.Inventory, pull builder.PullRef) (string, error) { forgot = append(forgot, pull) return "", nil } t.Cleanup(func() { forgetChecks = was }) p := inventory.Plan{ID: "plan-1", Delivery: &inventory.PlanDelivery{Merges: []inventory.PlanMerge{ {Repository: "novox/shop", Number: 7}}}} p.WalkSaid = inventory.WalkStarted forgetChecksOfWalk(t.Context(), nil, p) p.WalkSaid = "" forgetChecksOfWalk(t.Context(), nil, p) if len(forgot) != 0 { t.Fatalf("a walk not ended let go of %v, which it may still take", forgot) } p.WalkSaid = inventory.WalkEnded forgetChecksOfWalk(t.Context(), nil, p) if want := []builder.PullRef{{Owner: "novox", Repo: "shop", Number: 7}}; !reflect.DeepEqual(forgot, want) { t.Fatalf("the walk ended let go of %v", forgot) } } // A build in the scratch namespace is never registered: only the walk's copy in the module's own namespace is. func TestABuildInTheScratchNamespaceIsNeverRegistered(t *testing.T) { if got := inScratch([]link.MadeArtifact{{Name: "server", Reference: "store.internal:5000/app/server@sha256:aa"}}); got != "" { t.Fatalf("a module's own build reads as a scratch build: %s", got) } scratch := "store.internal:5000/scratch/app/server@sha256:aa" if got := inScratch([]link.MadeArtifact{{Name: "server", Reference: scratch}}); got != scratch { t.Fatalf("a scratch build was not found: %q", got) } if got := inScratch([]link.MadeArtifact{{Name: "look", Reference: "http://store.internal:5000/v2/scratch/app/look/blobs/sha256:bb"}}); got == "" { t.Fatal("a scratch archive was not found") } } // The walk says per module what it did: "reused from …" or "built: " — and a build seat from before says // neither, which is said. func TestAWalksBuildSaysWhetherItReusedOrBuilt(t *testing.T) { if got := (link.BuildResult{Reused: "reused from the check of novox/shop#7"}).MadeWords(); got != "reused from the check of novox/shop#7" { t.Fatalf("said %q", got) } if got := (link.BuildResult{BuiltWhy: "no check built it"}).MadeWords(); got != "built: no check built it" { t.Fatalf("said %q", got) } if got := (link.BuildResult{}).MadeWords(); !strings.Contains(got, "predates ADR 0350") { t.Fatalf("said %q", got) } } // A walk's module the check's build stood in for is registered by the digest the check built — the copy in the // module's namespace — and the walk keeps "reused from …" for it; its first-node gate judges that registered // build as it judges one the walk built. A scratch reference is refused. Needs the store (MESH_TEST_POSTGRES), and // is named in testdata/store-tests. // A walk's leave names the builds its own pull requests' verdicts made at the head each merge merged — never an // earlier head's — read once per walk, and none when they cannot be read: the builder takes nothing a verdict does // not name (novox/hq ADR 0350 §2, issue 703). func TestAWalksLeaveNamesTheBuildsItsPullRequestsVerdictsMade(t *testing.T) { was := checkedBuildsOf forgetWalkVerdicts() t.Cleanup(func() { checkedBuildsOf = was; forgetWalkVerdicts() }) var asked [][]link.PullRef pull := link.PullRef{Owner: "novox", Repo: "shop", Number: 7} atHead := link.CheckedBuild{Pull: pull, Check: "build-2", Head: "bbbbbbbbbbbb", Built: link.CheckBuilt{Module: "app", Key: "r1:2"}} earlier := link.CheckedBuild{Pull: pull, Check: "build-1", Head: "aaaaaaaaaaaa", Built: link.CheckBuilt{Module: "app", Key: "r1:1"}} checkedBuildsOf = func(_ context.Context, pulls []link.PullRef) ([]link.CheckedBuild, error) { asked = append(asked, pulls) return []link.CheckedBuild{atHead, earlier}, nil } pulls := []link.PullRef{pull} heads := map[link.PullRef]string{pull: "bbbbbbbbbbbb0123456789"} for range 3 { if got := reuseWithVerdicts(t.Context(), "plan-1", &link.ReuseAsk{Pulls: pulls}, heads); !reflect.DeepEqual(got.Builds, []link.CheckedBuild{atHead}) { t.Fatalf("the leave names %+v, not only the merged head's build", got.Builds) } } if !reflect.DeepEqual(asked, [][]link.PullRef{pulls}) { t.Fatalf("the verdicts were read %d times for one walk: %v", len(asked), asked) } // A merge whose head was not said takes nothing. if got := reuseWithVerdicts(t.Context(), "plan-1", &link.ReuseAsk{Pulls: pulls}, nil); got.Builds != nil { t.Fatalf("a merge of no said head names %+v", got.Builds) } checkedBuildsOf = func(context.Context, []link.PullRef) ([]link.CheckedBuild, error) { return nil, errors.New("no bus") } if got := reuseWithVerdicts(t.Context(), "plan-2", &link.ReuseAsk{Pulls: pulls}, heads); got.Builds != nil { t.Fatalf("verdicts that could not be read named %+v", got.Builds) } } // A check is handed only the builds earlier checks of its own pull request at its own head made: another pull // request's build, or an earlier head's, is never kept (novox/hq issue 703). None when they cannot be read. func TestACheckIsHandedOnlyItsOwnHeadsBuilds(t *testing.T) { was := checkedBuildsOf t.Cleanup(func() { checkedBuildsOf = was }) pull := link.PullRef{Owner: "novox", Repo: "shop", Number: 7} mine := link.CheckedBuild{Pull: pull, Check: "build-1", Head: "bbbbbbbbbbbb", Built: link.CheckBuilt{Module: "app", Key: "r1:1"}} earlier := link.CheckedBuild{Pull: pull, Check: "build-0", Head: "aaaaaaaaaaaa", Built: link.CheckBuilt{Module: "app", Key: "r1:1"}} another := link.CheckedBuild{Pull: link.PullRef{Owner: "novox", Repo: "shop", Number: 8}, Check: "build-9", Head: "bbbbbbbbbbbb", Built: link.CheckBuilt{Module: "app", Key: "r1:1"}} var asked [][]link.PullRef checkedBuildsOf = func(_ context.Context, pulls []link.PullRef) ([]link.CheckedBuild, error) { asked = append(asked, pulls) return []link.CheckedBuild{mine, earlier, another}, nil } p := link.PullUpdated{Owner: "novox", Repo: "shop", Number: 7, Commit: "bbbbbbbbbbbb0123456789"} builds := []link.CheckBuild{{Module: "app"}} if got := namedForTheCheck(t.Context(), p, builds); !reflect.DeepEqual(got, []link.CheckedBuild{mine}) || !reflect.DeepEqual(asked, [][]link.PullRef{{pull}}) { t.Fatalf("the check is handed %+v, read for %v", got, asked) } checkedBuildsOf = func(context.Context, []link.PullRef) ([]link.CheckedBuild, error) { return nil, errors.New("no bus") } if got := namedForTheCheck(t.Context(), p, builds); got != nil { t.Fatalf("verdicts that could not be read named %+v", got) } } func forgetWalkVerdicts() { walkVerdicts.Lock() defer walkVerdicts.Unlock() walkVerdicts.read = map[string]verdictsRead{} } // A walk's merged heads are the forge's word for each merge; a merge announced without its head, or a pull request // merged at two heads in one walk, has none, and its walk takes no check's build (novox/hq issue 703). func TestAWalksMergedHeadsAreTheForgesWord(t *testing.T) { merge := func(number int, head string) inventory.PlanMerge { return inventory.PlanMerge{Repository: "novox/shop", Number: number, Head: head} } p := &inventory.Plan{Delivery: &inventory.PlanDelivery{Merges: []inventory.PlanMerge{ merge(7, "aaaaaaaaaaaa1"), merge(8, ""), merge(9, "bbbbbbbbbbbb"), merge(9, "cccccccccccc")}}} heads := mergedHeadsOf(p) shop := func(n int) link.PullRef { return link.PullRef{Owner: "novox", Repo: "shop", Number: n} } if heads[shop(7)] != "aaaaaaaaaaaa1" || heads[shop(8)] != "" || heads[shop(9)] != "" { t.Fatalf("merged heads %v", heads) } }