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  }