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: `#`. 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}, {"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, " | ") }