A merge's group member counted as merged when its merge waited in any batch, but kinds never share a batch: the later member got a batch of its own, cut at once, and walked before the earlier member's batch was cut, breaking the group's order (ADR 0276 §5). Only a member kept in a batch of the merge's own kind counts as merged now; one kept in the other kind's keeps the merge waiting, and its batch names that member, so the two are cut in the order their windows close (review of #250).
399 lines
18 KiB
Go
399 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// The tests of novox/hq issue 525's window skip: a merge waits for the merge window only when a batch assembling
|
|
// holds a merge related to it, or its delivery group may have a member still to merge (ADR 0304 decision 1, ADR
|
|
// 0306 decisions 2 and 3). Walks stay one at a time.
|
|
|
|
// A merge from no head that shares nothing, with no walk open, is cut into its walk when it is heard: no window.
|
|
func TestALoneMergeSharingNothingIsCutAtOnce(t *testing.T) {
|
|
open := windowed(t)
|
|
asksWithPaths(t)
|
|
hear(t, open, lone(repoMerge("one", "c1", t0)), t0)
|
|
ws, bs := walks(t, open)
|
|
if len(ws) != 1 || len(bs) != 0 || !ws[0].Open() || ws[0].Commit != "c1" {
|
|
t.Fatalf("a lone merge sharing nothing: %d walk(s) %+v, %d batch(es)", len(ws), ws, len(bs))
|
|
}
|
|
if ws[0].Times == nil || ws[0].Times.WindowClosed == nil || !ws[0].Times.WindowClosed.Equal(t0) {
|
|
t.Fatalf("its window closed at %+v; want when it was heard, %s", ws[0].Times, t0)
|
|
}
|
|
}
|
|
|
|
// A merge related to the batch assembling joins it and waits for the window, which its coming restarts: by its
|
|
// repository, by a module in common (a dependent of what the batch moves), by an edge of the module graph between
|
|
// their modules (one that widens nothing), or by its delivery group's head.
|
|
func TestARelatedMergeJoinsTheBatchAndWaitsForTheWindow(t *testing.T) {
|
|
ctx := t.Context()
|
|
for _, c := range []string{"same repository", "a module in common", "an edge of the graph", "the same group head"} {
|
|
t.Run(c, func(t *testing.T) {
|
|
open := windowed(t)
|
|
groupsWaiting(t, "pr/c1")
|
|
asksWithPaths(t)
|
|
inv := open.inventory
|
|
first := repoMerge("one", "c1", t0)
|
|
second := lone(repoMerge("two", "c2", t0.Add(10*time.Second)))
|
|
why := map[string]string{"same repository": "both change novox/one", "a module in common": "both move two",
|
|
"an edge of the graph": "two depends on builder (built-by)",
|
|
"the same group head": "both are members of group " + first.Head}[c]
|
|
switch c {
|
|
case "same repository":
|
|
second = lone(repoMerge("one", "c2", t0.Add(10*time.Second)))
|
|
case "a module in common":
|
|
// two is built on one: a merge of one moves two too.
|
|
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "two", Version: "1",
|
|
Build: &catalogue.Build{On: []catalogue.BuildsOn{{Arg: "BASE", Module: "one"}}}},
|
|
inventory.Source{Repository: "novox/two", Seat: "git", Ref: "main", BuiltFrom: "c0", Head: "c0"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case "an edge of the graph":
|
|
// two is built by the build machine: an edge that orders and widens nothing.
|
|
if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: "builder", Version: "1",
|
|
Claims: []catalogue.Claim{{Name: "mesh-build-machine", Scope: catalogue.ScopeMesh}}},
|
|
inventory.Source{Repository: "novox/builder", Seat: "git", Ref: "main", BuiltFrom: "c0", Head: "c0"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
first = repoMerge("builder", "c1", t0)
|
|
case "the same group head":
|
|
second = fromHead(repoMerge("two", "c2", t0.Add(10*time.Second)), first.Head)
|
|
}
|
|
hear(t, open, first, t0)
|
|
said := captured(t, func() error {
|
|
return (following{open: open}).hearMerge(ctx, second, t0.Add(10*time.Second))
|
|
})
|
|
if !strings.Contains(said, "("+why+")") {
|
|
t.Fatalf("the merge was not said related by %q: %s", why, said)
|
|
}
|
|
ws, bs := walks(t, open)
|
|
if len(ws) != 0 || len(bs) != 1 || len(bs[0].Delivery.Merges) != 2 {
|
|
t.Fatalf("the related merge: %d walk(s), batches %+v", len(ws), bs)
|
|
}
|
|
if bs[0].Delivery.Batch.CutAtOnce {
|
|
t.Fatal("a batch a related merge joined is cut at once")
|
|
}
|
|
cutAt(t, open, t0.Add(99*time.Second))
|
|
if ws, _ := walks(t, open); len(ws) != 0 {
|
|
t.Fatalf("cut before the window the related merge restarted closed: %+v", ws)
|
|
}
|
|
cutAt(t, open, t0.Add(100*time.Second))
|
|
ws, bs = walks(t, open)
|
|
if len(ws) != 1 || len(bs) != 0 || len(ws[0].Delivery.Merges) != 2 {
|
|
t.Fatalf("after the window: %d walk(s), %d batch(es)", len(ws), len(bs))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A merge from a head whose delivery group has a member still to merge waits for the window, even with no batch
|
|
// assembling, and says why; its sibling joins the same batch by their head (ADR 0306 decision 2). A merge from a
|
|
// head that names no group shares nothing and is cut at once, as one from no head is.
|
|
func TestAMergeOfAGroupWaitsEvenAlone(t *testing.T) {
|
|
open := windowed(t)
|
|
asksWithPaths(t)
|
|
groupsWaiting(t, "feat/x")
|
|
hear(t, open, fromHead(repoMerge("one", "c1", t0), "feat/x"), t0)
|
|
ws, bs := walks(t, open)
|
|
if len(ws) != 0 || len(bs) != 1 || bs[0].Delivery.Batch.CutAtOnce || windowClosed(bs[0], t0) {
|
|
t.Fatalf("a group's merge alone: %d walk(s), batches %+v", len(ws), bs)
|
|
}
|
|
if words := batchWords(bs[0], t0); !strings.HasPrefix(words, "assembling: 90 s left") ||
|
|
!strings.Contains(words, "group feat/x has a member still to merge: novox/elsewhere (ready)") {
|
|
t.Fatalf("it reads %q", words)
|
|
}
|
|
hear(t, open, fromHead(repoMerge("two", "c2", t0.Add(5*time.Second)), "feat/x"), t0.Add(5*time.Second))
|
|
if _, bs := walks(t, open); len(bs) != 1 || len(bs[0].Delivery.Merges) != 2 {
|
|
t.Fatalf("the group's second member did not join the first: %+v", bs)
|
|
}
|
|
cutAt(t, open, t0.Add(94*time.Second))
|
|
if ws, _ := walks(t, open); len(ws) != 0 {
|
|
t.Fatalf("cut before its window closed: %+v", ws)
|
|
}
|
|
cutAt(t, open, t0.Add(95*time.Second))
|
|
ws, bs = walks(t, open)
|
|
if len(ws) != 1 || len(bs) != 0 || ws[0].CommitOf("novox/one") != "c1" || ws[0].CommitOf("novox/two") != "c2" {
|
|
t.Fatalf("at its window: %d walk(s) %+v, %d batch(es)", len(ws), ws, len(bs))
|
|
}
|
|
}
|
|
|
|
// A merge from a head that names no group, sharing nothing, with no walk open, is cut at once.
|
|
func TestAHeadedMergeInNoGroupIsCutAtOnce(t *testing.T) {
|
|
open := windowed(t)
|
|
asksWithPaths(t)
|
|
hear(t, open, fromHead(repoMerge("one", "c1", t0), "fix/alone"), t0)
|
|
if ws, bs := walks(t, open); len(ws) != 1 || len(bs) != 0 || !ws[0].Open() {
|
|
t.Fatalf("a headed merge in no group: %d walk(s), %d batch(es)", len(ws), len(bs))
|
|
}
|
|
}
|
|
|
|
// When mesh-delivery cannot say a head's group, the merge waits for the window, the safe side, and its batch says
|
|
// why.
|
|
func TestAGroupNobodyCanSayWaits(t *testing.T) {
|
|
open := windowed(t)
|
|
asksWithPaths(t)
|
|
was := deliveryGroupOf
|
|
deliveryGroupOf = func(context.Context, bool, string) ([]groupMember, bool, error) {
|
|
return nil, false, errors.New("nothing serves mesh-delivery.groups")
|
|
}
|
|
t.Cleanup(func() { deliveryGroupOf = was })
|
|
hear(t, open, fromHead(repoMerge("one", "c1", t0), "feat/x"), t0)
|
|
ws, bs := walks(t, open)
|
|
if len(ws) != 0 || len(bs) != 1 || bs[0].Delivery.Batch.CutAtOnce {
|
|
t.Fatalf("a merge whose group nobody could say: %d walk(s), batches %+v", len(ws), bs)
|
|
}
|
|
if words := batchWords(bs[0], t0); !strings.Contains(words, "mesh-delivery did not say whether group feat/x has a "+
|
|
"member still to merge (nothing serves mesh-delivery.groups): it waits for the window, the safe side") {
|
|
t.Fatalf("it reads %q", words)
|
|
}
|
|
}
|
|
|
|
// A member counts as still to merge only while its pull request is open, and not once this controller keeps its
|
|
// merge: a group whose other members are merged or closed makes nobody wait.
|
|
func TestAGroupsMergedOrClosedMembersMakeNobodyWait(t *testing.T) {
|
|
open := windowed(t)
|
|
asksWithPaths(t)
|
|
ctx := t.Context()
|
|
members := []groupMember{{Repository: "novox/one", State: "ready"}, {Repository: "novox/two", State: "published"},
|
|
{Repository: "novox/three", State: "stopped"}}
|
|
was := deliveryGroupOf
|
|
deliveryGroupOf = func(context.Context, bool, string) ([]groupMember, bool, error) { return members, true, nil }
|
|
t.Cleanup(func() { deliveryGroupOf = was })
|
|
m := fromHead(repoMerge("one", "c1", t0), "feat/x")
|
|
if why, err := groupWait(ctx, open.inventory, m, true, false); err != nil || why != "" {
|
|
t.Fatalf("the group's other members are merged or closed, yet: %q %v", why, err)
|
|
}
|
|
members = append(members, groupMember{Repository: "novox/four", State: "checked"})
|
|
if why, _ := groupWait(ctx, open.inventory, m, true, false); !strings.Contains(why, "novox/four (checked)") {
|
|
t.Fatalf("an open member: %q", why)
|
|
}
|
|
// novox/four's merge is kept in a batch already: merged, whatever mesh-delivery says yet.
|
|
hear(t, open, fromHead(repoMerge("two", "c4", t0), "feat/x"), t0)
|
|
members[3].Repository = "novox/two"
|
|
if why, _ := groupWait(ctx, open.inventory, m, true, false); why != "" {
|
|
t.Fatalf("a member this controller keeps the merge of still waits: %q", why)
|
|
}
|
|
if got := memberOf("novox/mesh-sdk@d41d255f28ad (held)"); got != (groupMember{Repository: "novox/mesh-sdk", State: "held"}) {
|
|
t.Fatalf("a member reads %+v", got)
|
|
}
|
|
}
|
|
|
|
// A merge related to two batches assembling folds the newer into the older, which answers every merge of both
|
|
// and names nothing twice; the folded one names the batch that took it over.
|
|
func TestAMergeRelatedToTwoBatchesFoldsThem(t *testing.T) {
|
|
open := windowed(t)
|
|
groupsWaiting(t, "feat/a", "feat/b")
|
|
asksWithPaths(t)
|
|
ctx := t.Context()
|
|
hear(t, open, fromHead(repoMerge("one", "c1", t0), "feat/a"), t0)
|
|
hear(t, open, fromHead(repoMerge("two", "c2", t0.Add(5*time.Second)), "feat/b"), t0.Add(5*time.Second))
|
|
_, bs := walks(t, open)
|
|
if len(bs) != 2 {
|
|
t.Fatalf("two merges sharing nothing: %d batch(es)", len(bs))
|
|
}
|
|
older, newer := bs[1], bs[0] // newest first
|
|
// Of one's repository and from two's head: related to both.
|
|
hear(t, open, fromHead(repoMerge("one", "c3", t0.Add(10*time.Second)), "feat/b"), t0.Add(10*time.Second))
|
|
_, bs = walks(t, open)
|
|
if len(bs) != 1 || bs[0].ID != older.ID || len(bs[0].Delivery.Merges) != 3 {
|
|
t.Fatalf("after a merge related to both: batches %+v; want %s answering three merges", bs, older.ID)
|
|
}
|
|
folded, err := open.inventory.PlanByID(ctx, newer.ID)
|
|
if err != nil || folded.State != inventory.PlanSuperseded || folded.Delivery == nil ||
|
|
folded.Delivery.TakenOverBy != older.ID {
|
|
t.Fatalf("the newer batch reads %s %+v (%v)", folded.State, folded.Delivery, err)
|
|
}
|
|
if left, _ := open.inventory.MergesOf(ctx, newer.ID); len(left) != 0 {
|
|
t.Fatalf("the folded batch still answers %+v", left)
|
|
}
|
|
cutAt(t, open, t0.Add(100*time.Second))
|
|
ws, bs := walks(t, open)
|
|
var cut []inventory.Plan
|
|
for _, w := range ws {
|
|
if w.State != inventory.PlanSuperseded {
|
|
cut = append(cut, w)
|
|
}
|
|
}
|
|
if len(cut) != 1 || len(bs) != 0 || cut[0].ID != older.ID || cut[0].CommitOf("novox/one") != "c3" ||
|
|
cut[0].CommitOf("novox/two") != "c2" {
|
|
t.Fatalf("the folded batch's walk: %+v, %d batch(es)", cut, len(bs))
|
|
}
|
|
}
|
|
|
|
// With a walk open, a batch cut at once still waits for it, said queued behind it (walks stay one at a time, ADR
|
|
// 0276 decision 4); it is cut once that walk ended, with no window to wait for.
|
|
func TestACutAtOnceBatchQueuesBehindAnOpenWalk(t *testing.T) {
|
|
open := windowed(t)
|
|
groupsWaiting(t, "pr/c1")
|
|
asksWithPaths(t)
|
|
hear(t, open, repoMerge("one", "c1", t0), t0)
|
|
cutAt(t, open, t0.Add(90*time.Second))
|
|
ws, _ := walks(t, open)
|
|
if len(ws) != 1 || !ws[0].Open() {
|
|
t.Fatalf("no walk open: %+v", ws)
|
|
}
|
|
first := ws[0]
|
|
heard := t0.Add(100 * time.Second)
|
|
hear(t, open, lone(repoMerge("two", "c2", heard)), heard)
|
|
ws, bs := walks(t, open)
|
|
if len(ws) != 1 || len(bs) != 1 || bs[0].State != inventory.PlanQueued || bs[0].Delivery.Batch.Behind != first.ID ||
|
|
!bs[0].Delivery.Batch.CutAtOnce {
|
|
t.Fatalf("a lone merge during a walk: %d walk(s), batches %+v", len(ws), bs)
|
|
}
|
|
if !strings.HasPrefix(batchWords(bs[0], heard), "queued behind "+first.ID) {
|
|
t.Fatalf("it reads %q", batchWords(bs[0], heard))
|
|
}
|
|
first.State = inventory.PlanDone
|
|
if err := open.inventory.SavePlan(t.Context(), &first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cutAt(t, open, heard.Add(time.Second))
|
|
ws, bs = walks(t, open)
|
|
if len(ws) != 2 || len(bs) != 0 || ws[0].Commit != "c2" {
|
|
t.Fatalf("the queued batch was not cut once the walk ended: %+v %+v", ws, bs)
|
|
}
|
|
if ws[0].Times == nil || ws[0].Times.WindowClosed == nil || !ws[0].Times.WindowClosed.Equal(heard) {
|
|
t.Fatalf("its window closed at %+v; want when its merge was heard, %s", ws[0].Times, heard)
|
|
}
|
|
}
|
|
|
|
// A stopped walk's other merges are walked again in a batch they are related to, and a batch of their own when
|
|
// they share nothing with the one assembling (ADR 0299, ADR 0304 decision 1): never in a batch of another
|
|
// change, whose window the stop would close.
|
|
func TestAStopWalksTheOthersAgainInABatchOfTheirOwnWhenTheyShareNothing(t *testing.T) {
|
|
open := windowed(t)
|
|
groupsWaiting(t, "pr/cat1aaaaaaaa")
|
|
endsItsLine(t)
|
|
asksRecorded(t)
|
|
ctx := t.Context()
|
|
cat := catalogueMerge("cat1aaaaaaaa", "app", t0)
|
|
one := fromHead(repoMerge("one", "one1bbbbbbbb", t0.Add(5*time.Second)), cat.Head)
|
|
hear(t, open, cat, t0.Add(time.Second))
|
|
hear(t, open, one, t0.Add(6*time.Second))
|
|
cutAt(t, open, t0.Add(2*time.Minute))
|
|
ws, _ := walks(t, open)
|
|
walk := ws[0]
|
|
hear(t, open, repoMerge("two", "two1cccccccc", t0.Add(3*time.Minute)), t0.Add(3*time.Minute))
|
|
_, bs := walks(t, open)
|
|
if len(bs) != 1 {
|
|
t.Fatalf("the merge heard while the walk ran is in %d batches", len(bs))
|
|
}
|
|
other := bs[0]
|
|
_, said, err := stopWalkWithout(ctx, open.inventory, walk.ID, "mesh-delivery for jochen", "its group stopped",
|
|
[]string{"novox/mesh-catalog@" + cat.Commit}, t0.Add(3*time.Minute+10*time.Second))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if said.WalkedAgainBy == "" || said.WalkedAgainBy == other.ID {
|
|
t.Fatalf("walked again by %q; want a batch of its own, not %s", said.WalkedAgainBy, other.ID)
|
|
}
|
|
b, _ := open.inventory.PlanByID(ctx, other.ID)
|
|
if b.Delivery.Batch == nil || b.Delivery.Batch.Closed || len(b.Delivery.Merges) != 1 {
|
|
t.Fatalf("the other change's batch reads %+v; want it untouched by the stop", b.Delivery)
|
|
}
|
|
}
|
|
|
|
// A catalogue batch's cut folds in every walk waiting for its delivery's word (cutBatch's carry), so what that
|
|
// walk moves is the batch's too: a lone merge related to the waiting walk joins the batch assembling and waits for
|
|
// its window, rather than being cut at once as sharing nothing with what the batch's own merges move.
|
|
func TestALoneMergeRelatedToAWalkTheCutFoldsJoinsTheBatch(t *testing.T) {
|
|
open := windowed(t)
|
|
groupsWaiting(t, "pr/c3")
|
|
asksWithPaths(t)
|
|
ctx := t.Context()
|
|
if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: "mesh-delivery", Version: "1",
|
|
Claims: []catalogue.Claim{{Name: catalogue.DeliverySeat, Scope: catalogue.ScopeMesh}}},
|
|
inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/mesh-delivery", Ref: "main",
|
|
BuiltFrom: "c0", Head: "c0"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := open.inventory.Assign(ctx, "anchor", "mesh-delivery"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
hear(t, open, repoMerge("one", "c1", t0), t0)
|
|
cutAt(t, open, t0.Add(90*time.Second))
|
|
if ws, _ := walks(t, open); len(ws) != 1 || !ws[0].Waiting() {
|
|
t.Fatalf("the walk does not wait for its word: %+v", ws)
|
|
}
|
|
hear(t, open, repoMerge("three", "c3", t0.Add(2*time.Minute)), t0.Add(2*time.Minute))
|
|
hear(t, open, lone(repoMerge("one", "c2", t0.Add(2*time.Minute+5*time.Second))), t0.Add(2*time.Minute+5*time.Second))
|
|
_, bs := walks(t, open)
|
|
if len(bs) != 1 || len(bs[0].Delivery.Merges) != 2 || bs[0].Delivery.Batch.CutAtOnce {
|
|
t.Fatalf("a lone merge related to the waiting walk: batches %+v", bs)
|
|
}
|
|
}
|
|
|
|
// A delivery group with a member of each kind (review of #250): a merge on the controller's own path and a
|
|
// catalogue merge never share a batch, so the later member cannot join the earlier one's. Kept in a batch of the
|
|
// other kind, the earlier member keeps the later one waiting, whatever mesh-delivery says yet — never cut at once
|
|
// to walk before it. The two batches are then cut in the order their windows close (cutBatchesHeld: the oldest
|
|
// closed batch first, an own-path one first at one look, and the other queued behind a walk that started): the
|
|
// earlier member's window closes first, so it walks first, in both orders.
|
|
func TestAGroupAcrossBothKindsKeepsItsOrder(t *testing.T) {
|
|
for _, ownFirst := range []bool{true, false} {
|
|
name := "catalogue member first"
|
|
if ownFirst {
|
|
name = "controller member first"
|
|
}
|
|
t.Run(name, func(t *testing.T) {
|
|
open := windowed(t)
|
|
asksWithPaths(t)
|
|
ctx := t.Context()
|
|
was := deliveryGroupOf
|
|
// mesh-delivery still says both members ready: it marks one merged only once its walk opened.
|
|
deliveryGroupOf = func(context.Context, bool, string) ([]groupMember, bool, error) {
|
|
return []groupMember{{Repository: "novox/mesh-controller", State: "ready"},
|
|
{Repository: "novox/mesh-catalog", State: "ready"}}, true, nil
|
|
}
|
|
t.Cleanup(func() { deliveryGroupOf = was })
|
|
own := fromHead(repoMerge("mesh-controller", "k1", t0), "feat/both-kinds")
|
|
cat := fromHead(catalogueMerge("cat1aaaaaaaa", "app", t0), "feat/both-kinds")
|
|
first, second := cat, own
|
|
if ownFirst {
|
|
first, second = own, cat
|
|
}
|
|
second.MergedAt = t0.Add(10 * time.Second).Format(time.RFC3339Nano)
|
|
hear(t, open, first, t0)
|
|
hear(t, open, second, t0.Add(10*time.Second))
|
|
ws, bs := walks(t, open)
|
|
if len(ws) != 0 || len(bs) != 2 {
|
|
t.Fatalf("both members heard: %d walk(s), %d batch(es); want two batches, nothing cut", len(ws), len(bs))
|
|
}
|
|
later := bs[0] // newest first
|
|
if later.Delivery.Batch.CutAtOnce || !strings.Contains(later.Delivery.Batch.Waits,
|
|
"has a member waiting in a batch of the other kind: novox/"+first.Repo+"@") {
|
|
t.Fatalf("the later member's batch reads %+v; want it waiting for the earlier member", later.Delivery.Batch)
|
|
}
|
|
// The earlier member's window closes first: its batch alone is cut.
|
|
cutAt(t, open, t0.Add(90*time.Second))
|
|
ws, bs = walks(t, open)
|
|
if len(ws) != 1 || !ws[0].Open() || ws[0].CommitOf("novox/"+first.Repo) != first.Commit || len(bs) != 1 ||
|
|
bs[0].ID != later.ID {
|
|
t.Fatalf("at the earlier window: walks %+v, batches %+v; want the earlier member alone cut", ws, bs)
|
|
}
|
|
earlier := ws[0]
|
|
// The later member's window closes while the earlier walk runs: it queues behind it.
|
|
cutAt(t, open, t0.Add(100*time.Second))
|
|
if _, bs := walks(t, open); len(bs) != 1 || bs[0].State != inventory.PlanQueued || bs[0].Delivery.Batch.Behind != earlier.ID {
|
|
t.Fatalf("the later member's batch does not queue behind the earlier walk: %+v", bs)
|
|
}
|
|
earlier.State = inventory.PlanDone
|
|
if err := open.inventory.SavePlan(ctx, &earlier); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cutAt(t, open, t0.Add(101*time.Second))
|
|
got, err := open.inventory.PlanByID(ctx, later.ID)
|
|
if err != nil || got.Batch() || got.CommitOf("novox/"+second.Repo) != second.Commit {
|
|
t.Fatalf("the later member was not walked after the earlier: %s %v", got.State, err)
|
|
}
|
|
})
|
|
}
|
|
}
|