package check import ( "context" "errors" "strings" "testing" "time" "git.novox.be/novox/mesh-sdk/go/delivery" ) func assignerOn(instance string, online bool) PresenceEvent { key, _ := delivery.PresencePartKey(delivery.AssignerModule, instance) return PresenceEvent{Key: key, Online: online} } func compEvent(t *testing.T, v delivery.CompositionVerdict) CompositionEvent { t.Helper() data, err := delivery.EncodeCompositionVerdict(v) if err != nil { t.Fatal(err) } return CompositionEvent{Value: data} } var passed = delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionPass, SDK: "x", Machines: []string{"g14"}} // 1297/B3d: with no assigner online, the wait answers ErrAssignerNotRunning as soon as both watches handed what stands. func TestNoAssignerAnswersAtOnce(t *testing.T) { presence, comps := make(chan PresenceEvent, 4), make(chan CompositionEvent, 4) presence <- assignerOn("novox", false) presence <- PresenceEvent{Settled: true} comps <- CompositionEvent{Settled: true} ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() if _, err := AwaitComposition(ctx, presence, comps); !errors.Is(err, ErrAssignerNotRunning) { t.Fatalf("answered %v, want the assigner not running", err) } } // 1297/B3d: with the assigner online it waits for the key, past a composition that waits, and answers the verdict. func TestAnOnlineAssignerIsWaitedFor(t *testing.T) { presence, comps := make(chan PresenceEvent, 4), make(chan CompositionEvent, 4) presence <- assignerOn("novox", true) presence <- PresenceEvent{Settled: true} comps <- CompositionEvent{Settled: true} done := make(chan error, 1) var got delivery.CompositionVerdict go func() { var err error got, err = AwaitComposition(context.Background(), presence, comps) done <- err }() comps <- compEvent(t, delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionWaits, Words: "w"}) select { case err := <-done: t.Fatalf("answered before the composition was written: %v", err) case <-time.After(50 * time.Millisecond): } comps <- compEvent(t, passed) if err := <-done; err != nil || got.State != delivery.CompositionPass { t.Fatalf("answered %+v, %v", got, err) } // And the last instance going offline during a wait ends it as not running. presence2, comps2 := make(chan PresenceEvent, 4), make(chan CompositionEvent, 4) presence2 <- assignerOn("novox", true) presence2 <- PresenceEvent{Settled: true} comps2 <- CompositionEvent{Settled: true} presence2 <- assignerOn("novox", false) if _, err := AwaitComposition(context.Background(), presence2, comps2); !errors.Is(err, ErrAssignerNotRunning) { t.Errorf("the assigner going offline answered %v", err) } } // 1297/B3d: a passing check with no assigner says at once, loud, that its composition is not judged; never a pass of it. func TestAPassingCheckWithNoAssignerSaysTheCompositionIsNotJudged(t *testing.T) { b := newBench(t) b.ex.Compositions = &fakeCompositions{err: ErrAssignerNotRunning} v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict if v.Composition == nil || v.Composition.State != delivery.CompositionNotRun || v.Composition.Words != delivery.AssignerNotRunning { t.Fatalf("composition = %+v", v.Composition) } if words := compositionWords(v.Composition, 0); words != "composition not judged: the assigner is not running" { t.Errorf("the line says %q", words) } } // 1297/B3d: the assigner starting writes the composition later, and the checker takes it into the verdict that said it // was not judged, writing the status again; a failed one fails the check. func TestTheAssignerStartingBringsTheComposition(t *testing.T) { for _, comp := range []delivery.CompositionVerdict{passed, {Group: "feat/x", Heads: "", State: delivery.CompositionFail, SDK: "x", Machine: "g14", Words: "g14 runs web, which the change removes"}} { ch, cs := checkerWithNotJudged(t) comp.Heads = cs.HeadsHash ch.Composed(context.Background(), comp) raw, _, _, _ := ch.Verdicts.Get(context.Background(), cs.Key) v, err := delivery.DecodeVerdict(raw) if err != nil || v.Composition == nil || v.Composition.State != comp.State { t.Fatalf("after the composition came, the verdict is %+v, %v", v, err) } if comp.State == delivery.CompositionFail && (v.State != delivery.VerdictFail || v.Failed.Machine != "g14") { t.Errorf("a failed composition left the verdict %+v", v) } owed := ch.Owed.(*memKV) keys, _ := owed.Keys(context.Background()) if len(keys) == 0 { t.Fatal("no status was written again") } data, _, _, _ := owed.Get(context.Background(), keys[0]) o, _ := delivery.DecodeCheckerOwed(data) if !strings.Contains(o.Text, "composition passed") && !strings.Contains(o.Text, "g14 runs web") { t.Errorf("the status's line says %q", o.Text) } } } // checkerWithNotJudged is a checker holding a candidate whose passing verdict said its composition was not judged. func checkerWithNotJudged(t *testing.T) (*Checker, *Case) { t.Helper() cs := caseOf(t, pullOf(repoA, 7, head1)) raw := candidateOf(t, cs.Group, pullOf(repoA, 7, head1)) ch := &Checker{Verdicts: newMemKV(), Owed: newMemKV(), Trunks: fixedTrunks{cs.Trunk}} ckey, _ := delivery.CandidatesKey(cs.Group) ch.init() c, err := delivery.DecodeCandidate(raw) if err != nil { t.Fatal(err) } ch.known[ckey] = c if got := CaseOf(c, ch.heads()); got == nil || got.Key != cs.Key { t.Fatalf("the case is %+v, want %s", got, cs.Key) } v := delivery.Verdict{Group: cs.Group, Heads: cs.Heads, State: delivery.VerdictPass, Levels: map[string]delivery.LevelResult{delivery.LevelUnit: {Run: 1}, delivery.LevelIntegration: {Run: 1}}, Composition: &delivery.Composition{State: delivery.CompositionNotRun, Words: delivery.AssignerNotRunning}} data, err := delivery.EncodeVerdict(v) if err != nil { t.Fatal(err) } if _, err := ch.Verdicts.Create(context.Background(), cs.Key, data); err != nil { t.Fatal(err) } return ch, cs } type fixedTrunks struct{ heads map[string]string } func (f fixedTrunks) Heads() map[string]string { return f.heads }