package versions import ( "context" "encoding/json" "errors" "reflect" "strings" "sync" "testing" "time" ) // mem is a Store in memory; before runs once before the next Create, to stage a race. type mem struct { mu sync.Mutex vals map[string][]byte revs map[string]uint64 rev uint64 before func() } func newMem() *mem { return &mem{vals: map[string][]byte{}, revs: map[string]uint64{}} } func (m *mem) Get(_ context.Context, key string) ([]byte, uint64, bool, error) { m.mu.Lock() defer m.mu.Unlock() v, ok := m.vals[key] return v, m.revs[key], ok, nil } func (m *mem) Create(ctx context.Context, key string, value []byte) (uint64, error) { if f := m.before; f != nil { m.before = nil f() } m.mu.Lock() defer m.mu.Unlock() if _, ok := m.vals[key]; ok { return 0, ErrConflict } m.rev++ m.vals[key], m.revs[key] = value, m.rev return m.rev, nil } func (m *mem) Update(_ context.Context, key string, value []byte, revision uint64) (uint64, error) { m.mu.Lock() defer m.mu.Unlock() if m.revs[key] != revision { return 0, ErrConflict } m.rev++ m.vals[key], m.revs[key] = value, m.rev return m.rev, nil } func (m *mem) KeysOf(_ context.Context, filter string) ([]string, error) { m.mu.Lock() defer m.mu.Unlock() prefix := strings.TrimSuffix(filter, "*") var out []string for k := range m.vals { if strings.HasPrefix(k, prefix) && !strings.Contains(k[len(prefix):], ".") { out = append(out, k) } } return out, nil } func known(seat, tags, reqs []string) Floor { return Floor{Seat: seat, Tags: tags, Requirements: reqs} } // TestTheBuildersVersions holds design 51 *The versions*, *The floor* and *Tagged versions* (DLV-B1, DLV-B2, DLV-B3): // one table per rule, the unit cases of the builder's numbering. func TestTheBuildersVersions(t *testing.T) { t.Run("keys", func(t *testing.T) { names := []string{"@novox/mesh-sdk", "git.novox.be/novox/mesh-sdk/go", "git.novox.be/novox/mesh-sdk", "a=40b", "a@b"} seen := map[string]string{} for _, n := range names { k := Key(n, "0.1.14") if strings.Count(k, ".") != 1 { t.Errorf("Key(%q) = %q, want two tokens", n, k) } if o, dup := seen[k]; dup { t.Errorf("%q and %q escape alike: %q", n, o, k) } seen[k] = n if !strings.HasPrefix(k, strings.TrimSuffix(NameFilter(n), "*")) { t.Errorf("Key(%q) = %q is not under NameFilter %q", n, k, NameFilter(n)) } } }) t.Run("floor", func(t *testing.T) { for _, c := range []struct { name string floor Floor want string }{ {"none starts at 0", known(nil, nil, nil), "0.1.0"}, {"the Go tag of today gives v0.1.14", known(nil, []string{"go/v0.1.13"}, []string{"v0.1.14-0.20261103221822-85ba0a9a70d8"}), "0.1.14"}, {"npm's 0.1.17 on the seat gives 0.1.18", known([]string{"0.1.17", "0.1.3"}, nil, nil), "0.1.18"}, {"another series and a tagged version count nothing", known([]string{"0.2.40", "0.1.9-feat-x.1"}, nil, nil), "0.1.0"}, {"the highest of the three", known([]string{"0.1.4"}, []string{"v0.1.9"}, []string{"v0.1.7"}), "0.1.10"}, } { if got := Next(nil, "0.1", c.floor); got != c.want { t.Errorf("%s: Next = %s, want %s", c.name, got, c.want) } } if got := Next([]Entry{{Version: "0.1.20", Series: "0.1"}, {Version: "0.1.21-x.1", Series: "0.1", Tag: "x"}}, "0.1", known([]string{"0.1.3"}, nil, nil)); got != "0.1.21" { t.Errorf("Next above the state's own = %s, want 0.1.21", got) } }) t.Run("decide", func(t *testing.T) { pub := Entry{Name: "n", Kind: KindPackage, Series: "0.1", Version: "0.1.5", Revision: "r1", Fingerprint: "fA", State: Published} ref := pub ref.State, ref.Version, ref.Revision = Refused, "0.1.6", "r2" img := Entry{Name: "i", Kind: KindBundle, Series: "0.1", Version: "0.1.2", Fingerprint: "fB", State: Published} silent := Floor{Silent: []string{SourceSeat}} for _, c := range []struct { name string entries []Entry kind, fp, rev string floor Floor version string keep, publish, unnum bool wait bool }{ {"a package's revision held published keeps its version and publishes nothing", []Entry{pub}, KindPackage, "fX", "r1", silent, "0.1.5", true, false, false, false}, {"a revision numbered but refused is published by its second build under the same version", []Entry{pub, ref}, KindPackage, "fY", "r2", known(nil, nil, nil), "0.1.6", true, true, false, false}, {"a new revision takes the next", []Entry{pub, ref}, KindPackage, "fZ", "r3", known(nil, nil, nil), "0.1.7", false, true, false, false}, {"a bundle's fingerprint keeps its number", []Entry{img}, KindBundle, "fB", "", silent, "0.1.2", true, false, false, false}, {"an unknown floor builds a bundle unnumbered", nil, KindBundle, "fC", "", silent, "", false, true, true, false}, {"an unknown floor builds an image unnumbered", nil, KindImage, "fC", "", silent, "", false, true, true, false}, {"an unknown floor makes a package wait", nil, KindPackage, "fC", "r9", silent, "", false, false, false, true}, } { d := Decide(c.entries, c.kind, "0.1", c.fp, c.rev, c.floor) if d.Version != c.version || d.Keep != c.keep || d.Publish != c.publish || d.Unnumbered != c.unnum || d.Wait != c.wait || d.Why == "" { t.Errorf("%s: Decide = %+v", c.name, d) } } }) t.Run("two claims never one number", func(t *testing.T) { st := newMem() v := Versions{Store: st, Now: func() time.Time { return time.Unix(0, 0) }} ctx := context.Background() st.before = func() { // a second claim takes 0.1.0 between the first's read and its create if _, _, err := (Versions{Store: st}).Number(ctx, Claim{Name: "n", Kind: KindBundle, Fingerprint: "f2"}); err != nil { t.Error(err) } } d, e, err := v.Number(ctx, Claim{Name: "n", Kind: KindBundle, Fingerprint: "f1"}) if err != nil || d.Version != "0.1.1" || e.State != Numbered || e.Series != DefaultSeries { t.Fatalf("Number = %+v, %+v, %v, want 0.1.1 after the other claim's 0.1.0", d, e, err) } if d, _, _ := v.Number(ctx, Claim{Name: "n", Kind: KindBundle, Fingerprint: "f1"}); !d.Keep || d.Version != "0.1.1" { t.Errorf("a numbered fingerprint claimed again = %+v, want it kept", d) } got, err := v.Mark(ctx, "n", "0.1.1", func(e *Entry) { e.State = Published }) if err != nil || got.State != Published { t.Errorf("Mark = %+v, %v", got, err) } if _, err := v.Mark(ctx, "n", "0.9.9", func(*Entry) {}); err == nil { t.Error("marking a version never numbered is no error") } }) t.Run("recorded", func(t *testing.T) { st := newMem() v := Versions{Store: st} ctx := context.Background() if _, _, err := v.Number(ctx, Claim{Name: "n", Kind: KindImage, Fingerprint: "f", Digest: "sha256:aa", Floor: known(nil, nil, nil)}); err != nil { t.Fatal(err) } for _, c := range []struct { version, digest string ok bool }{{"0.1.0", "sha256:aa", true}, {"0.1.0", "sha256:bb", false}, {"0.1.7", "sha256:aa", false}} { if _, ok, why, err := v.Recorded(ctx, "n", c.version, c.digest); err != nil || ok != c.ok || (!ok && why == "") { t.Errorf("Recorded(%s, %s) = %v, %q, %v; want %v", c.version, c.digest, ok, why, err, c.ok) } } // builder_versions answers the same through the tools bundle's read-only view. r := Reader{Keys: func() ([]string, error) { var all []string for k := range st.vals { all = append(all, k) } return all, nil }, Get: func(k string) (json.RawMessage, bool, error) { v, _, ok, err := st.Get(ctx, k) return v, ok, err }} a, err := AnswerVersions(ctx, r, "n", "0.1.0", "sha256:bb") if err != nil || len(a.Versions) != 1 || a.Recorded == nil || *a.Recorded || a.Why == "" { t.Errorf("builder_versions = %+v, %v; want one version and not recorded for another digest", a, err) } if _, err := AnswerVersions(ctx, r, " ", "", ""); err == nil { t.Error("builder_versions with no name answered") } }) t.Run("tagged", func(t *testing.T) { es := []Entry{{Version: "0.1.14", Series: "0.1"}, {Version: "0.1.15-feat-x.1", Series: "0.1", Tag: "feat-x"}} if got := Tagged(es, "0.1", "feat/X", known(nil, nil, nil)); got != "0.1.15-feat-x.2" { t.Errorf("Tagged = %s, want 0.1.15-feat-x.2", got) } if got := Tagged(nil, "0.1", "feat-x", known(nil, nil, nil)); got != "0.1.0-feat-x.1" { t.Errorf("a package new in a group = %s, want 0.1.0-feat-x.1", got) } if got := CleanTag("--Fix/รค_1965--"); got != "fix-1965" { t.Errorf("CleanTag = %s", got) } }) t.Run("merge order", func(t *testing.T) { es := []Entry{{Fingerprint: "f1", Version: "0.1.0", Series: "0.1"}} for _, c := range []struct { name string merge uint64 held map[string]uint64 want uint64 }{ {"an earlier merge numbered waits on nothing", 5, map[string]uint64{"f1": 3}, 0}, {"an earlier merge not numbered is waited on", 5, map[string]uint64{"f1": 3, "f2": 4}, 4}, {"a replaced fingerprint the indexer no longer holds is skipped", 5, map[string]uint64{}, 0}, {"a later merge is not waited on", 5, map[string]uint64{"f3": 7}, 0}, } { if got := WaitsOn(es, c.merge, c.held); got != c.want { t.Errorf("%s: WaitsOn = %d, want %d", c.name, got, c.want) } } }) t.Run("the floor wakes on its silent source, never a timer", func(t *testing.T) { ctx := context.Background() src := FloorSources{ Seat: func(context.Context, string, string) ([]string, error) { return []string{"0.1.3"}, nil }, Tags: func(context.Context, string, string) ([]string, error) { return nil, errors.New("no answer") }, Requirements: func(context.Context, string) ([]string, error) { return nil, nil }, } f, why := src.Ask(ctx, FloorOf{Seat: "oci-artifact-registry", Name: "m/program"}) if f.Known() || !reflect.DeepEqual(f.Silent, []string{SourceTags}) || why[SourceTags] != "no answer" { t.Fatalf("Ask = %+v, %v; want bridge_tags silent", f, why) } var fw FloorWaits w := fw.Wait("m/program", "oci-artifact-registry", "distribution", f, why, time.Unix(0, 0)) if !strings.Contains(w.Finding(), "bridge_tags (no answer)") { t.Errorf("finding = %q", w.Finding()) } for _, c := range []struct { kind, seat, module, name string want []string }{ {WakePublished, "oci-artifact-registry", "", "", nil}, // the seat answered already: a publish wakes nothing {WakeAccepted, "", "", "", nil}, {WakeForge, "", "", "", []string{"m/program"}}, {WakeCleared, "", "mesh-delivery-forge-bridge", "", []string{"m/program"}}, // recovered with nothing to say {WakeCleared, "", "mesh-delivery-indexer", "", nil}, {WakeByHand, "", "", "m/program", []string{"m/program"}}, {WakeByHand, "", "", "other", nil}, } { if got := fw.Wake(c.kind, c.seat, c.module, c.name); !reflect.DeepEqual(got, c.want) { t.Errorf("Wake(%s %s %s %s) = %v, want %v", c.kind, c.seat, c.module, c.name, got, c.want) } } seatDown := Floor{Silent: []string{SourceSeat}} fw.Wait("distribution/program", "container-image-registry", "distribution", seatDown, nil, time.Unix(0, 0)) if got := fw.Wake(WakePublished, "container-image-registry", "", ""); !reflect.DeepEqual(got, []string{"distribution/program"}) { t.Errorf("the holder's published woke %v, want its own image's name", got) } if got := fw.Wake(WakeCleared, "", "distribution", ""); !reflect.DeepEqual(got, []string{"distribution/program"}) { t.Errorf("the holder's clear woke %v", got) } if !fw.Answered("m/program") || fw.Answered("m/program") || len(fw.Waiting()) != 1 { t.Error("Answered drops the wait once") } }) t.Run("old form", func(t *testing.T) { forms := []string{"ref:artifact-store", "ref:registry"} base := []string{"ref:artifact-store"} both := []string{"ref:artifact-store", "ref:registry"} for _, c := range []struct { name string nodes []NodeReads dropped map[string]bool want FormsAnswer }{ {"one present node lacking it keeps the old form alone", []NodeReads{{Node: "a", Present: true, Serves: true, Stated: both}, {Node: "b", Present: true, Serves: true, Stated: base}}, nil, FormsAnswer{Single: "ref:artifact-store", Open: true, Holding: []string{"b"}}}, {"a node not serving manifest-reads counts as the baseline", []NodeReads{{Node: "a", Present: true, Serves: true, Stated: both}, {Node: "b", Present: true, Stated: both}}, nil, FormsAnswer{Single: "ref:artifact-store", Open: true, Holding: []string{"b"}}}, {"every present node states it: the new form beside the old, an away old node keeping the old in the single reference", []NodeReads{{Node: "a", Present: true, Serves: true, Stated: both}, {Node: "c", Serves: true, Stated: base}}, nil, FormsAnswer{Single: "ref:artifact-store", Beside: []string{"ref:registry"}, Open: true, Holding: []string{"c"}}}, {"the away node back with the new engine closes the window", []NodeReads{{Node: "a", Present: true, Serves: true, Stated: both}, {Node: "c", Present: true, Serves: true, Stated: []string{"ref:registry"}}}, nil, FormsAnswer{Single: "ref:registry"}}, {"a retired node dropped by hand counts no more", []NodeReads{{Node: "a", Present: true, Serves: true, Stated: []string{"ref:registry"}}, {Node: "c", Serves: true, Stated: base}}, map[string]bool{"c": true}, FormsAnswer{Single: "ref:registry"}}, } { if got := Forms(forms, base, c.nodes, c.dropped); !reflect.DeepEqual(got, c.want) { t.Errorf("%s: Forms = %+v, want %+v", c.name, got, c.want) } } t0 := time.Unix(0, 0) w, closed, overdue := OldFormWindow{}.Step(FormsAnswer{Open: true, Holding: []string{"c"}}, t0) if closed || len(overdue) != 0 || !w.Open { t.Fatalf("Step = %+v %v %v", w, closed, overdue) } w, _, overdue = w.Step(FormsAnswer{Open: true, Holding: []string{"c"}}, t0.Add(FindingAfter)) if !reflect.DeepEqual(overdue, []string{"c"}) { t.Errorf("seven days held: overdue %v, want c", overdue) } w, closed, _ = w.Step(FormsAnswer{}, t0.Add(FindingAfter+time.Hour)) if !closed || w.Open || !reflect.DeepEqual(w.LastHolding, []string{"c"}) { t.Errorf("closing: %+v closed=%v", w, closed) } if _, again, _ := w.Step(FormsAnswer{}, t0.Add(FindingAfter+2*time.Hour)); again { t.Error("a closed window closes again: old-form-closed would be said twice") } }) t.Run("at the claim", func(t *testing.T) { ctx := context.Background() tagsUp := false var asked []string src := FloorSources{ Seat: func(context.Context, string, string) ([]string, error) { return []string{"0.1.3"}, nil }, Tags: func(_ context.Context, repo, prefix string) ([]string, error) { asked = append(asked, repo+" "+prefix) if !tagsUp { return nil, errors.New("no answer") } return []string{"go/v0.1.13"}, nil }, Requirements: func(context.Context, string) ([]string, error) { return nil, nil }, } var said []NumberedBody fw := &FloorWaits{} n := &Numberer{Versions: Versions{Store: newMem()}, Floor: src, Waits: fw, Said: func(b NumberedBody) { said = append(said, b) }} c := BuildClaim{Module: "m", Path: "go", Repository: "novox/m", Fingerprint: "fp1", Commit: "c1", Revision: "7", Merge: 7, Artifacts: []ClaimArtifact{{Artifact: "program", Kind: KindBundle, Language: "go"}, {Artifact: "base", Kind: "upstream"}}} got, err := n.Number(ctx, c) if err != nil || len(got.Versions) != 0 || !reflect.DeepEqual(got.Unnumbered, []string{"program"}) || got.Wait != "" { t.Fatalf("silent floor: %+v, %v; want program unnumbered, the upstream copy left out", got, err) } if !reflect.DeepEqual(asked, []string{"novox/m go/v"}) || len(fw.Waiting()) != 1 || len(said) != 0 { t.Fatalf("asked %v, waits %+v, said %v", asked, fw.Waiting(), said) } pkg := c pkg.Artifacts = []ClaimArtifact{{Artifact: "lib", Kind: KindPackage, Language: "go"}} if got, _ := n.Number(ctx, pkg); got.Wait == "" || len(got.Versions) != 0 { t.Fatalf("a package with no floor = %+v; want it to wait", got) } tagsUp = true got, err = n.Number(ctx, c) if err != nil || got.Versions["program"] != "0.1.14" || len(said) != 1 || said[0].Version != "0.1.14" || len(fw.Waiting()) != 1 { t.Fatalf("floor answered: %+v, %v, said %v, waits %+v; want 0.1.14 said once, lib still waiting", got, err, said, fw.Waiting()) } if err := n.Built(ctx, c, got, map[string]string{"program": "sha256:aa"}); err != nil { t.Fatal(err) } again, _ := n.Number(ctx, c) if again.Versions["program"] != "0.1.14" || len(said) != 1 || len(asked) != 3 { t.Fatalf("again: %+v, said %d, asked %v; want the number kept, nothing said, the floor not asked", again, len(said), asked) } if _, ok, why, _ := n.Versions.Recorded(ctx, "m/program", "0.1.14", "sha256:bb"); ok { t.Fatalf("another digest recorded: %s", why) } // DLV-B4: the seat's events move the entry; a published wakes the names waiting on that seat. if wake, err := n.Heard(ctx, SeatArtifacts, HeardPublished, "e1", Outcome{Name: "m/other", Digest: "sha256:cc"}); err != nil || len(wake) != 0 { t.Fatalf("a by-digest publish of another seat's wait: %v, %v", wake, err) } n.Waits.Wait("m/lib", SeatGoPackages, "", Floor{Silent: []string{SourceSeat}}, nil, time.Unix(0, 0)) if wake, _ := n.Heard(ctx, SeatGoPackages, HeardPublished, "e2", Outcome{Name: "m/x", Digest: "sha256:dd"}); !reflect.DeepEqual(wake, []string{"m/lib"}) { t.Fatalf("a published on the Go seat woke %v, want m/lib, whose seat was silent", wake) } for _, h := range []struct{ event, state, reason string }{{HeardRefused, Refused, "bytes differ"}, {HeardPublished, Published, ""}, {HeardRefused, Published, ""}, {HeardRemoved, Removed, "retention"}} { if _, err := n.Heard(ctx, SeatArtifacts, h.event, "e3", Outcome{Name: "m/program", Version: "0.1.14", Digest: "sha256:aa", Reason: h.reason}); err != nil { t.Fatal(err) } all, _ := n.Versions.Of(ctx, "m/program") if all[0].State != h.state { t.Fatalf("after %s: %+v, want %s", h.event, all[0], h.state) } } if _, err := n.Heard(ctx, SeatArtifacts, HeardPublished, "e4", Outcome{Name: "m/none", Version: "0.1.0"}); err == nil { t.Fatal("a published of a version the builder never numbered is not said") } }) t.Run("fill", func(t *testing.T) { if _, full := Fill(Cap*79/100, Cap); full { t.Error("79 % is full") } if _, full := Fill((Cap*80+99)/100, Cap); !full { t.Error("80 % is not full") } }) }