package check import ( "context" "fmt" "strings" "testing" "github.com/nats-io/nats.go" "git.novox.be/novox/mesh-sdk/go/delivery" ) // Issue 1820: a build step goes to a node whose build module is not heard (a laptop asleep, not yet marked offline by the // watchman), and the bus answers "no responders". The check moves the step to another node of the same build module. // deafNodes makes every tool on the named nodes fail as the bus does when nobody listens there; count says how many // times the node was called. func deafNodes(b *bench, deaf ...string) { for tool, h := range b.steps.handlers { h := h b.steps.handlers[tool] = func(c stepCall) (any, error) { for _, n := range deaf { if c.node == n { return nil, fmt.Errorf("%s on %s: %w", c.tool, c.node, nats.ErrNoResponders) } } return h(c) } } } func nodesOf(calls []stepCall) []string { var out []string for _, c := range calls { out = append(out, c.node) } return out } func TestAFetchThatNobodyAnswersMovesToAnotherBuildNode(t *testing.T) { // 1820/AC1 b := newBench(t) b.ex.Toolchains = goEntries("ace", "g14") deafNodes(b, "ace") v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict if v.State != delivery.VerdictPass { t.Fatalf("verdict = %+v, want a pass on the node that answers", v) } if got := nodesOf(b.steps.tool("fetch")); len(got) != 2 || got[0] != "ace" || got[1] != "g14" { t.Errorf("fetch went to %v, want ace (no responders) then g14", got) } for _, c := range b.steps.tool("test") { if c.node != "g14" { t.Errorf("a test went to %s, want g14 only", c.node) } } if len(v.Machines) != 1 || v.Machines[0] != "g14" { t.Errorf("machines = %v, want [g14]: ace ran nothing", v.Machines) } } func TestATestStepThatNobodyAnswersIsFetchedAgainOnAnotherNode(t *testing.T) { // 1820/AC2 b := newBench(t) b.ex.Toolchains = goEntries("ace", "g14") // ace answers the fetch and the digest, then goes to sleep before the test. test := b.steps.handlers["build_go_test"] b.steps.handlers["build_go_test"] = func(c stepCall) (any, error) { if c.node == "ace" { return nil, fmt.Errorf("test on ace: %w", nats.ErrNoResponders) } return test(c) } v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict if v.State != delivery.VerdictPass { t.Fatalf("verdict = %+v, want a pass", v) } if got := nodesOf(b.steps.tool("fetch")); len(got) != 2 || got[1] != "g14" { t.Errorf("fetch went to %v, want ace then g14 (the workspace was on ace)", got) } for _, c := range b.steps.tool("test") { if c.node == "g14" && c.args["workspace"] == nil { t.Errorf("a test on g14 has no workspace") } } if len(v.Machines) != 1 || v.Machines[0] != "g14" { t.Errorf("machines = %v, want [g14]", v.Machines) } } func TestWhenNoBuildNodeAnswersTheFailureNamesEveryNodeTried(t *testing.T) { // 1820/AC3 b := newBench(t) b.ex.Toolchains = goEntries("ace", "g14", "shanks") deafNodes(b, "ace", "g14", "shanks") v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict if v.State != delivery.VerdictFail { t.Fatalf("verdict = %+v, want a failure", v) } for _, n := range []string{"ace", "g14", "shanks"} { if !strings.Contains(v.Failed.Words, n) { t.Errorf("failure %q does not name %s", v.Failed.Words, n) } } if !IsNodesFailure(v) { t.Errorf("failure %q is not a failure of the nodes, so it would not be judged again when they change", v.Failed.Words) } if got := len(b.steps.tool("fetch")); got != 3 { t.Errorf("fetch was tried %d times, want once on each of 3 nodes", got) } } func TestAStepThatFailedForTheChangeIsNotMovedToAnotherNode(t *testing.T) { // 1820/AC4 b := newBench(t) b.ex.Toolchains = goEntries("ace", "g14") b.steps.handlers["build_go_fetch"] = func(stepCall) (any, error) { return nil, fmt.Errorf("fetch on ace: the step failed: git exploded") } v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict if v.State != delivery.VerdictFail || len(b.steps.tool("fetch")) != 1 { t.Errorf("verdict = %+v after %d fetches, want a failure after one", v, len(b.steps.tool("fetch"))) } } func TestACanceledCheckIsNotMovedToAnotherNode(t *testing.T) { // 1820/AC4 b := newBench(t) b.ex.Toolchains = goEntries("ace", "g14") ctx, cancel := context.WithCancel(context.Background()) b.steps.handlers["build_go_fetch"] = func(stepCall) (any, error) { cancel() return nil, fmt.Errorf("fetch on ace: %w", nats.ErrNoResponders) } _, _ = b.ex.Judge(ctx, caseOf(t, pullOf(repoA, 7, head1))) if n := len(b.steps.tool("fetch")); n != 1 { t.Errorf("fetch was tried %d times after the check ended, want 1", n) } } func TestAPinReadThatNobodyAnswersIsTheForgesFailureNotABuildNodes(t *testing.T) { // 1820/AC5 b := groupBench(t, "cb54756c3865") b.ex.Forge.(*fakeForge).err = fmt.Errorf("gitea.gitea_get_file: %w", nats.ErrNoResponders) v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2))).Verdict if v.State != delivery.VerdictFail || v.Failed.Machine != "forge" { t.Errorf("verdict = %+v, want a failure at the forge: no build node was to blame", v) } }