Skip to content

Commit d8b511e

Browse files
authored
Merge pull request #92 from kubevirt/html-report
2 parents c1c58e2 + 1769697 commit d8b511e

File tree

20 files changed

+1237
-2
lines changed

20 files changed

+1237
-2
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,3 +177,12 @@ To generate plots you should execute:
177177
$ go run ./cmd/batch --gh-token /path/to/token --source <org/repo> --path $(pwd)/output --mode plot --target-metric merged-prs --start-date 2020-05-19
178178
```
179179
Plot mode requires data previously generated by fetch mode.
180+
181+
### html-report command
182+
183+
A command that generates HTML reports of test case failures per SIG. To create a report for SIG Compute failures:
184+
```
185+
go run ./cmd/html-report --sig compute --results-path ./output/kubevirt/kubevirt/results.json --path /tmp/
186+
```
187+
188+
This should create a HTML report called sig-compute-failure-report.html under /tmp/.

cmd/html-report/main.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
log "github.com/sirupsen/logrus"
7+
"github.com/spf13/cobra"
8+
9+
"github.com/kubevirt/ci-health/pkg/constants"
10+
"github.com/kubevirt/ci-health/pkg/htmlreport"
11+
"github.com/kubevirt/ci-health/pkg/types"
12+
)
13+
14+
func main() {
15+
opt := &types.Options{
16+
Path: constants.DefaultPath,
17+
Sig: "compute",
18+
ResultsPath: constants.DefaultResultsPath,
19+
}
20+
21+
cmd := &cobra.Command{
22+
Run: func(cmd *cobra.Command, arguments []string) {
23+
if err := htmlreport.Generate(opt); err != nil {
24+
log.Fatalf("error generating HTML report: %v", err)
25+
}
26+
reportPath := fmt.Sprintf("%s/sig-%s-failure-report.html", opt.Path, opt.Sig)
27+
log.Printf("HTML report written to %s", reportPath)
28+
},
29+
}
30+
flag := cmd.Flags()
31+
32+
flag.StringVar(&opt.Path, "path", opt.Path, "Path to output HTML reports to")
33+
flag.StringVar(&opt.Sig, "sig", opt.Sig, "Name of SIG to generate failure report for")
34+
flag.StringVar(&opt.ResultsPath, "results-path", opt.ResultsPath, "Path to the results.json file created by ci-health")
35+
36+
if err := cmd.Execute(); err != nil {
37+
log.Fatalf("error: %v", err)
38+
}
39+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ toolchain go1.24.3
66

77
require (
88
github.com/avast/retry-go v3.0.0+incompatible
9+
github.com/joshdk/go-junit v1.0.0
910
github.com/narqo/go-badge v0.0.0-20230821190521-c9a75c019a59
1011
github.com/onsi/ginkgo v1.16.5
1112
github.com/onsi/gomega v1.27.10

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
6464
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
6565
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
6666
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
67+
github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE=
68+
github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung=
6769
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
6870
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
6971
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=

pkg/constants/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const (
1919
QuarantineStats = "QuarantineStats"
2020

2121
DefaultPath = "/tmp/test"
22+
DefaultResultsPath = ""
2223
DefaultTokenPath = ""
2324
DefaultSource = "kubevirt/kubevirt"
2425
DefaultDataDays = 7

pkg/htmlreport/main.go

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package htmlreport
2+
3+
import (
4+
_ "embed"
5+
"encoding/json"
6+
"encoding/xml"
7+
"fmt"
8+
"html/template"
9+
"io"
10+
"net/http"
11+
"os"
12+
"regexp"
13+
"strings"
14+
15+
"github.com/joshdk/go-junit"
16+
"github.com/kubevirt/ci-health/pkg/sigretests"
17+
"github.com/kubevirt/ci-health/pkg/types"
18+
log "github.com/sirupsen/logrus"
19+
)
20+
21+
var (
22+
//go:embed sig-failure-report.gohtml
23+
sigFailureReportTemplate string
24+
)
25+
26+
type HTMLReportResults struct {
27+
Data struct {
28+
SIGRetests struct {
29+
FailedJobLeaderBoard []types.FailedJob `json:"FailedJobLeaderBoard"`
30+
} `json:"SIGRetests"`
31+
} `json:"Data"`
32+
}
33+
34+
type Failure struct {
35+
XMLName xml.Name `xml:"failure"`
36+
Message string `xml:"message,attr"`
37+
Type string `xml:"type,attr"`
38+
Value string `xml:",chardata"`
39+
}
40+
41+
type SigFailure struct {
42+
Sig string
43+
JobName string
44+
FailureURL string
45+
Testcase []junit.Test
46+
}
47+
48+
var jobRegexAliases = map[string]string{
49+
"compute": "sig-compute$|sig-compute-serial$|sig-compute-migrations$|sig-operator$|vgpu$|sev$",
50+
"network": "sig-network$|sriov$",
51+
"storage": "sig-storage$",
52+
}
53+
54+
func fetchResults(resultsPath string) (*HTMLReportResults, error) {
55+
body, err := os.ReadFile(resultsPath)
56+
if err != nil {
57+
return nil, fmt.Errorf("failed to read results.json file: %w", err)
58+
}
59+
60+
var results HTMLReportResults
61+
if err := json.Unmarshal(body, &results); err != nil {
62+
return nil, fmt.Errorf("failed to unmarshal results.json: %w", err)
63+
}
64+
65+
return &results, nil
66+
}
67+
68+
func fetchJunit(url string) ([]junit.Suite, error) {
69+
resp, err := sigretests.HttpGetWithRetry(url)
70+
if err != nil {
71+
return nil, fmt.Errorf("failed to fetch %s: %s", url, err)
72+
}
73+
defer resp.Body.Close()
74+
75+
// Ignore missing junit files as it suggests an issue with the job
76+
if resp.StatusCode == http.StatusNotFound {
77+
return nil, nil
78+
}
79+
80+
if resp.StatusCode != http.StatusOK {
81+
return nil, fmt.Errorf("failed to fetch %s: status code %d", url, resp.StatusCode)
82+
}
83+
84+
body, err := io.ReadAll(resp.Body)
85+
if err != nil {
86+
return nil, fmt.Errorf("failed to read %s body: %w", url, err)
87+
}
88+
89+
testsuite, err := junit.Ingest(body)
90+
if err == nil {
91+
return testsuite, nil
92+
}
93+
94+
return nil, fmt.Errorf("failed to unmarshal junit.functest.xml as <testsuites> or <testsuite>")
95+
}
96+
97+
func constructReportFilePath(opt *types.Options) string {
98+
return fmt.Sprintf("%s/sig-%s-failure-report.html", opt.Path, opt.Sig)
99+
}
100+
101+
func constructJunitURL(failureURL string) string {
102+
junitURL := strings.Replace(failureURL, "prow.ci.kubevirt.io//view/gs", "gcsweb.ci.kubevirt.io/gcs", 1)
103+
if !strings.HasSuffix(junitURL, "/") {
104+
junitURL += "/"
105+
}
106+
junitURL += "artifacts/junit.functest.xml"
107+
return junitURL
108+
}
109+
110+
func Generate(opt *types.Options) error {
111+
112+
if opt.ResultsPath == "" {
113+
return fmt.Errorf("the path to results.json is required")
114+
}
115+
116+
jobRegex, ok := jobRegexAliases[opt.Sig]
117+
if !ok {
118+
return fmt.Errorf("unknown SIG: %s", opt.Sig)
119+
}
120+
121+
results, err := fetchResults(opt.ResultsPath)
122+
if err != nil {
123+
return fmt.Errorf("failed to parse results.json: %w", err)
124+
}
125+
126+
compiledRegex, err := regexp.Compile(jobRegex)
127+
if err != nil {
128+
return fmt.Errorf("invalid job regex provided: %w", err)
129+
}
130+
131+
var sigFailures []SigFailure
132+
133+
for _, job := range results.Data.SIGRetests.FailedJobLeaderBoard {
134+
if !compiledRegex.MatchString(job.JobName) {
135+
continue
136+
}
137+
for _, failureURL := range job.FailureURLs {
138+
var sigFail SigFailure
139+
junitURL := constructJunitURL(failureURL)
140+
testSuites, err := fetchJunit(junitURL)
141+
if err != nil {
142+
log.Warnf("failed to fetch junit results: %s", err)
143+
continue
144+
}
145+
if testSuites == nil {
146+
// SIG CI failure
147+
continue
148+
}
149+
sigFail.Sig = opt.Sig
150+
sigFail.JobName = job.JobName
151+
sigFail.FailureURL = failureURL
152+
153+
for _, suite := range testSuites {
154+
for _, test := range suite.Tests {
155+
if test.Status == junit.StatusFailed {
156+
sigFail.Testcase = append(sigFail.Testcase, test)
157+
}
158+
}
159+
}
160+
sigFailures = append(sigFailures, sigFail)
161+
}
162+
163+
}
164+
165+
reportTemplate, err := template.New("sigFailures").Parse(sigFailureReportTemplate)
166+
if err != nil {
167+
return fmt.Errorf("could not read template: %w", err)
168+
}
169+
170+
outputFile, err := os.Create(constructReportFilePath(opt))
171+
if err != nil {
172+
return fmt.Errorf("could not create report file: %w", err)
173+
}
174+
defer outputFile.Close()
175+
176+
err = reportTemplate.Execute(outputFile, sigFailures)
177+
if err != nil {
178+
return fmt.Errorf("could not execute template: %w", err)
179+
}
180+
181+
return nil
182+
}

0 commit comments

Comments
 (0)