Files
mesh-delivery/modules/mesh-delivery-checker/internal/check/line.go
T
jschoubben 03eaf332ad Say the composition's time and the machines it composed in a check's line, once the assigner reports them (issue 1300, C10)
The line said only that the composition passed with an SDK commit, and its time sat among the parts that are dropped
first, so a forge status never said how long the composition took or which machines it covered.
2026-10-22 18:06:07 +02:00

243 lines
7.8 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 f.Words != "" {
return f.Test + ": " + f.Words
}
return f.Test
case f.Machine != "" && f.Words != "":
return f.Machine + ": " + f.Words
case f.Machine != "":
return f.Machine
}
return f.Words
}
// 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
}
func (p pieces) join(drop int) string {
parts := []string{p.head, p.commits, p.counts}
switch {
case drop < 3 && p.times != "":
parts = append(parts, p.times)
case drop == 3 && p.shortTimes != "":
parts = []string{p.head, p.commits, p.shortCounts, 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)
}
out := make([]string, 0, len(parts))
for _, s := range parts {
if s != "" {
out = append(out, s)
}
}
return strings.Join(out, " | ")
}
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 assigner's check reports it (issue 1297) passed 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 {
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, ", "))
}
}
switch {
case c.State == delivery.CompositionPass && short:
return "composition passed" + in + on // the SDK it composed with is in the report
case c.State == delivery.CompositionPass:
return "composition passed" + in + " with " + short12(c.SDK) + on
}
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, ", ")
}
// 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) pieces {
var pc pieces
commits := short12(p.Head) + " on " + short12(p.Trunk)
switch v.State {
case delivery.VerdictRunning:
pc.head = "checking " + commits
case delivery.VerdictPass:
pc.head = "pass " + commits
default:
pc.head = "failed: " + firstFailed(v)
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}, {"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.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) string {
return parts(v, cs, p, builds, slowest).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. 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.
func Line(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []string) string {
pc := parts(v, cs, p, builds, slowest)
for drop := 0; drop <= 4; drop++ {
if s := pc.join(drop); utf8.RuneCountInString(s) <= MaxLine {
return s
}
}
runes := []rune(pc.join(4))
return string(runes[:MaxLine-1]) + "…"
}