github.com/zppinho/prow@v0.0.0-20240510014325-1738badeb017/pkg/spyglass/lenses/junit/lens.go (about) 1 /* 2 Copyright 2018 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package junit provides a junit viewer for Spyglass 18 package junit 19 20 import ( 21 "bytes" 22 "encoding/json" 23 "fmt" 24 "html/template" 25 "path/filepath" 26 "sort" 27 "time" 28 29 "github.com/GoogleCloudPlatform/testgrid/metadata/junit" 30 "github.com/sirupsen/logrus" 31 32 "sigs.k8s.io/prow/pkg/config" 33 "sigs.k8s.io/prow/pkg/spyglass/api" 34 "sigs.k8s.io/prow/pkg/spyglass/lenses" 35 ) 36 37 const ( 38 name = "junit" 39 title = "JUnit" 40 priority = 5 41 passedStatus testStatus = "Passed" 42 failedStatus testStatus = "Failed" 43 skippedStatus testStatus = "Skipped" 44 ) 45 46 func init() { 47 lenses.RegisterLens(Lens{}) 48 } 49 50 type testStatus string 51 52 // Lens is the implementation of a JUnit-rendering Spyglass lens. 53 type Lens struct{} 54 55 type JVD struct { 56 NumTests int 57 Passed []TestResult 58 Failed []TestResult 59 Skipped []TestResult 60 Flaky []TestResult 61 } 62 63 // Config returns the lens's configuration. 64 func (lens Lens) Config() lenses.LensConfig { 65 return lenses.LensConfig{ 66 Name: name, 67 Title: title, 68 Priority: priority, 69 } 70 } 71 72 // Header renders the content of <head> from template.html. 73 func (lens Lens) Header(artifacts []api.Artifact, resourceDir string, config json.RawMessage, spyglassConfig config.Spyglass) string { 74 t, err := template.ParseFiles(filepath.Join(resourceDir, "template.html")) 75 if err != nil { 76 return fmt.Sprintf("<!-- FAILED LOADING HEADER: %v -->", err) 77 } 78 var buf bytes.Buffer 79 if err := t.ExecuteTemplate(&buf, "header", nil); err != nil { 80 return fmt.Sprintf("<!-- FAILED EXECUTING HEADER TEMPLATE: %v -->", err) 81 } 82 return buf.String() 83 } 84 85 // Callback does nothing. 86 func (lens Lens) Callback(artifacts []api.Artifact, resourceDir string, data string, config json.RawMessage, spyglassConfig config.Spyglass) string { 87 return "" 88 } 89 90 type JunitResult struct { 91 junit.Result 92 } 93 94 func (jr JunitResult) Duration() time.Duration { 95 return time.Duration(jr.Time * float64(time.Second)).Round(time.Second) 96 } 97 98 func (jr JunitResult) Status() testStatus { 99 res := passedStatus 100 if jr.Skipped != nil { 101 res = skippedStatus 102 } else if jr.Failure != nil || jr.Errored != nil { 103 res = failedStatus 104 } 105 return res 106 } 107 108 func (jr JunitResult) SkippedReason() string { 109 res := "" 110 if jr.Skipped != nil { 111 res = jr.Message(-1) // Don't truncate 112 } 113 return res 114 } 115 116 // TestResult holds data about a test extracted from junit output 117 type TestResult struct { 118 Junit []JunitResult 119 Link string 120 } 121 122 // Body renders the <body> for JUnit tests 123 func (lens Lens) Body(artifacts []api.Artifact, resourceDir string, data string, config json.RawMessage, spyglassConfig config.Spyglass) string { 124 jvd := lens.getJvd(artifacts) 125 126 junitTemplate, err := template.ParseFiles(filepath.Join(resourceDir, "template.html")) 127 if err != nil { 128 logrus.WithError(err).Error("Error executing template.") 129 return fmt.Sprintf("Failed to load template file: %v", err) 130 } 131 132 var buf bytes.Buffer 133 if err := junitTemplate.ExecuteTemplate(&buf, "body", jvd); err != nil { 134 logrus.WithError(err).Error("Error executing template.") 135 } 136 137 return buf.String() 138 } 139 140 func (lens Lens) getJvd(artifacts []api.Artifact) JVD { 141 type testResults struct { 142 // Group results based on their full path name 143 junit [][]JunitResult 144 link string 145 path string 146 err error 147 } 148 type testIdentifier struct { 149 suite string 150 class string 151 name string 152 } 153 resultChan := make(chan testResults) 154 for _, artifact := range artifacts { 155 go func(artifact api.Artifact) { 156 groups := make(map[testIdentifier][]JunitResult) 157 var testsSequence []testIdentifier 158 result := testResults{ 159 link: artifact.CanonicalLink(), 160 path: artifact.JobPath(), 161 } 162 var contents []byte 163 contents, result.err = artifact.ReadAll() 164 if result.err != nil { 165 logrus.WithError(result.err).WithField("artifact", artifact.CanonicalLink()).Warn("Error reading artifact") 166 resultChan <- result 167 return 168 } 169 var suites *junit.Suites 170 suites, result.err = junit.Parse(contents) 171 if result.err != nil { 172 logrus.WithError(result.err).WithField("artifact", artifact.CanonicalLink()).Info("Error parsing junit file.") 173 resultChan <- result 174 return 175 } 176 var record func(suite junit.Suite) 177 record = func(suite junit.Suite) { 178 for _, subSuite := range suite.Suites { 179 record(subSuite) 180 } 181 182 for _, test := range suite.Results { 183 // There are cases where multiple entries of exactly the same 184 // testcase in a single junit result file, this could result 185 // from reruns of test cases by `go test --count=N` where N>1. 186 // Deduplicate them here in this case, and classify a test as being 187 // flaky if it both succeeded and failed 188 k := testIdentifier{suite.Name, test.ClassName, test.Name} 189 groups[k] = append(groups[k], JunitResult{Result: test}) 190 if len(groups[k]) == 1 { 191 testsSequence = append(testsSequence, k) 192 } 193 } 194 } 195 for _, suite := range suites.Suites { 196 record(suite) 197 } 198 for _, identifier := range testsSequence { 199 result.junit = append(result.junit, groups[identifier]) 200 } 201 resultChan <- result 202 }(artifact) 203 } 204 results := make([]testResults, 0, len(artifacts)) 205 for range artifacts { 206 results = append(results, <-resultChan) 207 } 208 sort.Slice(results, func(i, j int) bool { return results[i].path < results[j].path }) 209 210 var jvd JVD 211 var duplicates int 212 213 for _, result := range results { 214 if result.err != nil { 215 continue 216 } 217 for _, tests := range result.junit { 218 var ( 219 skipped bool 220 passed bool 221 failed bool 222 flaky bool 223 ) 224 for _, test := range tests { 225 // skipped test has no reason to rerun, so no deduplication 226 if test.Status() == skippedStatus { 227 skipped = true 228 } else if test.Status() == failedStatus { 229 if passed { 230 passed = false 231 failed = false 232 flaky = true 233 } 234 if !flaky { 235 failed = true 236 } 237 } else if failed { // Test succeeded but marked failed previously 238 passed = false 239 failed = false 240 flaky = true 241 } else if !flaky { // Test succeeded and not marked as flaky 242 passed = true 243 } 244 } 245 246 if skipped { 247 jvd.Skipped = append(jvd.Skipped, TestResult{ 248 Junit: tests, 249 Link: result.link, 250 }) 251 // if the skipped test is a rerun of a failed test 252 if failed { 253 // store it as failed too 254 jvd.Failed = append(jvd.Failed, TestResult{ 255 Junit: tests, 256 Link: result.link, 257 }) 258 // account for the duplication 259 duplicates++ 260 } 261 } else if failed { 262 jvd.Failed = append(jvd.Failed, TestResult{ 263 Junit: tests, 264 Link: result.link, 265 }) 266 } else if flaky { 267 jvd.Flaky = append(jvd.Flaky, TestResult{ 268 Junit: tests, 269 Link: result.link, 270 }) 271 } else { 272 jvd.Passed = append(jvd.Passed, TestResult{ 273 Junit: tests, 274 Link: result.link, 275 }) 276 } 277 } 278 } 279 280 jvd.NumTests = len(jvd.Passed) + len(jvd.Failed) + len(jvd.Flaky) + len(jvd.Skipped) - duplicates 281 return jvd 282 }