diff --git a/modules/mesh-delivery-pipeline/internal/pipeline/ended_test.go b/modules/mesh-delivery-pipeline/internal/pipeline/ended_test.go new file mode 100644 index 00000000..765cb420 --- /dev/null +++ b/modules/mesh-delivery-pipeline/internal/pipeline/ended_test.go @@ -0,0 +1,60 @@ +package pipeline + +import ( + "strings" + "testing" + + seat "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshpipelinerunner" + nodebuildgo "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/nodebuildgo" +) + +// pipeline-ended carries the module's test counts and says it over budget past 500 unit or 50 integration tests (design 47 G +// §24, §419), and a group's one pipeline ends with group-ended under the group once every member ended (§73, §332). +func TestPipelineEndedCarriesTestCountsAndAGroupEndsOnItsMembers(t *testing.T) { + h, store, _, _, _, _ := fixture() + bodies := map[string]any{} + var order []string + h.Say = func(subject string, body any) error { + ev := subject[strings.Index(subject, ".event.")+7:] + bodies[ev] = body + order = append(order, ev) + return nil + } + e := store[pkey] + e.p.Steps = e.p.Steps[:2] + e.p.Steps[1].Status = seat.StepResultStatusRequired + e.p.Group = "g1" + store[pkey] = e + // A second member of the same group, on the same source revision, still running. + store["main.5.lib"] = struct { + p seat.Pipeline + rev uint64 + }{seat.Pipeline{V: 1, Pipeline: "main.5.lib", Branch: "main", Source: 5, Module: "lib", Commit: "c2", Group: "g1", Outcome: seat.PipelineOutcomeRunning, + Steps: []seat.StepResult{{Step: seat.StepResultStepLint, Status: seat.StepResultStatusRequired, Outcome: seat.StepResultOutcomeRunning, Ask: "b1", Seat: nodebuildgo.Seat}}}, 1} + + lint := ended("a1", "lint", "passed") + unit := ended("a2", "unit", "passed") + unit.Tests = &nodebuildgo.TestCounts{Unit: 501, Integration: 0} + for _, ev := range []StepEnded{lint, unit} { + if _, err := h.OnStepEnded(goSeat, goSeat.Prefix+"n1", ev); err != nil { + t.Fatal(err) + } + } + pe, ok := bodies["pipeline-ended.app.main.g1"].(seat.PipelineEnded) + if !ok || pe.Tests == nil || pe.Tests.Unit != 501 || !pe.OverBudget { + t.Fatalf("pipeline-ended = %+v (said %v)", bodies["pipeline-ended.app.main.g1"], order) + } + if _, said := bodies["group-ended.main.g1"]; said { + t.Fatalf("group-ended said while a member runs: %v", order) + } + lib := ended("b1", "lint", "passed") + lib.Pipeline, lib.Module, lib.Commit, lib.Fingerprint = "main.5.lib", "lib", "c2", "" + if _, err := h.OnStepEnded(goSeat, goSeat.Prefix+"n1", lib); err != nil { + t.Fatal(err) + } + ge, ok := bodies["group-ended.main.g1"].(seat.GroupEnded) + if !ok || ge.Outcome != seat.GroupEndedOutcomePassed || ge.Group != "g1" || ge.Branch != "main" || ge.Source != 5 || len(ge.Pipelines) != 2 { + t.Fatalf("group-ended = %+v (said %v)", bodies["group-ended.main.g1"], order) + } + // A pipeline of no group says no group-ended. +} diff --git a/modules/mesh-delivery-pipeline/internal/pipeline/steps.go b/modules/mesh-delivery-pipeline/internal/pipeline/steps.go index fd2c3fa3..a5c60fc5 100644 --- a/modules/mesh-delivery-pipeline/internal/pipeline/steps.go +++ b/modules/mesh-delivery-pipeline/internal/pipeline/steps.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "sort" "strings" "time" @@ -207,6 +208,49 @@ func (h *Hearer) said(p seat.Pipeline, asked, ended bool) error { if err := h.say(seat.EventPipelineEnded, p, nil); err != nil { return fmt.Errorf("pipeline %s ended but %s is not said: %w", p.Pipeline, seat.EventPipelineEnded, err) } + return h.groupEnded(p) + } + return nil +} + +// groupEnded says a group's one pipeline's end once every member's pipeline of the same watched branch and source revision +// ended (design 47 G §73, §332): on the subject under the group, no module part, with every member's end. A pipeline of no +// group says none; a member still running says none yet. +func (h *Hearer) groupEnded(p seat.Pipeline) error { + if p.Group == "" { + return nil + } + pkeys, err := h.Store.Keys("*.*.*") + if err != nil { + return fmt.Errorf("the members of group %s are not listed: %w", p.Group, err) + } + sort.Strings(pkeys) + var members []seat.PipelineEnded + outcome, why := seat.GroupEndedOutcomePassed, "" + var last *time.Time + for _, key := range pkeys { + m, _, err := h.Store.Get(key) + if err != nil || m.Branch != p.Branch || m.Source != p.Source || m.Group != p.Group { + continue + } + if m.Outcome == seat.PipelineOutcomeRunning { + return nil + } + members = append(members, pipelineEnded(m)) + if m.Ended != nil && (last == nil || m.Ended.After(*last)) { + last = m.Ended + } + if m.Outcome != seat.PipelineOutcomePassed && outcome != seat.GroupEndedOutcomeFailed { + outcome, why = m.Outcome, m.Module+": "+m.Why + if m.Outcome == seat.PipelineOutcomeFailed { + outcome = seat.GroupEndedOutcomeFailed + } + } + } + subject := "mesh.seat." + seat.Seat + ".event." + seat.EventGroupEnded + "." + keys.EscapeName(p.Branch) + "." + keys.EscapeName(p.Group) + if err := h.Say(subject, seat.GroupEnded{V: 1, Branch: p.Branch, Group: p.Group, Source: p.Source, Revision: p.Revision, Ended: last, + Outcome: outcome, Why: why, Pipelines: members}); err != nil { + return fmt.Errorf("group %s ended but %s is not said: %w", p.Group, seat.EventGroupEnded, err) } return nil } @@ -247,8 +291,31 @@ func (h *Hearer) say(event string, p seat.Pipeline, step *seat.StepResult) error } } +// The test budget per catalog module (design 47 G §24, §419; the test budget): at most 500 unit and 50 integration tests. +const ( + budgetUnit = 500 + budgetIntegration = 50 +) + +// testCounts is the module's test counts, the sum of its test steps' own counts; nil when no step counted any. +func testCounts(steps []seat.StepResult) *seat.TestCounts { + var t seat.TestCounts + counted := false + for _, s := range steps { + if s.Tests != nil { + t.Unit, t.Integration, counted = t.Unit+s.Tests.Unit, t.Integration+s.Tests.Integration, true + } + } + if !counted { + return nil + } + return &t +} + func pipelineEnded(p seat.Pipeline) seat.PipelineEnded { - return seat.PipelineEnded{V: 1, Pipeline: p.Pipeline, Branch: p.Branch, Module: p.Module, Commit: p.Commit, Fingerprint: p.Fingerprint, + tests := testCounts(p.Steps) + return seat.PipelineEnded{Tests: tests, OverBudget: tests != nil && (tests.Unit > budgetUnit || tests.Integration > budgetIntegration), + V: 1, Pipeline: p.Pipeline, Branch: p.Branch, Module: p.Module, Commit: p.Commit, Fingerprint: p.Fingerprint, Group: p.Group, Revision: p.Revision, Source: p.Source, Run: p.Run, Outcome: p.Outcome, Why: p.Why, Started: p.Started, Ended: p.Ended, EndCount: p.EndCount, Steps: p.Steps} }