Skip to content

Commit e6ffbe8

Browse files
Improve our test summary report (Azure#5115)
* Format package summary as a table Removes the common prefix of all package names to reduce the size of the report, and to aid scanning. * Reduce size of slow tests table too * Use factory to accumulate test runs Allows us to use JSONFormat instances transiently, instead of keeping them in memory * Scan file line by line * Use enumeration for action * Avoid intermediate strings when parsing * Add missing header * Remove enumeration in favour of strings * Remove string concatenation for keys * Use unique to decrease heap pressure * Include missing file
1 parent 2bbe5d7 commit e6ffbe8

File tree

3 files changed

+194
-86
lines changed

3 files changed

+194
-86
lines changed

v2/tools/mangle-test-json/main.go

Lines changed: 87 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
package main
77

88
import (
9+
"bufio"
910
"encoding/json"
1011
"errors"
1112
"fmt"
@@ -45,7 +46,7 @@ func main() {
4546
for k, v := range byPackage {
4647
packages = append(packages, k)
4748
sort.Slice(v, func(i, j int) bool {
48-
return v[i].Test < v[j].Test
49+
return v[i].Test.Value() < v[j].Test.Value()
4950
})
5051
}
5152

@@ -67,60 +68,58 @@ func min(i, j int) int {
6768
return j
6869
}
6970

70-
func actionSymbol(d TestRun) string {
71-
switch d.Action {
72-
case "pass":
73-
return "✅"
74-
case "fail":
75-
return "❌"
76-
case "skip":
77-
return "⏭️"
78-
default:
79-
panic(fmt.Sprintf("unhandled action: %s", d.Action))
80-
}
81-
}
82-
8371
func loadJSON(
8472
testOutputFile string,
8573
log logr.Logger,
8674
) map[string][]TestRun {
87-
content, err := os.ReadFile(testOutputFile)
75+
file, err := os.Open(testOutputFile)
8876
if err != nil {
8977
log.Error(
9078
err,
91-
"Unable to read file",
79+
"Unable to open file",
9280
"file", testOutputFile)
81+
return make(map[string][]TestRun)
9382
}
83+
defer file.Close()
9484

95-
// Break into individual lines to make error reporting easier
96-
lines := strings.Split(string(content), "\n")
97-
98-
data := make([]JSONFormat, 0, len(lines))
85+
scanner := bufio.NewScanner(file)
86+
factory := newTestRunFactory()
9987
errCount := 0
100-
for row, line := range lines {
88+
row := 0
89+
for scanner.Scan() {
90+
line := scanner.Bytes()
10191
if len(line) == 0 {
10292
// Skip empty lines
10393
continue
10494
}
10595

10696
var d JSONFormat
107-
err := json.Unmarshal([]byte(line), &d)
97+
err := json.Unmarshal(line, &d)
10898
if err != nil {
10999
// Write the line to the log so we don't lose the content
100+
text := string(line)
110101
log.Info(
111102
"Unable to parse",
112-
"line", line)
103+
"line", text)
113104

114-
if line != "" && !strings.HasPrefix(line, "FAIL") {
105+
if text != "" && !strings.HasPrefix(text, "FAIL") {
115106
// It's a parse failure we care about, write details
116-
logError(log, err, row, line)
107+
logError(log, err, row, text)
117108
errCount++
118109
}
119110

120111
continue
121112
}
122113

123-
data = append(data, d)
114+
factory.apply(d)
115+
row++
116+
}
117+
118+
if err := scanner.Err(); err != nil {
119+
log.Error(
120+
err,
121+
"Error reading file",
122+
"file", testOutputFile)
124123
}
125124

126125
if errCount > 0 {
@@ -129,43 +128,15 @@ func loadJSON(
129128
"count", errCount)
130129
}
131130

132-
// Track all the test runs
133-
testRuns := make(map[string]*TestRun)
134-
for _, d := range data {
135-
136-
// Find (or create) the test run for this item of data
137-
testrun, found := testRuns[d.key()]
138-
if !found {
139-
testrun = &TestRun{
140-
Package: d.Package,
141-
Test: d.Test,
142-
}
143-
144-
testRuns[d.key()] = testrun
145-
}
146-
147-
switch d.Action {
148-
case "run":
149-
testrun.run(d.Time)
150-
151-
case "pause":
152-
testrun.pause(d.Time)
153-
154-
case "cont":
155-
testrun.resume(d.Time)
156-
157-
case "output":
158-
testrun.output(d.Output)
159-
160-
case "pass", "fail", "skip":
161-
testrun.complete(d.Action, d.Time)
131+
// package → list of tests
132+
byPackage := make(map[string][]TestRun)
133+
for pkg, pkgRuns := range factory.testRuns {
134+
var runs []TestRun
135+
for _, r := range pkgRuns {
136+
runs = append(runs, *r)
162137
}
163-
}
164138

165-
// package → list of tests
166-
byPackage := make(map[string][]TestRun, len(data))
167-
for _, v := range testRuns {
168-
byPackage[v.Package] = append(byPackage[v.Package], *v)
139+
byPackage[pkg] = runs
169140
}
170141

171142
return byPackage
@@ -182,6 +153,11 @@ func sensitiveRound(d time.Duration) time.Duration {
182153
func printSummary(packages []string, byPackage map[string][]TestRun) {
183154
fmt.Printf("## Package Summary\n\n")
184155

156+
commonPrefix := findCommonPackagePrefix(packages)
157+
158+
fmt.Printf("| Result | %s | Time |\n", commonPrefix)
159+
fmt.Printf("|--------|:---|-----:|\n")
160+
185161
// output table-of-contents
186162
for _, pkg := range packages {
187163
tests := byPackage[pkg]
@@ -195,8 +171,11 @@ func printSummary(packages []string, byPackage map[string][]TestRun) {
195171
totalRuntime += t.RunTime
196172
}
197173

198-
overallOutcome := actionSymbol(tests[0])
199-
fmt.Printf("* %s `%s` (runtime %s)\n", overallOutcome, pkg, totalRuntime)
174+
overallOutcome := tests[0].actionSymbol()
175+
shortPkgName := displayNameForPackage(pkg, commonPrefix)
176+
totalRuntime = sensitiveRound(totalRuntime)
177+
178+
fmt.Printf("| %s | %s | %s |\n", overallOutcome, shortPkgName, totalRuntime)
200179
}
201180

202181
fmt.Println()
@@ -210,7 +189,7 @@ func printDetails(packages []string, byPackage map[string][]TestRun) {
210189
for _, pkg := range packages {
211190
tests := byPackage[pkg]
212191
// check package-level indicator, which will be first ("" test name):
213-
if tests[0].Action != "fail" {
192+
if tests[0].Action != Failed {
214193
continue // no failed tests, skip
215194
} else {
216195
anyFailed = true
@@ -249,9 +228,9 @@ func printDetails(packages []string, byPackage map[string][]TestRun) {
249228
continue
250229
}
251230

252-
fmt.Printf("#### Test `%s`\n", test.Test)
231+
fmt.Printf("#### Test `%s`\n", test.Test.Value())
253232

254-
if test.Action == "fail" {
233+
if test.Action == Failed {
255234
fmt.Printf("Failed in %s:\n", test.RunTime)
256235
} else {
257236
fmt.Printf("Elapsed %s:\n", test.RunTime)
@@ -268,7 +247,7 @@ func printDetails(packages []string, byPackage map[string][]TestRun) {
268247

269248
// Output info on stderr, so that test failure isn’t silent on console
270249
// when running `task ci`, and that full logs are available if they get trimmed
271-
fmt.Fprintf(os.Stderr, "- Test failed: %s\n", test.Test)
250+
fmt.Fprintf(os.Stderr, "- Test failed: %s\n", test.Test.Value())
272251
fmt.Fprintln(os.Stderr, "=== TEST OUTPUT ===")
273252
for _, outputLine := range test.Output {
274253
fmt.Fprint(os.Stderr, outputLine) // note that line already has newline attached
@@ -280,7 +259,7 @@ func printDetails(packages []string, byPackage map[string][]TestRun) {
280259
}
281260

282261
if !anyFailed {
283-
fmt.Println("**🎉 All tests passed. 🎉**")
262+
fmt.Printf("**🎉 All tests passed. 🎉**\n\n")
284263
}
285264
}
286265

@@ -315,11 +294,19 @@ func printSlowTests(byPackage map[string][]TestRun) {
315294
return allTests[i].RunTime > allTests[j].RunTime
316295
})
317296

318-
fmt.Println("| Package | Name | Time |")
297+
pkgPrefix := allTests[0].Package.Value()
298+
for _, test := range allTests {
299+
pkgPrefix = commonPrefix(pkgPrefix, test.Package.Value())
300+
}
301+
302+
pkgPrefix = strings.TrimRight(pkgPrefix, "/")
303+
304+
fmt.Printf("| %s | Name | Time |\n", pkgPrefix)
319305
fmt.Println("|---------|------|-----:|")
320306
for i := 0; i < min(10, len(allTests)); i += 1 {
321307
test := allTests[i]
322-
fmt.Printf("| `%s` | `%s` | %s |\n", test.Package, test.Test, test.RunTime)
308+
pkg := displayNameForPackage(test.Package.Value(), pkgPrefix)
309+
fmt.Printf("| `%s` | `%s` | %s |\n", pkg, test.Test.Value(), test.RunTime)
323310
}
324311
}
325312

@@ -366,7 +353,34 @@ func logError(
366353
)
367354
}
368355

369-
// key returns a unique key for a test run
370-
func (d JSONFormat) key() string {
371-
return d.Package + "/" + d.Test
356+
func commonPrefix(
357+
left string,
358+
right string,
359+
) string {
360+
minLen := min(len(left), len(right))
361+
i := 0
362+
for i < minLen && left[i] == right[i] {
363+
i++
364+
}
365+
366+
return left[:i]
367+
}
368+
369+
// findCommonPackagePrefix finds the common prefix of all package names, to shorten display
370+
func findCommonPackagePrefix(packages []string) string {
371+
prefix := packages[0]
372+
for _, pkg := range packages {
373+
prefix = commonPrefix(prefix, pkg)
374+
}
375+
prefix = strings.TrimRight(prefix, "/")
376+
return prefix
377+
}
378+
379+
func displayNameForPackage(
380+
pkg string,
381+
commonPrefix string,
382+
) string {
383+
name := strings.TrimPrefix(pkg, commonPrefix)
384+
name = strings.TrimLeft(name, "/")
385+
return name
372386
}

0 commit comments

Comments
 (0)