feat: artifacts client
All checks were successful
default / ensure tests work (push) Successful in 27s
All checks were successful
default / ensure tests work (push) Successful in 27s
This commit is contained in:
parent
24ad00274d
commit
d77ef9f9c6
1 changed files with 1364 additions and 6 deletions
198
internal/testreport/main.go
Normal file
198
internal/testreport/main.go
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
// 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()
|
||||
}
|
||||
Loading…
Reference in a new issue