Skip to content

Commit 1769697

Browse files
committed
htmlreport: Add new command that creates HTML reports of SIG test case failures
This new command will primarily used by periodic prow jobs to generate daily reports on each specific SIG's tescase failures. These reports will help SIG members to identify and tackle the faiures. The reports will be stored on the kubevirt GCS bucket and can be linked to from the ci-health README where we track the number of SIG e2e failures Signed-off-by: Brian Carey <bcarey@redhat.com>
1 parent 3da4cc0 commit 1769697

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
@@ -165,3 +165,12 @@ To generate plots you should execute:
165165
$ 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
166166
```
167167
Plot mode requires data previously generated by fetch mode.
168+
169+
### html-report command
170+
171+
A command that generates HTML reports of test case failures per SIG. To create a report for SIG Compute failures:
172+
```
173+
go run ./cmd/html-report --sig compute --results-path ./output/kubevirt/kubevirt/results.json --path /tmp/
174+
```
175+
176+
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
@@ -18,6 +18,7 @@ const (
1818
SIGRetests = "SIGRetests"
1919

2020
DefaultPath = "/tmp/test"
21+
DefaultResultsPath = ""
2122
DefaultTokenPath = ""
2223
DefaultSource = "kubevirt/kubevirt"
2324
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)