github.com/saucelabs/saucectl@v0.175.1/internal/junit/junit.go (about)

     1  package junit
     2  
     3  import (
     4  	"encoding/xml"
     5  	"fmt"
     6  
     7  	"golang.org/x/exp/maps"
     8  )
     9  
    10  // FileName is the name of the JUnit report.
    11  const FileName = "junit.xml"
    12  
    13  // Property maps to a <property> element that's part of <properties>.
    14  type Property struct {
    15  	Name  string `xml:"name,attr"`
    16  	Value string `xml:"value,attr"`
    17  }
    18  
    19  // TestCase maps to <testcase> element
    20  type TestCase struct {
    21  	Name string `xml:"name,attr"`
    22  	// Assertions is the number of assertions in the test case.
    23  	Assertions string `xml:"assertions,attr,omitempty"`
    24  	// Time in seconds it took to run the test case.
    25  	Time string `xml:"time,attr"`
    26  	// Timestamp as specified by ISO 8601 (2014-01-21T16:17:18).
    27  	// Timezone is optional.
    28  	Timestamp string `xml:"timestamp,attr"`
    29  	ClassName string `xml:"classname,attr"`
    30  	// Status indicates success or failure of the test. May be used instead of
    31  	// Error, Failure or Skipped or in addition to them.
    32  	Status    string   `xml:"status,attr,omitempty"`
    33  	File      string   `xml:"file,attr,omitempty"`
    34  	SystemErr string   `xml:"system-err,omitempty"`
    35  	SystemOut string   `xml:"system-out,omitempty"`
    36  	Error     *Error   `xml:"error,omitempty"`
    37  	Failure   *Failure `xml:"failure,omitempty"`
    38  	Skipped   *Skipped `xml:"skipped,omitempty"`
    39  }
    40  
    41  // IsError returns true if the test case errored. Multiple fields are taken
    42  // into account to determine this.
    43  func (tc TestCase) IsError() bool {
    44  	return tc.Error != nil || tc.Status == "error"
    45  }
    46  
    47  // IsFailure returns true if the test case failed. Multiple fields are taken
    48  // into account to determine this.
    49  func (tc TestCase) IsFailure() bool {
    50  	return tc.Failure != nil || tc.Status == "failure" || tc.Status == "failed"
    51  }
    52  
    53  // IsSkipped returns true if the test case was skipped. Multiple fields are
    54  // taken into account to determine this.
    55  func (tc TestCase) IsSkipped() bool {
    56  	return tc.Skipped != nil || tc.Status == "skipped"
    57  }
    58  
    59  // Failure maps to either a <failure> or <error> element. It usually indicates
    60  // assertion failures. Depending on the framework, this may also indicate an
    61  // unexpected error, much like Error does. Some frameworks use Error or the
    62  // 'status' attribute on TestCase instead.
    63  type Failure struct {
    64  	// Message is a short description of the failure.
    65  	Message string `xml:"message,attr"`
    66  	// Type is the type of failure, e.g. "java.lang.AssertionError".
    67  	Type string `xml:"type,attr"`
    68  	// Text is a failure description or stack trace.
    69  	Text string `xml:",chardata"`
    70  }
    71  
    72  // Error maps to <error> element. It usually indicates unexpected errors.
    73  // Some frameworks use Failure or the 'status' attribute on TestCase instead.
    74  type Error struct {
    75  	// Message is a short description of the error.
    76  	Message string `xml:"message,attr"`
    77  	// Type is the type of error, e.g. "java.lang.NullPointerException".
    78  	Type string `xml:"type,attr"`
    79  	// Text is an error description or stack trace.
    80  	Text string `xml:",chardata"`
    81  }
    82  
    83  // Skipped maps to <skipped> element. Indicates a skipped test. Some frameworks
    84  // use the 'status' attribute on TestCase instead.
    85  type Skipped struct {
    86  	// Message is a short description that explains why the test was skipped.
    87  	Message string `xml:"message,attr"`
    88  }
    89  
    90  // TestSuite maps to <testsuite> element
    91  type TestSuite struct {
    92  	Name       string     `xml:"name,attr"`
    93  	Tests      int        `xml:"tests,attr"`
    94  	Properties []Property `xml:"properties>property"`
    95  	Errors     int        `xml:"errors,attr,omitempty"`
    96  	Failures   int        `xml:"failures,attr,omitempty"`
    97  	// Disabled is the number of disabled or skipped tests. Some frameworks use Skipped instead.
    98  	Disabled int `xml:"disabled,attr,omitempty"`
    99  	// Skipped is the number of skipped or disabled tests. Some frameworks use Disabled instead.
   100  	Skipped int `xml:"skipped,attr,omitempty"`
   101  	// Time in seconds it took to run the test suite.
   102  	Time string `xml:"time,attr,omitempty"`
   103  	// Timestamp as specified by ISO 8601 (2014-01-21T16:17:18). Timezone may not be specified.
   104  	Timestamp string     `xml:"timestamp,attr,omitempty"`
   105  	Package   string     `xml:"package,attr,omitempty"`
   106  	File      string     `xml:"file,attr,omitempty"`
   107  	TestCases []TestCase `xml:"testcase"`
   108  	SystemErr string     `xml:"system-err,omitempty"`
   109  	SystemOut string     `xml:"system-out,omitempty"`
   110  }
   111  
   112  // AddTestCases adds test cases to the test suite. If unique is true, existing
   113  // test cases with the same name will be replaced.
   114  func (ts *TestSuite) AddTestCases(unique bool, tcs ...TestCase) {
   115  	if !unique {
   116  		ts.TestCases = append(ts.TestCases, tcs...)
   117  		return
   118  	}
   119  
   120  	// index existing test cases by name
   121  	testMap := make(map[string]TestCase)
   122  	for _, tc := range ts.TestCases {
   123  		key := fmt.Sprintf("%s.%s", tc.ClassName, tc.Name)
   124  		testMap[key] = tc
   125  	}
   126  
   127  	// add new test cases
   128  	for _, tc := range tcs {
   129  		key := fmt.Sprintf("%s.%s", tc.ClassName, tc.Name)
   130  		testMap[key] = tc
   131  	}
   132  
   133  	// convert map back to slice
   134  	ts.TestCases = maps.Values(testMap)
   135  }
   136  
   137  // Compute updates some statistics for the test suite based on the test cases it
   138  // contains.
   139  //
   140  // Updates the following fields:
   141  //   - Tests
   142  //   - Errors
   143  //   - Failures
   144  //   - Skipped/Disabled
   145  func (ts *TestSuite) Compute() {
   146  	ts.Tests = len(ts.TestCases)
   147  	ts.Errors = 0
   148  	ts.Failures = 0
   149  	ts.Disabled = 0
   150  	ts.Skipped = 0
   151  
   152  	for _, tc := range ts.TestCases {
   153  		if tc.IsError() {
   154  			ts.Errors++
   155  		} else if tc.IsFailure() {
   156  			ts.Failures++
   157  		} else if tc.IsSkipped() {
   158  			ts.Skipped++
   159  		}
   160  		// we favor skipped over disabled, so ignore disabled
   161  	}
   162  }
   163  
   164  // TestSuites maps to root junit <testsuites> element
   165  type TestSuites struct {
   166  	XMLName    xml.Name    `xml:"testsuites"`
   167  	TestSuites []TestSuite `xml:"testsuite"`
   168  	Name       string      `xml:"name,attr,omitempty"`
   169  	// Time in seconds it took to run all the test suites.
   170  	Time  string `xml:"time,attr,omitempty"`
   171  	Tests int    `xml:"tests,attr,omitempty"`
   172  	// Disabled is the number of disabled or skipped tests. Some frameworks use Skipped instead.
   173  	Disabled int `xml:"disabled,attr,omitempty"`
   174  	// Skipped is the number of skipped or disabled tests. Some frameworks use Disabled instead.
   175  	Skipped  int `xml:"skipped,attr,omitempty"`
   176  	Failures int `xml:"failures,attr,omitempty"`
   177  	Errors   int `xml:"errors,attr,omitempty"`
   178  }
   179  
   180  // Compute updates _some_ statistics for the entire report based on the test
   181  // cases it contains. This is an expensive and destructive operation.
   182  // Use judiciously.
   183  //
   184  // Updates the following fields:
   185  //   - Tests
   186  //   - Errors
   187  //   - Failures
   188  //   - Skipped/Disabled
   189  func (ts *TestSuites) Compute() {
   190  	ts.Tests = 0
   191  	ts.Errors = 0
   192  	ts.Failures = 0
   193  	ts.Disabled = 0
   194  	ts.Skipped = 0
   195  
   196  	for i := range ts.TestSuites {
   197  		suite := &ts.TestSuites[i]
   198  		suite.Compute()
   199  
   200  		ts.Tests += suite.Tests
   201  		ts.Errors += suite.Errors
   202  		ts.Failures += suite.Failures
   203  		ts.Disabled += suite.Disabled
   204  		ts.Skipped += suite.Skipped
   205  	}
   206  }
   207  
   208  // TestCases returns all test cases from all test suites.
   209  func (ts *TestSuites) TestCases() []TestCase {
   210  	var tcs []TestCase
   211  	for _, ts := range ts.TestSuites {
   212  		tcs = append(tcs, ts.TestCases...)
   213  	}
   214  
   215  	return tcs
   216  }
   217  
   218  // Parse a junit report from an XML encoded byte string. The root <testsuites>
   219  // element is optional if there's only one <testsuite> element. In that case,
   220  // Parse will parse the <testsuite> and wrap it in a TestSuites struct.
   221  func Parse(data []byte) (TestSuites, error) {
   222  	var tss TestSuites
   223  	err := xml.Unmarshal(data, &tss)
   224  	if err != nil {
   225  		// root <testsuites> is optional
   226  		// parse a <testsuite> element instead
   227  		var ts TestSuite
   228  		if err = xml.Unmarshal(data, &ts); err != nil {
   229  			return tss, err
   230  		}
   231  
   232  		tss.TestSuites = []TestSuite{
   233  			ts,
   234  		}
   235  	}
   236  
   237  	return tss, err
   238  }
   239  
   240  // MergeReports merges multiple junit reports into a single report.
   241  func MergeReports(reports ...TestSuites) TestSuites {
   242  	suites := make(map[string]TestSuite)
   243  
   244  	for _, rep := range reports {
   245  		for _, suite := range rep.TestSuites {
   246  			indexedSuite, ok := suites[suite.Name]
   247  			if !ok {
   248  				suites[suite.Name] = suite
   249  				continue
   250  			}
   251  
   252  			indexedSuite.AddTestCases(true, suite.TestCases...)
   253  			suites[suite.Name] = indexedSuite
   254  		}
   255  	}
   256  
   257  	return TestSuites{
   258  		TestSuites: maps.Values(suites),
   259  	}
   260  }