-
Notifications
You must be signed in to change notification settings - Fork 887
ci: Print go test stats #6855
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
ci: Print go test stats #6855
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
0aeb408
ci: Print go test stats
mafredri 5ba9c18
Make gen
mafredri d031855
Add golden test for ci-report
mafredri fc04391
Gosec
mafredri 9f8b993
Rename ignored file
mafredri b0db1ea
Reduce size
mafredri c71ab07
Typos be gone
mafredri 0da7fa0
Rename json, avoid linter
mafredri 004ca74
wip
mafredri 39a55b8
Add tests
mafredri 9568510
Merge branch 'main' into mafredri/ci-stats
mafredri 3248e83
Fix windows newlines
mafredri File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# ci-report | ||
|
||
This program generates a CI report from the `gotests.json` generated by `go test -json` (we use `gotestsum` as a frontend). | ||
|
||
## Limitations | ||
|
||
We won't generate any report/stats for tests that weren't run. To find all existing tests, we could use: `go test ./... -list=. -json`, but the time it takes is probably not worth it. Usually most tests will run, even if there are errors and we're using `-failfast`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
package main | ||
|
||
import ( | ||
"bufio" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"os" | ||
"strings" | ||
"time" | ||
|
||
"golang.org/x/exp/slices" | ||
"golang.org/x/xerrors" | ||
) | ||
|
||
func main() { | ||
if len(os.Args) != 2 { | ||
_, _ = fmt.Println("usage: ci-report <gotests.json>") | ||
os.Exit(1) | ||
} | ||
name := os.Args[1] | ||
|
||
goTests, err := parseGoTestJSON(name) | ||
if err != nil { | ||
_, _ = fmt.Printf("error parsing gotestsum report: %v", err) | ||
os.Exit(1) | ||
} | ||
|
||
rep, err := parseCIReport(goTests) | ||
if err != nil { | ||
_, _ = fmt.Printf("error parsing ci report: %v", err) | ||
os.Exit(1) | ||
} | ||
|
||
err = printCIReport(os.Stdout, rep) | ||
if err != nil { | ||
_, _ = fmt.Printf("error printing report: %v", err) | ||
os.Exit(1) | ||
} | ||
} | ||
|
||
func parseGoTestJSON(name string) (GotestsumReport, error) { | ||
f, err := os.Open(name) | ||
if err != nil { | ||
return GotestsumReport{}, xerrors.Errorf("error opening gotestsum json file: %w", err) | ||
} | ||
defer f.Close() | ||
|
||
dec := json.NewDecoder(f) | ||
var report GotestsumReport | ||
for { | ||
var e GotestsumReportEntry | ||
err = dec.Decode(&e) | ||
if errors.Is(err, io.EOF) { | ||
break | ||
} | ||
if err != nil { | ||
return GotestsumReport{}, xerrors.Errorf("error decoding json: %w", err) | ||
} | ||
e.Package = strings.TrimPrefix(e.Package, "github.com/coder/coder/") | ||
report = append(report, e) | ||
} | ||
|
||
return report, nil | ||
} | ||
|
||
func parseCIReport(report GotestsumReport) (CIReport, error) { | ||
packagesSortedByName := []string{} | ||
packageTimes := map[string]float64{} | ||
packageFail := map[string]int{} | ||
packageSkip := map[string]bool{} | ||
testTimes := map[string]float64{} | ||
testSkip := map[string]bool{} | ||
testOutput := map[string]string{} | ||
testSortedByName := []string{} | ||
timeouts := map[string]string{} | ||
timeoutRunningTests := map[string]bool{} | ||
for i, e := range report { | ||
switch e.Action { | ||
// A package/test may fail or pass. | ||
case Fail: | ||
if e.Test == "" { | ||
packageTimes[e.Package] = *e.Elapsed | ||
} else { | ||
packageFail[e.Package]++ | ||
name := e.Package + "." + e.Test | ||
testTimes[name] = *e.Elapsed | ||
} | ||
case Pass: | ||
if e.Test == "" { | ||
packageTimes[e.Package] = *e.Elapsed | ||
} else { | ||
name := e.Package + "." + e.Test | ||
delete(testOutput, name) | ||
mafredri marked this conversation as resolved.
Show resolved
Hide resolved
|
||
testTimes[name] = *e.Elapsed | ||
} | ||
|
||
// Gather all output (deleted when irrelevant). | ||
case Output: | ||
name := e.Package + "." + e.Test // May be pkg.Test or pkg. | ||
if _, ok := timeouts[name]; ok || strings.HasPrefix(e.Output, "panic: test timed out") { | ||
timeouts[name] += e.Output | ||
continue | ||
} | ||
if e.Test != "" { | ||
name := e.Package + "." + e.Test | ||
testOutput[name] += e.Output | ||
} | ||
|
||
// Packages start, tests run and either may be skipped. | ||
case Start: | ||
packagesSortedByName = append(packagesSortedByName, e.Package) | ||
case Run: | ||
name := e.Package + "." + e.Test | ||
testSortedByName = append(testSortedByName, name) | ||
case Skip: | ||
if e.Test == "" { | ||
packageSkip[e.Package] = true | ||
} else { | ||
name := e.Package + "." + e.Test | ||
testSkip[name] = true | ||
delete(testOutput, name) | ||
} | ||
|
||
// Ignore. | ||
case Cont: | ||
case Pause: | ||
|
||
default: | ||
return CIReport{}, xerrors.Errorf("unknown action: %v in entry %d (%v)", e.Action, i, e) | ||
} | ||
} | ||
|
||
// Normalize timeout from "pkg." or "pkg.Test" to "pkg". | ||
timeoutsNorm := make(map[string]string) | ||
for k, v := range timeouts { | ||
names := strings.SplitN(k, ".", 2) | ||
pkg := names[0] | ||
if _, ok := timeoutsNorm[pkg]; ok { | ||
panic("multiple timeouts for package: " + pkg) | ||
} | ||
timeoutsNorm[pkg] = v | ||
|
||
// Mark all running tests as timed out. | ||
// panic: test timed out after 2s\nrunning tests:\n\tTestAgent_Session_TTY_Hushlogin (0s)\n\n ... | ||
parts := strings.SplitN(v, "\n", 3) | ||
if len(parts) == 3 && strings.HasPrefix(parts[1], "running tests:") { | ||
s := bufio.NewScanner(strings.NewReader(parts[2])) | ||
for s.Scan() { | ||
name := s.Text() | ||
if !strings.HasPrefix(name, "\tTest") { | ||
break | ||
} | ||
name = strings.TrimPrefix(name, "\t") | ||
name = strings.SplitN(name, " ", 2)[0] | ||
timeoutRunningTests[pkg+"."+name] = true | ||
packageFail[pkg]++ | ||
} | ||
} | ||
} | ||
timeouts = timeoutsNorm | ||
|
||
sortAZ := func(a, b string) bool { return a < b } | ||
slices.SortFunc(packagesSortedByName, sortAZ) | ||
slices.SortFunc(testSortedByName, sortAZ) | ||
mafredri marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
var rep CIReport | ||
|
||
for _, pkg := range packagesSortedByName { | ||
output, timeout := timeouts[pkg] | ||
rep.Packages = append(rep.Packages, PackageReport{ | ||
Name: pkg, | ||
Time: packageTimes[pkg], | ||
Skip: packageSkip[pkg], | ||
Fail: packageFail[pkg] > 0, | ||
Timeout: timeout, | ||
Output: output, | ||
NumFailed: packageFail[pkg], | ||
}) | ||
} | ||
|
||
for _, test := range testSortedByName { | ||
names := strings.SplitN(test, ".", 2) | ||
skip := testSkip[test] | ||
out, fail := testOutput[test] | ||
rep.Tests = append(rep.Tests, TestReport{ | ||
Package: names[0], | ||
Name: names[1], | ||
Time: testTimes[test], | ||
Skip: skip, | ||
Fail: fail, | ||
Timeout: timeoutRunningTests[test], | ||
Output: out, | ||
}) | ||
} | ||
|
||
return rep, nil | ||
} | ||
|
||
func printCIReport(dst io.Writer, rep CIReport) error { | ||
enc := json.NewEncoder(dst) | ||
enc.SetIndent("", " ") | ||
err := enc.Encode(rep) | ||
if err != nil { | ||
return xerrors.Errorf("error encoding json: %w", err) | ||
} | ||
return nil | ||
} | ||
|
||
type CIReport struct { | ||
Packages []PackageReport `json:"packages"` | ||
Tests []TestReport `json:"tests"` | ||
} | ||
|
||
type PackageReport struct { | ||
Name string `json:"name"` | ||
Time float64 `json:"time"` | ||
Skip bool `json:"skip,omitempty"` | ||
Fail bool `json:"fail,omitempty"` | ||
NumFailed int `json:"num_failed,omitempty"` | ||
Timeout bool `json:"timeout,omitempty"` | ||
Output string `json:"output,omitempty"` // Output present e.g. for timeout. | ||
} | ||
|
||
type TestReport struct { | ||
Package string `json:"package"` | ||
Name string `json:"name"` | ||
Time float64 `json:"time"` | ||
Skip bool `json:"skip,omitempty"` | ||
Fail bool `json:"fail,omitempty"` | ||
Timeout bool `json:"timeout,omitempty"` | ||
Output string `json:"output,omitempty"` | ||
} | ||
|
||
type GotestsumReport []GotestsumReportEntry | ||
|
||
type GotestsumReportEntry struct { | ||
Time time.Time `json:"Time"` | ||
Action Action `json:"Action"` | ||
Package string `json:"Package"` | ||
Test string `json:"Test,omitempty"` | ||
Output string `json:"Output,omitempty"` | ||
Elapsed *float64 `json:"Elapsed,omitempty"` | ||
} | ||
|
||
type Action string | ||
|
||
const ( | ||
Cont Action = "cont" | ||
Fail Action = "fail" | ||
Output Action = "output" | ||
Pass Action = "pass" | ||
Pause Action = "pause" | ||
Run Action = "run" | ||
Skip Action = "skip" | ||
Start Action = "start" | ||
) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we run it with PostgreSQL too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do, see
Makefile
.