diff --git a/modules/mesh-delivery-indexer/internal/index/branch_take_in.go b/modules/mesh-delivery-indexer/internal/index/branch_take_in.go index 0f274de0..b1df57d8 100644 --- a/modules/mesh-delivery-indexer/internal/index/branch_take_in.go +++ b/modules/mesh-delivery-indexer/internal/index/branch_take_in.go @@ -4,6 +4,7 @@ import ( "context" "errors" "maps" + "strings" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery" "git.novox.be/novox/mesh-sdk/go/process" @@ -51,5 +52,10 @@ func (ix *Indexer) ApplyBranchSource(ctx context.Context, branch string, a meshd if err != nil { return err } + // A cycle with a build edge no running version breaks is refused here as at the trunk's take-in (design 47 G §49, A-326). + if cycle := RefusedCycle(WithEdges(ix.withActs(ctx, entries))); cycle != nil { + ix.logf("catalog graph: the branch %s at source revision %d keeps no revision: its catalog graph holds a cycle with a build edge, which no running version breaks (design 47 G §49): %s; taken in again at its next source-moved", branch, a.Revision, strings.Join(cycle, ", ")) + return nil + } return ix.KeepBranchEntries(ctx, branch, a.Revision, entries) } diff --git a/modules/mesh-delivery-indexer/internal/index/branch_take_in_test.go b/modules/mesh-delivery-indexer/internal/index/branch_take_in_test.go index 253245a2..64786d5b 100644 --- a/modules/mesh-delivery-indexer/internal/index/branch_take_in_test.go +++ b/modules/mesh-delivery-indexer/internal/index/branch_take_in_test.go @@ -6,6 +6,7 @@ import ( "fmt" "maps" "slices" + "strings" "testing" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog" @@ -73,3 +74,40 @@ func TestABranchOtherThanTheFirstIsTakenInIntoItsOwnRevisionAndTheTrunkIsLeftAlo t.Errorf("the same heads said %d revisions (%v), want one", len(said), err) } } + +// Design 47 G §49, A-326: a branch take-in refuses a cycle with a build edge as the trunk's does: the branch keeps no revision +// at those heads and says why, naming every artifact on the cycle. +func TestABranchTakeInKeepsNoRevisionWithARefusedCycle(t *testing.T) { + ctx := context.Background() + mods := builderModules(t) + for i, m := range mods { + switch m.name { + case "avahi": // avahi is built on mesh-tools (kind 3) ... + mods[i].manifest = strings.Replace(catalogueManifest(t, "avahi"), `"build": {`, `"build": { + "on": [{"arg": "RUNTIME_BASE", "module": "mesh-tools", "artifact": "tools"}],`, 1) + case "mesh-tools": // ... and mesh-tools' artifact needs avahi's (kind 2): no build seat breaks the cycle. + mods[i].manifest = strings.Replace(m.manifest, `"name": "tools",`, `"name": "tools", "needs": ["avahi:tools"],`, 1) + } + } + ix, _, merged := newFormIndexerUnmerged(t, mods...) + ix.Revisions = newMemObjects(0) + ix.Graph = newMem() + var logs strings.Builder + ix.Logf = func(format string, args ...any) { fmt.Fprintf(&logs, format+"\n", args...) } + ix.Message = func(context.Context, string, string, int) ([]byte, error) { return json.Marshal(merged) } + var said []meshcatalog.CatalogRevisionEvent + ix.SayCatalogRevision = func(_ context.Context, e meshcatalog.CatalogRevisionEvent) error { + said = append(said, e) + return nil + } + answer := meshdelivery.SourceAnswer{Branch: "beta", Revision: 9, Repositories: []meshdelivery.SourceHead{{Repository: catalogueRepo, Head: merged.Merge, Trunk: "beta"}}} + if err := ix.ApplyBranchSource(ctx, "beta", answer); err != nil { + t.Fatal(err) + } + if _, _, err := ReadRevision(ctx, ix.Revisions, BranchObject("beta", 1)); err == nil || len(said) != 0 { + t.Errorf("the branch kept a revision with a refused cycle (said %+v)", said) + } + if want := "mesh-tools:tools, avahi:tools, mesh-tools:tools"; !strings.Contains(logs.String(), want) { + t.Errorf("the journal %q does not say the cycle %s", logs.String(), want) + } +}