Files
mesh-delivery/modules/mesh-delivery-checker/internal/check/line.go
T

406 lines
15 KiB
Go

package check
import (
"fmt"
"slices"
"strings"
"unicode/utf8"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// 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 meshdeliverychecker.VerdictsByGroupHeadsValue) string {
f := failedOf(v)
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 meshdeliverychecker.VerdictsByGroupHeadsValue) []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(meshdeliverychecker.VerdictsByGroupHeadsValue{Failed: &FailedFirst{Machine: f.Machine, Test: f.Test, Words: f.Words}}))
}
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 meshdeliverychecker.VerdictsByGroupHeadsValue, 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 meshdeliverycheckerclient.MaxFailures counted too.
func failedCount(v meshdeliverychecker.VerdictsByGroupHeadsValue) int {
n := len(v.Failures)
if n > meshdeliverycheckerclient.MaxFailures {
var more int
if _, err := fmt.Sscanf(v.Failures[n-1].Test, "and %d more", &more); err == nil {
return meshdeliverycheckerclient.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
// series names each change of a package series the pull request makes (design 32, DLV-C4); never dropped.
series 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, p.series}
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.series, 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 meshdeliverychecker.VerdictsByGroupHeadsValueLevelsValue) 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 *meshdeliverychecker.VerdictsByGroupHeadsValueComposition, 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 *meshdeliverychecker.VerdictsByGroupHeadsValueComposition, took int64, short bool) string {
if c != nil && c.State == meshdeliverycheckerclient.CompositionNotRun && c.Words != "" {
return "composition not judged: " + c.Words
}
if c == nil || c.State == meshdeliverycheckerclient.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 == meshdeliverycheckerclient.CompositionPass && c.Words != "" {
warned = " (" + c.Words + ")" // a node that did not answer (ADR 0593 §9)
}
switch {
case c.State == meshdeliverycheckerclient.CompositionPass && short:
return "composition passed" + in + on + warned // what it composed with is in the report
case c.State == meshdeliverycheckerclient.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 meshdeliverychecker.VerdictsByGroupHeadsValue, 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 meshdeliverychecker.VerdictsByGroupHeadsValue, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) pieces {
var pc pieces
pc.judged = judged[pullKey(p)]
pc.series = seriesWords(cs, p)
if cs != nil {
pc.warning = cs.Warning(pullKey(p))
}
commits := short12(p.Head) + " on " + short12(p.Trunk)
switch v.State {
case meshdeliverycheckerclient.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 meshdeliverycheckerclient.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{buildstep.LevelUnit, buildstep.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{buildstep.LevelUnit, buildstep.LevelIntegration} {
if r, ok := v.Levels[level]; ok {
name := map[string]string{buildstep.LevelUnit: "unit", buildstep.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 != meshdeliverycheckerclient.VerdictRunning {
t := tookOf(v)
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 != meshdeliverycheckerclient.VerdictRunning {
pc.composition = compositionWords(v.Composition, tookOf(v).Composition)
pc.shortComposition = composition(v.Composition, tookOf(v).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 != meshdeliverycheckerclient.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 meshdeliverychecker.VerdictsByGroupHeadsValue, 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 meshdeliverychecker.VerdictsByGroupHeadsValue, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) string {
pc := parts(v, cs, p, builds, slowest, judged)
if v.State == meshdeliverycheckerclient.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.series)
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 != "" || pc.series != "" {
for _, counts := range []string{pc.shortCounts, ""} {
if s := joinSet(pc.head, pc.commits, counts, pc.judged, pc.series); utf8.RuneCountInString(s) <= MaxLine {
return s
}
}
rest := " | " + joinSet(pc.commits, pc.judged, pc.series)
room := MaxLine - utf8.RuneCountInString(rest) - 1
if head := []rune(pc.head); room > 0 && len(head) > room {
return string(head[:room]) + "…" + rest
}
}
if v.State == meshdeliverycheckerclient.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, pc.series); 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, " | ")
}