A change to mesh-build-go was judged by the installed module it replaces, so such pull requests were merged by hand. ADR 0535 §1, §2, §4, §5.
269 lines
9.0 KiB
Go
269 lines
9.0 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
|
|
// judged names the program that judged a change to a build module (ADR 0535 §5); never dropped.
|
|
judged 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.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.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 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, judged map[string]string) pieces {
|
|
var pc pieces
|
|
pc.judged = judged[pullKey(p)]
|
|
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, 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)
|
|
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
|
|
}
|
|
}
|
|
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, " | ")
|
|
}
|