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 } // failedCount is how many tests a verdict names as failed, 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 } // pullKey names a pull request of a case: `#`. 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.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.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, ", ") } // 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)] 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 = "failed: " + firstFailed(v) if n := failedCount(v); n > 1 { pc.head = fmt.Sprintf("failed (%d tests): %s", n, 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, " | ") }