package view import ( "errors" "strings" "testing" "git.novox.be/novox/mesh-sdk/go/delivery" ) // 1280 (ADR 0556 clauses 2 and 3): the view derives, per repository, how far each module reached and the reached head, writes // it when it differs, and says each new reached head once as its event delivered. func TestTheViewDerivesHowFarEachRepositoryReached(t *testing.T) { const web, sdk = "novox/web", "novox/sdk" commitA, commitB, commitC := strings.Repeat("a", 40), strings.Repeat("b", 40), strings.Repeat("c", 40) commitS, commitX := strings.Repeat("5", 40), strings.Repeat("e", 40) const fpMail = "src2:3333333333333333333333333333333333333333333333333333333333333333" mod := func(module, fp, commit string, src uint64, history bool) Entry { v, err := delivery.EncodeModule(delivery.Module{FP2: fp, Manifest: strings.Repeat("a", 64), Repository: web, Commit: commit, Src: src, Path: module}) if err != nil { t.Fatal(err) } return Entry{Bucket: delivery.ModulesBucket, Key: module, Value: v, At: t0, History: history} } src := func(rev uint64, webHead string) Entry { v, err := delivery.EncodeSource(delivery.Source{Group: "g", Heads: map[string]string{web: webHead, sdk: commitS}}) if err != nil { t.Fatal(err) } return Entry{Bucket: delivery.SourceBucket, Key: delivery.SourceMainKey, Value: v, At: t0, Revision: rev, History: true} } taken := func(repository, merge string) Entry { key, _ := delivery.RepositoryKey(repository) v, err := delivery.EncodeTaken(delivery.Taken{Repository: repository, Merge: merge, At: t0}) if err != nil { t.Fatal(err) } return Entry{Bucket: delivery.TakenBucket, Key: key, Value: v, At: t0} } haveIn := func(node, module, fp string, phase delivery.Phase, health delivery.Health) Entry { e := haveFP(t, node, module, fp, health) v, _ := delivery.EncodeHave(delivery.Have{FP: fp, Phase: phase, Health: health}) e.Value = v return e } // web changed at A (fpOld) and at C (fpNew); mail first appeared at A. The source named A, B, then C as web's head. world := func(t *testing.T) []Entry { cur, _ := delivery.DecodeSource(src(12, commitC).Value) current := Entry{Bucket: delivery.SourceBucket, Key: delivery.SourceMainKey, Revision: 12, At: t0} current.Value, _ = delivery.EncodeSource(cur) return []Entry{src(10, commitA), src(11, commitB), src(12, commitC), current, mod("web", fpOld, commitA, 10, true), mod("web", fpNew, commitC, 12, true), mod("mail", fpMail, commitA, 0, true), mod("web", fpNew, commitC, 12, false), mod("mail", fpMail, commitA, 0, false), taken(web, commitC), taken(sdk, commitS), wantE(t, "ace", "web", delivery.Want{FP: fpNew}), haveFP(t, "ace", "web", fpNew, delivery.HealthOK), wantE(t, "g14", "web", delivery.Want{FP: fpNew}), haveFP(t, "g14", "web", fpNew, delivery.HealthOK), wantE(t, "ace", "mail", delivery.Want{FP: fpMail}), haveFP(t, "ace", "mail", fpMail, delivery.HealthOK)} } for _, tc := range []struct { name string then []Entry reached string // web's reached head; "" none holds string // words one of web's holds carries; "" none waiting string // words web's module web waits with; "" none notYet []string // the merges the reached head's event says are not yet on the machines refused bool // the bus refuses every write and event until the watches handed over (a start, issue 1126) }{ {name: "every node runs the newest build", reached: commitC}, {name: "a node lagging stops the head before the change it lacks", reached: commitB, holds: "web on g14", then: []Entry{haveFP(t, "g14", "web", fpOld, delivery.HealthOK)}, waiting: "not yet the"}, {name: "a want held back at an older build holds the head as a lag does", reached: commitB, holds: "web on g14 runs the build of", then: []Entry{wantE(t, "g14", "web", delivery.Want{FP: fpOld, Newest: fpNew, Rule: 2}), haveFP(t, "g14", "web", fpOld, delivery.HealthOK)}}, {name: "a failing health check is said and holds nothing", reached: commitC, waiting: "health check failed", then: []Entry{haveFP(t, "g14", "web", fpNew, delivery.HealthFailed)}}, {name: "a node with no running have holds no head back and its module's merges are not yet", reached: commitC, waiting: "not running", notYet: []string{commitA, commitC}, then: []Entry{haveIn("g14", "web", fpNew, delivery.PhaseApplying, delivery.HealthUnknown)}}, {name: "an unrelated module not running leaves the head reached (the reviewer's probe)", reached: commitC, notYet: []string{commitA}, then: []Entry{haveIn("ace", "mail", fpMail, delivery.PhaseFailed, delivery.HealthFailed)}}, {name: "a module no node wants holds nothing", reached: commitC, then: []Entry{wantE(t, "ace", "mail", delivery.Want{Removed: true})}}, {name: "the indexer not yet having taken the head in", reached: commitB, holds: "has not yet taken in", then: []Entry{taken(web, commitB)}}, {name: "a change whose source revision is not known yet holds no head the source named", reached: commitC, then: []Entry{mod("web", "src2:"+strings.Repeat("9", 64), commitX, 0, true), mod("web", "src2:"+strings.Repeat("9", 64), commitX, 0, false)}}, // 1126: the view started while the bus did not answer its writes; its reach and its event are made at the next change it // takes in, though the derivation is the same, so the state reach never keeps an older have than node_haves says. {name: "a write and an event refused at the start are made at the next change", reached: commitC, refused: true, then: []Entry{haveFP(t, "ace", "mail", fpMail, delivery.HealthOK)}}, } { t.Run(tc.name, func(t *testing.T) { m := New() var written []ReachWrite var said []DeliveredEvent refuse := tc.refused errRefused := errors.New("nats: no responders available for request") m.SetReachWriter(func(w ReachWrite) error { if refuse { return errRefused } written = append(written, w) return nil }) m.SetDeliveredEvent(func(e DeliveredEvent) error { if refuse { return errRefused } if e.Repository != "" { said = append(said, e) } return nil }) fill(m, world(t)...) if len(written) != 0 { t.Fatalf("written before every watch handed over: %v", written) } for _, b := range reachBuckets { m.Initial(b) } refuse = false fill(m, tc.then...) key, _ := delivery.RepositoryKey(web) r := m.Reach()[key] if r.Reached != tc.reached { t.Errorf("reached = %.8s, want %.8s (holds %v)", r.Reached, tc.reached, r.Holds) } if got := strings.Join(r.Holds, "; "); (tc.holds == "") != (got == "") || !strings.Contains(got, tc.holds) { t.Errorf("holds = %q, want words with %q", got, tc.holds) } if got := r.Modules["web"].Waiting; (tc.waiting == "") != (got == "") || !strings.Contains(got, tc.waiting) { t.Errorf("web waits with %q, want words with %q", got, tc.waiting) } // A library reaches the head the indexer took in, with no module. sk, _ := delivery.RepositoryKey(sdk) if lib := m.Reach()[sk]; lib.Reached != commitS || len(lib.Modules) != 0 { t.Errorf("the library's reach = %+v", lib) } // Each key is written once with its last value; a reached head is said once, for its repository alone. last := map[string]delivery.Reach{} for _, w := range written { v, err := delivery.DecodeReach(w.Value) if err != nil { t.Fatalf("written %s: %v", w.Key, err) } last[w.Key] = v } if !delivery.SameReach(last[key], r) { t.Errorf("the last value written is %+v, not the reach derived %+v", last[key], r) } if tc.reached != "" { var last DeliveredEvent for _, e := range said { if e.Repository == web { last = e } } if last.Repository == "" { t.Errorf("the reached head of %s was never said", web) } if strings.Join(last.NotYet, ",") != strings.Join(tc.notYet, ",") { t.Errorf("not yet = %v, want %v", last.NotYet, tc.notYet) } } { n := len(said) fill(m, tc.then...) if len(said) != n && tc.reached == commitC { t.Errorf("said again: %+v", said[n:]) } for _, e := range said { if len(e.Heads) != 1 || e.Group != "" || e.Revision == 0 { t.Errorf("a reached head said as %+v", e) } } } }) } }