github.com/jenkins-x/jx/v2@v2.1.155/pkg/cmd/step/report/step_report_junit.go (about) 1 package report 2 3 import ( 4 "encoding/xml" 5 "io/ioutil" 6 "os" 7 "path/filepath" 8 9 "github.com/google/uuid" 10 "github.com/jenkins-x/jx-logging/pkg/log" 11 "github.com/jenkins-x/jx/v2/pkg/cmd/helper" 12 "github.com/jenkins-x/jx/v2/pkg/cmd/opts" 13 "github.com/jenkins-x/jx/v2/pkg/cmd/opts/step" 14 "github.com/jenkins-x/jx/v2/pkg/cmd/templates" 15 "github.com/jenkins-x/jx/v2/pkg/reportingtools" 16 "github.com/jenkins-x/jx/v2/pkg/util" 17 "github.com/pkg/errors" 18 "github.com/spf13/cobra" 19 ) 20 21 var ( 22 stepReportJUnitLong = templates.LongDesc(`This step is used to generate an HTML report from *.junit.xml files created from running BDD tests.`) 23 stepReportJUnitExample = templates.Examples(` 24 # Collect every *.junit.xml file from --in-dir, merge them, and store them in --out-dir with a file name --output-name and provide an HTML report title 25 jx step report --in-dir /randomdir --out-dir /outdir --merge --output-name resulting_report.html --suite-name This_is_the_report_title 26 27 # Collect every *.junit.xml file without defining --in-dir and use the value of $REPORTS_DIR , merge them, and store them in --out-dir with a file name --output-name 28 jx step report --out-dir /outdir --merge --output-name resulting_report.html 29 30 # Select a single *.junit.xml file and create a report form it 31 jx step report --in-dir /randomdir --out-dir /outdir --target-report test.junit.xml --output-name resulting_report.html 32 `) 33 ) 34 35 // StepReportJUnitOptions contains the command line flags and other helper objects 36 type StepReportJUnitOptions struct { 37 StepReportOptions 38 reportingtools.XUnitClient 39 MergeReports bool 40 ReportsDir string 41 TargetReport string 42 SuiteName string 43 OutputReportName string 44 DeleteReportFn func(reportName string) error 45 } 46 47 // TestSuites is the representation of the root of a *.junit.xml xml file 48 type TestSuites struct { 49 XMLName xml.Name `xml:"testsuites"` 50 Text string `xml:",chardata"` 51 TestSuites []TestSuite `xml:"testsuite"` 52 } 53 54 // TestSuite is the representation of a <testsuite> of a *.junit.xml xml file 55 type TestSuite struct { 56 XMLName xml.Name `xml:"testsuite"` 57 Text string `xml:",chardata"` 58 Name string `xml:"name,attr"` 59 Tests string `xml:"tests,attr"` 60 Failures string `xml:"failures,attr"` 61 Errors string `xml:"errors,attr"` 62 Time string `xml:"time,attr"` 63 TestCase []TestCase `xml:"testcase"` 64 } 65 66 // TestCase is the representation of an individual test case within a TestSuite in a *.junit.xml xml file 67 type TestCase struct { 68 XMLName xml.Name `xml:"testcase"` 69 Text string `xml:",chardata"` 70 Name string `xml:"name,attr"` 71 Classname string `xml:"classname,attr"` 72 Time string `xml:"time,attr"` 73 Failure *Failure `xml:"failure,omitempty"` 74 SystemOut string `xml:"system-out"` 75 } 76 77 // Failure is the representation of a Failure that can be present in a TestCase within a TestSuite in a *.junit.xml xml file 78 type Failure struct { 79 Text string `xml:",chardata"` 80 Type string `xml:"type,attr"` 81 } 82 83 // NewCmdStepReportJUnit Creates a new Command object 84 func NewCmdStepReportJUnit(commonOpts *opts.CommonOptions) *cobra.Command { 85 options := &StepReportJUnitOptions{ 86 StepReportOptions: StepReportOptions{ 87 StepOptions: step.StepOptions{ 88 CommonOptions: commonOpts, 89 }, 90 }, 91 } 92 93 cmd := &cobra.Command{ 94 Use: "junit", 95 Short: "Creates a HTML report from junit files", 96 Long: stepReportJUnitLong, 97 Example: stepReportJUnitExample, 98 Run: func(cmd *cobra.Command, args []string) { 99 options.Cmd = cmd 100 options.Args = args 101 err := options.Run() 102 helper.CheckErr(err) 103 }, 104 } 105 106 options.StepReportOptions.AddReportFlags(cmd) 107 108 cmd.Flags().StringVarP(&options.ReportsDir, "in-dir", "f", "", "The directory to get the reports from") 109 cmd.Flags().StringVarP(&options.OutputReportName, "output-name", "n", "", "The result of parsing the report(s) in HTML format") 110 cmd.Flags().StringVarP(&options.TargetReport, "target-report", "t", "", "The name of a single report file to parse") 111 cmd.Flags().StringVarP(&options.SuiteName, "suite-name", "s", "", "The name of the tests suite to be shown in the HTML report") 112 cmd.Flags().BoolVarP(&options.MergeReports, "merge", "m", false, "Whether or not to merge the report files in the \"in-folder\" to parse them and show it as a single test run") 113 114 return cmd 115 } 116 117 // Run generates the report 118 func (o *StepReportJUnitOptions) Run() error { 119 if o.XUnitClient == nil { 120 o.XUnitClient = reportingtools.XUnitViewer{} 121 } 122 123 if o.DeleteReportFn == nil { 124 o.DeleteReportFn = util.DeleteFile 125 } 126 127 //We want to finish gracefully, otherwise the pipeline would fail 128 err := o.XUnitClient.EnsureXUnitViewer(o.CommonOptions) 129 if err != nil { 130 return logErrorAndExitGracefully("there was a problem ensuring the presence of xunit-viewer", err) 131 } 132 133 // check $REPORTS_DIR is set, overridden by "in-folder" 134 if o.ReportsDir == "" { 135 o.ReportsDir = os.Getenv("REPORTS_DIR") 136 } 137 138 matchingReportFiles, err := o.obtainingMatchingReportFiles() 139 if err != nil { 140 return logErrorAndExitGracefully("there was a problem obtaining the matching report files", err) 141 } 142 143 targetFileName, err := generateTargetParsableReportName() 144 defer o.DeleteReportFn(targetFileName) //nolint:errcheck 145 if err != nil { 146 return logErrorAndExitGracefully("there was a problem generating a parsable temporary file", err) 147 } 148 149 // if "merge" is true, merge every xml file into one, called <targetFileName>, if there's only one file, rename to <targetFileName> 150 if o.MergeReports { 151 err = o.mergeJUnitReportFiles(matchingReportFiles, targetFileName) 152 if err != nil { 153 return logErrorAndExitGracefully("there was a problem merging the junit reports: %+v", err) 154 } 155 } else { 156 err = o.prepareSingleFileForParsing(targetFileName) 157 if err != nil { 158 return logErrorAndExitGracefully("there was a problem preparing a single junit report: %+v", err) 159 } 160 } 161 162 if o.OutputDir != "" { 163 o.OutputReportName = filepath.Join(o.OutputDir, o.OutputReportName) 164 } 165 166 // Generate report with xunit-viewer from <targetFileName> 167 err = o.XUnitClient.CreateHTMLReport(o.OutputReportName, o.SuiteName, targetFileName) 168 if err != nil { 169 return logErrorAndExitGracefully("error creating the HTML report", err) 170 } 171 return nil 172 } 173 174 func generateTargetParsableReportName() (string, error) { 175 fileName := uuid.New().String() + ".xml" 176 xunitReportsPath := filepath.Join(os.TempDir(), "xunit-reports") 177 err := os.MkdirAll(xunitReportsPath, os.ModePerm) 178 if err != nil { 179 return "", errors.Wrap(err, "there was a problem creating the xunit-reports dir in the temp dir") 180 } 181 fileName = filepath.Join(xunitReportsPath, fileName) 182 return fileName, nil 183 } 184 185 func (o *StepReportJUnitOptions) obtainingMatchingReportFiles() ([]string, error) { 186 matchingReportFiles, err := filepath.Glob(filepath.Join(o.ReportsDir, "*.junit.xml")) 187 if err != nil { 188 return nil, errors.Wrapf(err, "There was an error reading the report files in directory %s", o.ReportsDir) 189 } 190 191 if matchingReportFiles == nil { 192 return nil, errors.Errorf("no report files to parse in %s, skipping", o.ReportsDir) 193 } 194 return matchingReportFiles, nil 195 } 196 197 func (o *StepReportJUnitOptions) prepareSingleFileForParsing(resultFileName string) error { 198 if o.TargetReport == "" { 199 return errors.New("the TargetReport name is empty, parsing will ber skipped") 200 } 201 err := util.CopyFile(filepath.Join(o.ReportsDir, o.TargetReport), resultFileName) 202 if err != nil { 203 return errors.Wrap(err, "there was a problem copying the report file to temp directory, skipping") 204 } 205 return nil 206 } 207 208 func (o *StepReportJUnitOptions) mergeJUnitReportFiles(jUnitReportFiles []string, resultFileName string) error { 209 log.Logger().Infof(util.ColorInfo("Performing merge of *.junit.xml files in %s"), o.ReportsDir) 210 211 aggregatedTestSuites := TestSuites{} 212 for _, v := range jUnitReportFiles { 213 bytes, err := ioutil.ReadFile(v) 214 if err != nil { 215 return err 216 } 217 218 // trying to parse <testsuites></testsuites> 219 var testSuites TestSuites 220 err = xml.Unmarshal(bytes, &testSuites) 221 if err != nil { 222 // If no <testsuites></testsuites>, trying to parse <testsuite></testsuite> 223 var testSuite TestSuite 224 err = xml.Unmarshal(bytes, &testSuite) 225 if err != nil { 226 return err 227 } 228 aggregatedTestSuites.TestSuites = append(aggregatedTestSuites.TestSuites, testSuite) 229 } else { 230 for _, testSuite := range testSuites.TestSuites { 231 aggregatedTestSuites.TestSuites = append(aggregatedTestSuites.TestSuites, testSuite) 232 } 233 } 234 } 235 236 suitesBytes, err := xml.Marshal(aggregatedTestSuites) 237 if err != nil { 238 return err 239 } 240 241 err = ioutil.WriteFile(resultFileName, suitesBytes, 0600) 242 if err != nil { 243 return err 244 } 245 246 return nil 247 } 248 249 func logErrorAndExitGracefully(message string, err error) error { 250 log.Logger().Errorf("%s: %+v", message, err.Error()) 251 return nil 252 }