RN9: pipeline-ended carries test counts and over-budget; group-ended says a group's end
pipeline-ended carries the module's test counts (the sum of its steps') and over-budget past 500 unit or 50 integration tests; once every member of a group on a watched branch's source revision ended, group-ended is said under the group. Builds: design 47 G §24, §73, §332, §419 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -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.
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user