278 lines
9.4 KiB
Go
278 lines
9.4 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)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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 && 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, ", "))
|
|
}
|
|
}
|
|
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}, {"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)
|
|
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, " | ")
|
|
}
|