198 lines
4.3 KiB
Go
198 lines
4.3 KiB
Go
|
|
// Command testreport turns `go test -json` output into a markdown report. It
|
||
|
|
// prints the report to stdout and, when GITHUB_STEP_SUMMARY is set, appends it
|
||
|
|
// to the CI step summary so the pipeline shows what ran.
|
||
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bufio"
|
||
|
|
"encoding/json"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"os"
|
||
|
|
"sort"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type event struct {
|
||
|
|
Action string `json:"Action"`
|
||
|
|
Package string `json:"Package"`
|
||
|
|
Test string `json:"Test"`
|
||
|
|
Elapsed float64 `json:"Elapsed"`
|
||
|
|
Output string `json:"Output"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type testKey struct {
|
||
|
|
pkg string
|
||
|
|
name string
|
||
|
|
}
|
||
|
|
|
||
|
|
type testResult struct {
|
||
|
|
pkg string
|
||
|
|
name string
|
||
|
|
status string
|
||
|
|
dur time.Duration
|
||
|
|
output strings.Builder
|
||
|
|
}
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
in := io.Reader(os.Stdin)
|
||
|
|
if len(os.Args) > 1 {
|
||
|
|
f, err := os.Open(os.Args[1])
|
||
|
|
if err != nil {
|
||
|
|
fmt.Fprintln(os.Stderr, "testreport:", err)
|
||
|
|
os.Exit(1)
|
||
|
|
}
|
||
|
|
defer f.Close()
|
||
|
|
in = f
|
||
|
|
}
|
||
|
|
|
||
|
|
events, err := decode(in)
|
||
|
|
if err != nil {
|
||
|
|
fmt.Fprintln(os.Stderr, "testreport:", err)
|
||
|
|
os.Exit(1)
|
||
|
|
}
|
||
|
|
|
||
|
|
out := summarize(events)
|
||
|
|
fmt.Print(out)
|
||
|
|
|
||
|
|
if path := os.Getenv("GITHUB_STEP_SUMMARY"); path != "" {
|
||
|
|
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||
|
|
if err != nil {
|
||
|
|
fmt.Fprintln(os.Stderr, "testreport:", err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
defer f.Close()
|
||
|
|
if _, err := f.WriteString(out); err != nil {
|
||
|
|
fmt.Fprintln(os.Stderr, "testreport:", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func decode(r io.Reader) ([]event, error) {
|
||
|
|
var events []event
|
||
|
|
dec := json.NewDecoder(bufio.NewReader(r))
|
||
|
|
for {
|
||
|
|
var e event
|
||
|
|
err := dec.Decode(&e)
|
||
|
|
if err == io.EOF {
|
||
|
|
return events, nil
|
||
|
|
}
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
events = append(events, e)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func summarize(events []event) string {
|
||
|
|
tests := map[testKey]*testResult{}
|
||
|
|
var testOrder []testKey
|
||
|
|
pkgOrder := []string{}
|
||
|
|
pkgSeen := map[string]bool{}
|
||
|
|
pkgFailed := map[string]bool{}
|
||
|
|
pkgOutput := map[string]strings.Builder{}
|
||
|
|
|
||
|
|
for _, e := range events {
|
||
|
|
if e.Package == "" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
if !pkgSeen[e.Package] {
|
||
|
|
pkgSeen[e.Package] = true
|
||
|
|
pkgOrder = append(pkgOrder, e.Package)
|
||
|
|
}
|
||
|
|
|
||
|
|
if e.Test == "" {
|
||
|
|
switch e.Action {
|
||
|
|
case "output":
|
||
|
|
b := pkgOutput[e.Package]
|
||
|
|
b.WriteString(e.Output)
|
||
|
|
pkgOutput[e.Package] = b
|
||
|
|
case "fail":
|
||
|
|
pkgFailed[e.Package] = true
|
||
|
|
}
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
key := testKey{e.Package, e.Test}
|
||
|
|
res, ok := tests[key]
|
||
|
|
if !ok {
|
||
|
|
res = &testResult{pkg: e.Package, name: e.Test}
|
||
|
|
tests[key] = res
|
||
|
|
testOrder = append(testOrder, key)
|
||
|
|
}
|
||
|
|
|
||
|
|
switch e.Action {
|
||
|
|
case "output":
|
||
|
|
res.output.WriteString(e.Output)
|
||
|
|
case "pass", "fail", "skip":
|
||
|
|
res.status = e.Action
|
||
|
|
res.dur = time.Duration(e.Elapsed * float64(time.Second))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
sort.Slice(testOrder, func(i, j int) bool {
|
||
|
|
if testOrder[i].pkg != testOrder[j].pkg {
|
||
|
|
return testOrder[i].pkg < testOrder[j].pkg
|
||
|
|
}
|
||
|
|
return testOrder[i].name < testOrder[j].name
|
||
|
|
})
|
||
|
|
|
||
|
|
var passed, failed, skipped int
|
||
|
|
for _, res := range tests {
|
||
|
|
switch res.status {
|
||
|
|
case "pass":
|
||
|
|
passed++
|
||
|
|
case "fail":
|
||
|
|
failed++
|
||
|
|
case "skip":
|
||
|
|
skipped++
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var b strings.Builder
|
||
|
|
b.WriteString("## Go test report\n\n")
|
||
|
|
fmt.Fprintf(&b, "**%d passed, %d failed, %d skipped**\n\n", passed, failed+len(pkgFailed), skipped)
|
||
|
|
|
||
|
|
if len(testOrder) > 0 {
|
||
|
|
b.WriteString("| Package | Test | Status | Duration |\n")
|
||
|
|
b.WriteString("| --- | --- | --- | --- |\n")
|
||
|
|
for _, key := range testOrder {
|
||
|
|
res := tests[key]
|
||
|
|
status := res.status
|
||
|
|
if status == "" {
|
||
|
|
status = "unknown"
|
||
|
|
}
|
||
|
|
fmt.Fprintf(&b, "| %s | %s | %s | %s |\n", res.pkg, res.name, status, res.dur.Round(time.Millisecond))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
var failures []string
|
||
|
|
for _, key := range testOrder {
|
||
|
|
if tests[key].status == "fail" {
|
||
|
|
failures = append(failures, fmt.Sprintf("%s.%s", tests[key].pkg, tests[key].name))
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if len(failures) > 0 || len(pkgFailed) > 0 {
|
||
|
|
b.WriteString("\n### Failures\n\n")
|
||
|
|
for _, key := range testOrder {
|
||
|
|
res := tests[key]
|
||
|
|
if res.status != "fail" {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
fmt.Fprintf(&b, "<details><summary>%s.%s</summary>\n\n```\n%s```\n\n</details>\n\n", res.pkg, res.name, res.output.String())
|
||
|
|
}
|
||
|
|
failedPkgs := make([]string, 0, len(pkgFailed))
|
||
|
|
for pkg := range pkgFailed {
|
||
|
|
failedPkgs = append(failedPkgs, pkg)
|
||
|
|
}
|
||
|
|
sort.Strings(failedPkgs)
|
||
|
|
for _, pkg := range failedPkgs {
|
||
|
|
output := pkgOutput[pkg]
|
||
|
|
fmt.Fprintf(&b, "<details><summary>%s</summary>\n\n```\n%s```\n\n</details>\n\n", pkg, output.String())
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return b.String()
|
||
|
|
}
|