Files
mesh-delivery/modules/mesh-delivery-checker/internal/check/line.go
T
jschoubben 0ce3409141 mesh-delivery-checker: make a check's composition from every placed node's engine answer
ADR 0593 §7, §9: the checker asks compose-candidate of each placed node in parallel within its own setting
compose-answer-bound (60 s); a refusal fails the check naming node and module, a silent, unready or
unreachable node passes with "not composed on that node: no answer". A group with a node-engine member is
composed by the composer at that head on the Go build seat over each node's input. The wait on the
assigner's compositions goes, and the checker invokes the one engine verb it needs.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 18:56:20 +01:00

399 lines
14 KiB
Go

package check
import (
"fmt"
"slices"
"strings"
"unicode/utf8"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// MaxLine is the most characters a status's line has: the forge shows a status's description in a short space, and the full
// report is linked (ADR 0447 clause 3, ADR 0484 clause 7). A line that would be longer drops its lesser parts in a fixed
// order, and is cut last.
const MaxLine = 140
// seconds is a time in milliseconds in the unit a person reads: 350ms, 4.1s, 72s.
func seconds(ms int64) string {
switch {
case ms < 1000:
return fmt.Sprintf("%dms", ms)
case ms < 10000:
return fmt.Sprintf("%.1fs", float64(ms)/1000)
}
return fmt.Sprintf("%ds", (ms+500)/1000)
}
func short12(s string) string {
if len(s) > 12 {
return s[:12]
}
return s
}
// firstFailed is what failed first, in words: a conflict, a refused step or a test, named before anything else.
func firstFailed(v delivery.Verdict) string {
f := v.Failed
switch {
case f.Test != "":
if w := firstWordsLine(f.Words); w != "" {
return f.Test + ": " + w
}
return f.Test
case f.Machine != "" && firstWordsLine(f.Words) != "":
return f.Machine + ": " + firstWordsLine(f.Words)
case f.Machine != "":
return f.Machine
}
return firstWordsLine(f.Words)
}
// failedLines is everything a verdict names as failed, one line each: every failed test with its first line (issue
// 1921), or what failed first when the verdict names no list (a conflict, a refused step, an older verdict).
func failedLines(v delivery.Verdict) []string {
if len(v.Failures) == 0 {
if f := firstFailed(v); f != "" {
return []string{f}
}
return nil
}
lines := make([]string, 0, len(v.Failures))
for _, f := range v.Failures {
lines = append(lines, firstFailed(delivery.Verdict{Failed: f}))
}
return lines
}
// failedHead is the head of a failed verdict's line: what failed first, and when more did, how many failed and "and N
// more" after it (the operator, 2026-11-02), so a line cut to MaxLine still says that more failed. The words of the first
// are cut to fit room characters when room is not negative; the count and "and N more" are never cut.
func failedHead(v delivery.Verdict, room int) string {
first := firstFailed(v)
n := failedCount(v)
if n <= 1 {
return clipRunes("failed: "+first, room)
}
prefix, more := fmt.Sprintf("failed (%d): ", n), fmt.Sprintf("; and %d more", n-1)
if room >= 0 {
first = clipRunes(first, room-utf8.RuneCountInString(prefix+more))
}
return prefix + first + more
}
// clipRunes cuts s to at most n characters, the last an ellipsis; a negative n leaves it whole.
func clipRunes(s string, n int) string {
r := []rune(s)
if n < 0 || len(r) <= n {
return s
}
if n < 1 {
return ""
}
return string(r[:n-1]) + "…"
}
// failedCount is how many failures a verdict names, the ones after delivery.MaxFailures counted too.
func failedCount(v delivery.Verdict) int {
n := len(v.Failures)
if n > delivery.MaxFailures {
var more int
if _, err := fmt.Sscanf(v.Failures[n-1].Test, "and %d more", &more); err == nil {
return delivery.MaxFailures + more
}
}
return n
}
// firstWordsLine is the first line of a failure's words: the words are several lines (issue 1901) and the status line one.
func firstWordsLine(w string) string { return firstLine(w) }
// pieces are the parts of a line, from the most to the least needed.
type pieces struct {
head, commits, counts, shortCounts, times, shortTimes, over, builds, composition, shortComposition, group string
// judged names the program that judged a change to a build module (ADR 0535 §5); never dropped.
judged string
// carried says, of a carried verdict, the commits it was carried from (ADR 0548 §8); never dropped.
carried string
// warning says that a part of the check did not run, for instance that an older build module could not refuse a compiled program; never dropped.
warning string
}
// pullKey names a pull request of a case: `<owner/repo>#<number>`.
func pullKey(p Pull) string { return fmt.Sprintf("%s#%d", p.Repository, p.Number) }
func (p pieces) join(drop int) string {
parts := []string{p.head, p.commits, p.carried, p.warning, p.counts, p.judged}
switch {
case drop < 3 && p.times != "":
parts = append(parts, p.times)
case drop == 3 && p.shortTimes != "":
parts = []string{p.head, p.commits, p.carried, p.warning, p.shortCounts, p.judged, p.shortTimes}
}
if p.over != "" {
parts = append(parts, p.over)
}
if drop < 1 && p.builds != "" {
parts = append(parts, p.builds)
}
if drop < 2 && p.group != "" {
parts = append(parts, p.group)
}
switch {
case drop >= 3 && p.shortComposition != "":
parts = append(parts, p.shortComposition)
case p.composition != "":
parts = append(parts, p.composition)
}
return joinSet(parts...)
}
func levelWords(name string, r delivery.LevelResult) string {
s := fmt.Sprintf("%s %d run, %d kept", name, r.Run, r.Kept)
if r.Skipped > 0 {
s += fmt.Sprintf(", %d skipped", r.Skipped)
}
return s
}
// compositionWords says the composition: not yet run, or once the engines answered (ADR 0593 §7) passed, with the nodes
// that gave no answer, or failed, with the time it took and the machines it composed (issue 1300).
func compositionWords(c *delivery.Composition, took int64) string { return composition(c, took, false) }
// composition is compositionWords; short counts the machines and leaves the SDK commit to the report.
func composition(c *delivery.Composition, took int64, short bool) string {
if c != nil && c.State == delivery.CompositionNotRun && c.Words != "" {
return "composition not judged: " + c.Words
}
if c == nil || c.State == delivery.CompositionNotRun {
return "composition not yet run"
}
in := ""
if took > 0 {
in = " in " + seconds(took)
}
on := ""
if len(c.Machines) > 0 {
machines := slices.Sorted(slices.Values(c.Machines))
on = " on " + strings.Join(machines, ", ")
switch {
case len(machines) > 1 && short:
on = fmt.Sprintf(" on %d machines", len(machines))
case len(machines) > 1:
on = fmt.Sprintf(" on %d machines: %s", len(machines), strings.Join(machines, ", "))
}
}
warned := ""
if c.State == delivery.CompositionPass && c.Words != "" {
warned = " (" + c.Words + ")" // a node that did not answer (ADR 0593 §9)
}
switch {
case c.State == delivery.CompositionPass && short:
return "composition passed" + in + on + warned // what it composed with is in the report
case c.State == delivery.CompositionPass:
return "composition passed" + in + " with " + short12(c.SDK) + on + warned
}
return "composition failed" + in + on + ": " + c.Words
}
func buildWords(builds []BuildNote) string {
if len(builds) == 0 {
return ""
}
parts := make([]string, len(builds))
for i, b := range builds {
parts[i] = b.Module + " " + b.State
}
return "builds: " + strings.Join(parts, ", ")
}
// carriedWords say the commits a verdict was carried from, for the line of pull request p (ADR 0548 §8): its own trunk's
// commit as twelve characters, a sibling's with its name before it; only those that moved, sorted. Empty when none did.
func carriedWords(from map[string]string, v delivery.Verdict, cs *Case, p Pull) string {
var own string
var others []string
for r, old := range from {
now, ok := v.Trunk[r]
if cs != nil && !ok {
now, ok = cs.Trunk[r]
}
if !ok {
now = v.Beside[r]
}
if now == "" || now == old {
continue
}
if strings.EqualFold(r, p.Repository) {
own = short12(old)
continue
}
name := r
if i := strings.LastIndex(r, "/"); i >= 0 {
name = r[i+1:]
}
others = append(others, name+" "+short12(old))
}
slices.Sort(others)
if own != "" {
others = append([]string{own}, others...)
}
return strings.Join(others, ", ")
}
// parts makes the pieces of the line of a pull request of a case.
func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) pieces {
var pc pieces
pc.judged = judged[pullKey(p)]
if cs != nil {
pc.warning = cs.Warning(pullKey(p))
}
commits := short12(p.Head) + " on " + short12(p.Trunk)
switch v.State {
case delivery.VerdictRunning:
pc.head = "checking " + commits
if cs != nil && cs.CarriedFrom != nil {
// Out of date and being carried to the new commit (ADR 0548 §6): pending, and saying so.
pc.head = "being carried to " + commits
if w := carriedWords(cs.CarriedFrom, v, cs, p); w != "" {
pc.carried = "from " + w
}
}
case delivery.VerdictPass:
pc.head = "pass " + commits
if w := carriedWords(v.CarriedFrom, v, cs, p); w != "" {
pc.carried = "carried from " + w // the counts that follow say what the carry ran and kept
}
default:
pc.head = failedHead(v, -1)
pc.commits = commits
}
var counts []string
for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} {
if r, ok := v.Levels[level]; ok {
counts = append(counts, levelWords(level, r))
}
}
pc.counts = strings.Join(counts, " | ")
var short []string
for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} {
if r, ok := v.Levels[level]; ok {
name := map[string]string{delivery.LevelUnit: "unit", delivery.LevelIntegration: "int"}[level]
w := fmt.Sprintf("%s %d run %d kept", name, r.Run, r.Kept)
if r.Skipped > 0 {
w += fmt.Sprintf(" %d skipped", r.Skipped)
}
short = append(short, w)
}
}
pc.shortCounts = strings.Join(short, " | ")
if v.State != delivery.VerdictRunning {
t := v.Took
var times []string
for _, x := range []struct {
name string
ms int64
}{{"queue", t.Queue}, {"clone", t.Clone}, {"lint", t.Lint}, {"unit", t.Unit}, {"int setup", t.IntegrationSetup}, {"int run", t.IntegrationRun}} {
if x.ms > 0 {
times = append(times, x.name+" "+seconds(x.ms))
}
}
if len(times) > 0 {
pc.times = strings.Join(times, ", ")
}
// The compact form for a line that is too long: the parts that dominate, the integration level's setup and run as one.
short = short[:0:0]
if t.Clone > 0 {
short = append(short, "clone "+seconds(t.Clone))
}
if t.Lint > 0 {
short = append(short, "lint "+seconds(t.Lint))
}
if t.Unit > 0 {
short = append(short, "unit "+seconds(t.Unit))
}
switch {
case t.IntegrationSetup > 0 && t.IntegrationRun > 0:
short = append(short, "int "+seconds(t.IntegrationSetup)+"+"+seconds(t.IntegrationRun))
case t.IntegrationRun > 0:
short = append(short, "int "+seconds(t.IntegrationRun))
}
pc.shortTimes = strings.Join(short, " ")
}
if len(v.OverBound) > 0 {
pc.over = "over bound: " + strings.Join(v.OverBound, ", ")
if len(slowest) > 0 {
pc.over += " (slowest " + strings.Join(slowest, ", ") + ")"
}
}
pc.builds = buildWords(builds)
if v.State != delivery.VerdictRunning {
pc.composition = compositionWords(v.Composition, v.Took.Composition)
pc.shortComposition = composition(v.Composition, v.Took.Composition, true)
}
if cs != nil && len(cs.Pulls) > 1 {
pc.group = fmt.Sprintf("group %s of %d pull requests", cs.Group, len(cs.Pulls))
if v.State != delivery.VerdictPass {
pc.group = fmt.Sprintf("%s#%d in group %s", p.Repository, p.Number, cs.Group)
}
}
return pc
}
// Report is the full line of a pull request's status, with no limit: what the read tools and the full report say.
func Report(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) string {
return parts(v, cs, p, builds, slowest, judged).join(0)
}
// Line is the words of a status for one pull request of a case (ADR 0447 clause 3, ADR 0484 clause 7): the verdict first (a
// failure names the conflict, the refused step or the test before anything), the head and the trunk commit as twelve
// characters each, the tests run, kept and skipped per level, the time of each part, a level over its bound with its slowest
// tests, the builds of the touched modules, and that the composition was not yet run; for a change to a build module, the
// program that judged it (judged, by pullKey; ADR 0535 §5). It is at most MaxLine characters: the
// builds, then the group drop first, then the times are given in their compact form, then dropped, and the line is cut at last.
// The program that judged is never dropped: a line naming it gives the counts in their compact form, then drops them and
// what follows, and is cut in its first part rather than lose it.
func Line(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) string {
pc := parts(v, cs, p, builds, slowest, judged)
if v.State == delivery.VerdictFail {
// The head gives way first to the commits and what is never dropped: its count and "and N more" stay whole.
kept := joinSet(pc.commits, pc.carried, pc.warning, pc.judged)
pc.head = failedHead(v, MaxLine-utf8.RuneCountInString(" | "+kept))
}
for drop := 0; drop <= 4; drop++ {
if s := pc.join(drop); utf8.RuneCountInString(s) <= MaxLine {
return s
}
}
if pc.judged != "" {
for _, counts := range []string{pc.shortCounts, ""} {
if s := joinSet(pc.head, pc.commits, counts, pc.judged); utf8.RuneCountInString(s) <= MaxLine {
return s
}
}
rest := " | " + joinSet(pc.commits, pc.judged)
room := MaxLine - utf8.RuneCountInString(rest) - 1
if head := []rune(pc.head); room > 0 && len(head) > room {
return string(head[:room]) + "…" + rest
}
}
if v.State == delivery.VerdictFail {
// A failure's head, sized above, and the commits fit: the counts and the composition are in the report.
if s := joinSet(pc.head, pc.commits, pc.carried, pc.warning, pc.judged); utf8.RuneCountInString(s) <= MaxLine {
return s
}
}
runes := []rune(pc.join(4))
return string(runes[:MaxLine-1]) + "…"
}
// joinSet joins the parts that are not empty.
func joinSet(parts ...string) string {
out := make([]string, 0, len(parts))
for _, s := range parts {
if s != "" {
out = append(out, s)
}
}
return strings.Join(out, " | ")
}