github.com/redhat-appstudio/e2e-tests@v0.0.0-20230619105049-9a422b2094d7/pkg/framework/custom_junit_reporter.go (about) 1 /* 2 This is modified JUnit XML Reporter for Ginkgo framework. 3 Original version is available on https://github.com/onsi/ginkgo/blob/master/reporters/junit_report.go 4 5 Ginkgo project repository: https://github.com/onsi/ginkgo 6 7 MIT License: 8 Copyright (c) 2013-2014 Onsi Fakhouri 9 10 Permission is hereby granted, free of charge, to any person obtaining 11 a copy of this software and associated documentation files (the 12 "Software"), to deal in the Software without restriction, including 13 without limitation the rights to use, copy, modify, merge, publish, 14 distribute, sublicense, and/or sell copies of the Software, and to 15 permit persons to whom the Software is furnished to do so, subject to 16 the following conditions: 17 18 The above copyright notice and this permission notice shall be 19 included in all copies or substantial portions of the Software. 20 21 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 25 LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 26 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 27 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 */ 29 30 package framework 31 32 import ( 33 "crypto/sha1" 34 "encoding/hex" 35 "encoding/xml" 36 "fmt" 37 "os" 38 "strings" 39 40 . "github.com/onsi/ginkgo/v2/reporters" 41 types "github.com/onsi/ginkgo/v2/types" 42 "k8s.io/klog/v2" 43 ) 44 45 func GenerateCustomJUnitReport(report types.Report, dst string) error { 46 return GenerateCustomJUnitReportWithConfig(report, dst, JunitReportConfig{}) 47 } 48 49 func GenerateCustomJUnitReportWithConfig(report types.Report, dst string, config JunitReportConfig) error { 50 suite := JUnitTestSuite{ 51 Name: report.SuiteDescription, 52 Package: report.SuitePath, 53 Time: report.RunTime.Seconds(), 54 Timestamp: report.StartTime.Format("2006-01-02T15:04:05"), 55 Properties: JUnitProperties{ 56 Properties: []JUnitProperty{ 57 {"SuiteSucceeded", fmt.Sprintf("%t", report.SuiteSucceeded)}, 58 {"SuiteHasProgrammaticFocus", fmt.Sprintf("%t", report.SuiteHasProgrammaticFocus)}, 59 {"SpecialSuiteFailureReason", strings.Join(report.SpecialSuiteFailureReasons, ",")}, 60 {"SuiteLabels", fmt.Sprintf("[%s]", strings.Join(report.SuiteLabels, ","))}, 61 {"RandomSeed", fmt.Sprintf("%d", report.SuiteConfig.RandomSeed)}, 62 {"RandomizeAllSpecs", fmt.Sprintf("%t", report.SuiteConfig.RandomizeAllSpecs)}, 63 {"LabelFilter", report.SuiteConfig.LabelFilter}, 64 {"FocusStrings", strings.Join(report.SuiteConfig.FocusStrings, ",")}, 65 {"SkipStrings", strings.Join(report.SuiteConfig.SkipStrings, ",")}, 66 {"FocusFiles", strings.Join(report.SuiteConfig.FocusFiles, ";")}, 67 {"SkipFiles", strings.Join(report.SuiteConfig.SkipFiles, ";")}, 68 {"FailOnPending", fmt.Sprintf("%t", report.SuiteConfig.FailOnPending)}, 69 {"FailFast", fmt.Sprintf("%t", report.SuiteConfig.FailFast)}, 70 {"FlakeAttempts", fmt.Sprintf("%d", report.SuiteConfig.FlakeAttempts)}, 71 {"DryRun", fmt.Sprintf("%t", report.SuiteConfig.DryRun)}, 72 {"ParallelTotal", fmt.Sprintf("%d", report.SuiteConfig.ParallelTotal)}, 73 {"OutputInterceptorMode", report.SuiteConfig.OutputInterceptorMode}, 74 }, 75 }, 76 } 77 for _, spec := range report.SpecReports { 78 79 if spec.LeafNodeType != types.NodeTypeIt { 80 continue 81 } 82 test := JUnitTestCase{ 83 Name: shortenStringAddHash(spec), 84 Classname: getClassnameFromReport(spec), 85 Status: spec.State.String(), 86 Time: spec.RunTime.Seconds(), 87 } 88 if !spec.State.Is(config.OmitTimelinesForSpecState) { 89 test.SystemErr = systemErrForUnstructuredReporters(spec) 90 } 91 if !config.OmitCapturedStdOutErr { 92 test.SystemOut = systemOutForUnstructuredReporters(spec) 93 } 94 suite.Tests += 1 95 96 switch spec.State { 97 case types.SpecStateSkipped: 98 message := "skipped" 99 if spec.Failure.Message != "" { 100 message += " - " + spec.Failure.Message 101 } 102 test.Skipped = &JUnitSkipped{Message: message} 103 suite.Skipped += 1 104 case types.SpecStatePending: 105 test.Skipped = &JUnitSkipped{Message: "pending"} 106 suite.Disabled += 1 107 case types.SpecStateFailed: 108 test.Failure = &JUnitFailure{ 109 Message: spec.Failure.Message, 110 Type: "failed", 111 Description: fmt.Sprintf("%s\n%s", spec.Failure.Location.String(), spec.Failure.Location.FullStackTrace), 112 } 113 suite.Failures += 1 114 case types.SpecStateInterrupted: 115 test.Error = &JUnitError{ 116 Message: "interrupted", 117 Type: "interrupted", 118 Description: interruptDescriptionForUnstructuredReporters(spec.Failure), 119 } 120 suite.Errors += 1 121 case types.SpecStateAborted: 122 test.Failure = &JUnitFailure{ 123 Message: spec.Failure.Message, 124 Type: "aborted", 125 Description: fmt.Sprintf("%s\n%s", spec.Failure.Location.String(), spec.Failure.Location.FullStackTrace), 126 } 127 suite.Errors += 1 128 case types.SpecStatePanicked: 129 test.Error = &JUnitError{ 130 Message: spec.Failure.ForwardedPanic, 131 Type: "panicked", 132 Description: fmt.Sprintf("%s\n%s", spec.Failure.Location.String(), spec.Failure.Location.FullStackTrace), 133 } 134 suite.Errors += 1 135 } 136 137 suite.TestCases = append(suite.TestCases, test) 138 } 139 140 junitReport := JUnitTestSuites{ 141 Tests: suite.Tests, 142 Disabled: suite.Disabled + suite.Skipped, 143 Errors: suite.Errors, 144 Failures: suite.Failures, 145 Time: suite.Time, 146 TestSuites: []JUnitTestSuite{suite}, 147 } 148 149 f, err := os.Create(dst) 150 if err != nil { 151 return err 152 } 153 _, err2 := f.WriteString(xml.Header) 154 if err2 != nil { 155 klog.Error(err2) 156 } 157 encoder := xml.NewEncoder(f) 158 encoder.Indent(" ", " ") 159 err3 := encoder.Encode(junitReport) 160 if err3 != nil { 161 klog.Error(err3) 162 } 163 return f.Close() 164 } 165 166 // This function generates folder structure for the rp_preproc tool with logs for upload in Report Portal 167 func GenerateRPPreprocReport(report types.Report, rpPreprocParentDir string) { 168 rpPreprocDir := rpPreprocParentDir + "/rp_preproc" 169 //Delete directory, if exists 170 if _, err := os.Stat(rpPreprocDir); !os.IsNotExist(err) { 171 err2 := os.RemoveAll(rpPreprocDir) 172 if err2 != nil { 173 klog.Error(err2) 174 } 175 } 176 //Generate folder structure for RPPreproc with logs 177 for i := range report.SpecReports { 178 reportSpec := report.SpecReports[i] 179 //generate folders only for failed tests 180 if !reportSpec.Failure.IsZero() { 181 if reportSpec.LeafNodeType == types.NodeTypeIt { 182 name := shortenStringAddHash(reportSpec) 183 filePath := rpPreprocDir + "/attachments/xunit/" + name 184 if err3 := os.MkdirAll(filePath, os.ModePerm); err3 != nil { 185 klog.Error(err3) 186 } else { 187 writeLogInFile(filePath+"/ginkgoWriter.log", reportSpec.CapturedGinkgoWriterOutput) 188 writeLogInFile(filePath+"/stdOutErr.log", reportSpec.CapturedStdOutErr) 189 writeLogInFile(filePath+"/failureMessage.log", reportSpec.FailureMessage()) 190 writeLogInFile(filePath+"/failureLocation.log", reportSpec.FailureLocation().FullStackTrace) 191 } 192 } 193 } 194 } 195 } 196 197 func writeLogInFile(filePath string, log string) { 198 // Do not create empty files 199 if len(log) != 0 { 200 f, err := os.Create(filePath) 201 if err != nil { 202 klog.Error(err) 203 } 204 defer f.Close() 205 206 _, err2 := f.WriteString(log) 207 208 if err2 != nil { 209 klog.Error(err2) 210 } 211 } 212 } 213 214 func interruptDescriptionForUnstructuredReporters(failure types.Failure) string { 215 out := &strings.Builder{} 216 out.WriteString(failure.Message + "\n") 217 NewDefaultReporter(types.ReporterConfig{NoColor: true}, out).EmitProgressReport(failure.ProgressReport) 218 return out.String() 219 } 220 221 func systemErrForUnstructuredReporters(spec types.SpecReport) string { 222 out := &strings.Builder{} 223 gw := spec.CapturedGinkgoWriterOutput 224 cursor := 0 225 for _, pr := range spec.ProgressReports { 226 if cursor < pr.TimelineLocation.Offset { 227 if pr.TimelineLocation.Offset < len(gw) { 228 out.WriteString(gw[cursor:pr.TimelineLocation.Offset]) 229 cursor = pr.TimelineLocation.Offset 230 } else if cursor < len(gw) { 231 out.WriteString(gw[cursor:]) 232 cursor = len(gw) 233 } 234 } 235 NewDefaultReporter(types.ReporterConfig{NoColor: true}, out).EmitProgressReport(pr) 236 } 237 238 if cursor < len(gw) { 239 out.WriteString(gw[cursor:]) 240 } 241 return out.String() 242 } 243 244 func systemOutForUnstructuredReporters(spec types.SpecReport) string { 245 return spec.CapturedStdOutErr 246 } 247 248 func getClassnameFromReport(report types.SpecReport) string { 249 texts := []string{} 250 texts = append(texts, report.ContainerHierarchyTexts...) 251 if report.LeafNodeText != "" { 252 texts = append(texts, report.LeafNodeText) 253 } 254 if len(texts) > 0 { 255 classStrings := strings.Fields(texts[0]) 256 return classStrings[0][1:] 257 } else { 258 return strings.Join(texts, " ") 259 } 260 } 261 262 // This function is used to shorten classname and add hash to prevent issues with filesystems(255 chars for folder name) and to avoid conflicts(same shortened name of a classname) 263 func shortenStringAddHash(report types.SpecReport) string { 264 className := getClassnameFromReport(report) 265 s := report.FullText() 266 replacedClass := strings.Replace(s, className, "", 1) 267 if len(replacedClass) > 100 { 268 stringToHash := replacedClass[100:] 269 h := sha1.New() 270 h.Write([]byte(stringToHash)) 271 sha1_hash := hex.EncodeToString(h.Sum(nil)) 272 stringWithHash := replacedClass[0:100] + " sha: " + sha1_hash 273 return stringWithHash 274 } else { 275 return replacedClass 276 } 277 }