github.com/tiagovtristao/plz@v13.4.0+incompatible/src/test/xml_results.go (about)

     1  // Parser for JUnit XML output.
     2  
     3  package test
     4  
     5  import (
     6  	"bytes"
     7  	"encoding/xml"
     8  	"io/ioutil"
     9  	"os"
    10  	"path"
    11  	"time"
    12  
    13  	"github.com/thought-machine/please/src/core"
    14  	"io"
    15  )
    16  
    17  func looksLikeJUnitXMLTestResults(b []byte) bool {
    18  	return bytes.HasPrefix(b, []byte{'<', '?', 'x', 'm', 'l'}) || bytes.HasPrefix(b, []byte{'<', 't', 'e', 's', 't'})
    19  }
    20  
    21  func parseJUnitXMLTestResults(data []byte) (core.TestSuites, error) {
    22  	results := core.TestSuites{}
    23  	decoder := xml.NewDecoder(bytes.NewReader(data))
    24  	for {
    25  		token, err := decoder.Token()
    26  		switch err {
    27  		case nil:
    28  		case io.EOF:
    29  			return results, nil
    30  		default:
    31  			return results, err
    32  		}
    33  
    34  		switch tok := token.(type) {
    35  		case xml.StartElement:
    36  			switch tok.Name.Local {
    37  			case "test":
    38  				// UnitTest.cpp Test
    39  				uxmlTest := unitTestXMLTest{}
    40  				decoder.DecodeElement(&uxmlTest, &tok)
    41  				xmlTest := uxmlTest.toJUnitXMLTest()
    42  				testSuite := core.TestSuite{
    43  					Name: uxmlTest.Suite,
    44  				}
    45  				testCase := core.TestCase{
    46  					Name: uxmlTest.Name,
    47  				}
    48  				appendResult(xmlTest, &testCase)
    49  				testSuite.TestCases = append(testSuite.TestCases, testCase)
    50  				testSuite.Duration += xmlTest.Duration()
    51  				results.TestSuites = append(results.TestSuites, testSuite)
    52  			case "testcase":
    53  				// One or more bare tests, put each one in a synthetic test suite
    54  				testSuite := core.TestSuite{}
    55  				xmlTest := jUnitXMLTest{}
    56  				testCase := core.TestCase{}
    57  				decoder.DecodeElement(&xmlTest, &tok)
    58  				appendResult(xmlTest, &testCase)
    59  				testSuite.TestCases = append(testSuite.TestCases, testCase)
    60  				testSuite.Duration += xmlTest.Duration()
    61  				results.TestSuites = append(results.TestSuites, testSuite)
    62  			case "testsuite": // Just a single test suite (this is the usual output from junit, for example)
    63  				xmlTestSuite := jUnitXMLTestSuite{}
    64  				decoder.DecodeElement(&xmlTestSuite, &tok)
    65  				results.TestSuites = append(results.TestSuites, toCoreTestSuite(xmlTestSuite))
    66  			case "testsuites": // We might have a collection of existing test suites, if we're parsing our own output.
    67  				xmlTestSuites := jUnitXMLTestSuites{}
    68  				decoder.DecodeElement(&xmlTestSuites, &tok)
    69  
    70  				var duration time.Duration
    71  				for _, xmlTestSuite := range xmlTestSuites.TestSuites {
    72  					results.TestSuites = append(results.TestSuites, toCoreTestSuite(xmlTestSuite))
    73  					duration += xmlTestSuite.Duration()
    74  				}
    75  			}
    76  		}
    77  	}
    78  }
    79  
    80  func toCoreTestSuite(xmlTestSuite jUnitXMLTestSuite) core.TestSuite {
    81  	testSuite := core.TestSuite{
    82  		Package:    xmlTestSuite.Package,
    83  		Name:       xmlTestSuite.Name,
    84  		Timestamp:  xmlTestSuite.Timestamp,
    85  		Duration:   xmlTestSuite.Duration(),
    86  		Properties: toCoreProperties(xmlTestSuite.Properties),
    87  	}
    88  	for _, test := range xmlTestSuite.TestCases {
    89  		result := core.TestCase{
    90  			ClassName: test.ClassName,
    91  			Name:      test.Name,
    92  		}
    93  		appendResult(test, &result)
    94  		testSuite.TestCases = append(testSuite.TestCases, result)
    95  	}
    96  	return testSuite
    97  }
    98  
    99  func toCoreProperties(properties jUnitXMLProperties) map[string]string {
   100  	props := make(map[string]string)
   101  	for _, prop := range properties.Property {
   102  		props[prop.Name] = prop.Value
   103  	}
   104  	return props
   105  }
   106  
   107  func appendResult(test jUnitXMLTest, results *core.TestCase) {
   108  	// There can be only one of these
   109  	if test.Failure != nil {
   110  		appendFailure(test, results, *test.Failure)
   111  	} else if test.Error != nil {
   112  		appendError(test, results, *test.Error)
   113  	} else if test.Skipped != nil {
   114  		appendSkipped(test, results, *test.Skipped)
   115  	} else {
   116  		appendSuccess(test, results)
   117  	}
   118  
   119  	if len(test.FlakyFailure) > 0 {
   120  		for _, flake := range test.FlakyFailure {
   121  			appendFlakyFailure(test, results, flake)
   122  		}
   123  	}
   124  	if len(test.FlakyError) > 0 {
   125  		// The test ultimately succeeded but errored possibly several times.
   126  		// We have added the success above.
   127  		for _, flake := range test.FlakyError {
   128  			appendFlakyError(test, results, flake)
   129  		}
   130  	}
   131  	if len(test.RerunFailure) > 0 {
   132  		// The test never succeeded and flaked possibly several times.
   133  		// We have already added the first failure above.
   134  		for _, flake := range test.RerunFailure {
   135  			appendRerunFailure(test, results, flake)
   136  		}
   137  	}
   138  	if len(test.RerunError) > 0 {
   139  		// The test never succeeded and errored possibly several times.
   140  		// We have already added the first error above.
   141  		for _, flake := range test.RerunError {
   142  			appendRerunError(test, results, flake)
   143  		}
   144  	}
   145  }
   146  
   147  func appendFailure(test jUnitXMLTest, results *core.TestCase, failure jUnitXMLFailure) {
   148  	d := time.Duration(test.Time)
   149  	results.Executions = append(results.Executions, core.TestExecution{
   150  		Failure: &core.TestResultFailure{
   151  			Message:   failure.Message,
   152  			Type:      failure.Type,
   153  			Traceback: failure.Traceback,
   154  		},
   155  		Duration: &d,
   156  		Stdout:   test.Stdout,
   157  		Stderr:   test.Stderr,
   158  	})
   159  }
   160  
   161  func appendFlakyFailure(test jUnitXMLTest, results *core.TestCase, flake jUnitXMLFlaky) {
   162  	d := time.Duration(test.Time)
   163  	results.Executions = append(results.Executions, core.TestExecution{
   164  		Failure: &core.TestResultFailure{
   165  			Message:   flake.Message,
   166  			Type:      flake.Type,
   167  			Traceback: flake.Traceback,
   168  		},
   169  		Duration: &d,
   170  		Stdout:   test.Stdout,
   171  		Stderr:   test.Stderr,
   172  	})
   173  }
   174  
   175  func appendFlakyError(test jUnitXMLTest, results *core.TestCase, flake jUnitXMLFlaky) {
   176  	results.Executions = append(results.Executions, core.TestExecution{
   177  		Error: &core.TestResultFailure{
   178  			Message:   flake.Message,
   179  			Type:      flake.Type,
   180  			Traceback: flake.Traceback,
   181  		},
   182  		Stdout: test.Stdout,
   183  		Stderr: test.Stderr,
   184  	})
   185  }
   186  
   187  func appendRerunFailure(test jUnitXMLTest, results *core.TestCase, flake jUnitXMLRerunFailure) {
   188  	d := time.Duration(test.Time)
   189  	results.Executions = append(results.Executions, core.TestExecution{
   190  		Failure: &core.TestResultFailure{
   191  			Message:   flake.Message,
   192  			Type:      flake.Type,
   193  			Traceback: flake.Traceback,
   194  		},
   195  		Duration: &d,
   196  		Stdout:   test.Stdout,
   197  		Stderr:   test.Stderr,
   198  	})
   199  }
   200  
   201  func appendRerunError(test jUnitXMLTest, results *core.TestCase, flake jUnitXMLRerunError) {
   202  	results.Executions = append(results.Executions, core.TestExecution{
   203  		Error: &core.TestResultFailure{
   204  			Message:   flake.Message,
   205  			Type:      flake.Type,
   206  			Traceback: flake.Traceback,
   207  		},
   208  		Stdout: test.Stdout,
   209  		Stderr: test.Stderr,
   210  	})
   211  }
   212  
   213  func appendError(test jUnitXMLTest, results *core.TestCase, error jUnitXMLError) {
   214  	results.Executions = append(results.Executions, core.TestExecution{
   215  		Error: &core.TestResultFailure{
   216  			Message:   error.Message,
   217  			Type:      error.Type,
   218  			Traceback: error.Traceback,
   219  		},
   220  		Stdout: test.Stdout,
   221  		Stderr: test.Stderr,
   222  	})
   223  }
   224  
   225  func appendSkipped(test jUnitXMLTest, results *core.TestCase, skipped jUnitXMLSkipped) {
   226  	results.Executions = append(results.Executions, core.TestExecution{
   227  		Skip: &core.TestResultSkip{
   228  			Message: skipped.Message,
   229  		},
   230  		Stdout: test.Stdout,
   231  		Stderr: test.Stderr,
   232  	})
   233  }
   234  
   235  func appendSuccess(test jUnitXMLTest, results *core.TestCase) {
   236  	duration := test.Duration()
   237  	results.Executions = append(results.Executions, core.TestExecution{
   238  		Duration: &duration,
   239  		Stdout:   test.Stdout,
   240  		Stderr:   test.Stderr,
   241  	})
   242  }
   243  
   244  type jUnitXMLTestSuites struct {
   245  	Errors   uint   `xml:"errors,attr,omitempty"`
   246  	Failures uint   `xml:"failures,attr,omitempty"`
   247  	Name     string `xml:"name,attr,omitempty"`
   248  	Skipped  uint   `xml:"skipped,attr,omitempty"`
   249  	Tests    uint   `xml:"tests,attr,omitempty"`
   250  	timed    `xml:"time,attr,omitempty"`
   251  
   252  	TestSuites []jUnitXMLTestSuite `xml:"testsuite,omitempty"`
   253  
   254  	XMLName xml.Name `xml:"testsuites"`
   255  }
   256  
   257  type jUnitXMLTestSuite struct {
   258  	Name  string `xml:"name,attr"`
   259  	Tests int    `xml:"tests,attr"`
   260  
   261  	Errors    int    `xml:"errors,attr,omitempty"`
   262  	Failures  int    `xml:"failures,attr,omitempty"`
   263  	HostName  string `xml:"hostname,attr,omitempty"`
   264  	Skipped   int    `xml:"skipped,attr,omitempty"`
   265  	Package   string `xml:"package,attr,omitempty"`
   266  	timed     `xml:"time,attr,omitempty"`
   267  	Timestamp string `xml:"timestamp,attr,omitempty"`
   268  
   269  	Properties jUnitXMLProperties `xml:"properties,omitempty"`
   270  	TestCases  []jUnitXMLTest     `xml:"testcase"`
   271  	Stdout     string             `xml:"system-out,omitempty"`
   272  	Stderr     string             `xml:"system-err,omitempty"`
   273  
   274  	XMLName xml.Name `xml:"testsuite"`
   275  }
   276  
   277  type jUnitXMLTest struct {
   278  	Name string `xml:"name,attr"`
   279  
   280  	Assertions uint   `xml:"assertions,attr,omitempty"`
   281  	ClassName  string `xml:"classname,attr,omitempty"`
   282  	Status     string `xml:"status,attr,omitempty"`
   283  	timed      `xml:"time,attr,omitempty"`
   284  
   285  	Error        *jUnitXMLError         `xml:"error,omitempty"`
   286  	FlakyError   []jUnitXMLFlaky        `xml:"flakyError,omitempty"`
   287  	RerunError   []jUnitXMLRerunError   `xml:"rerunError,omitempty"`
   288  	Failure      *jUnitXMLFailure       `xml:"failure,omitempty"`
   289  	FlakyFailure []jUnitXMLFlaky        `xml:"flakyFailure,omitempty"`
   290  	RerunFailure []jUnitXMLRerunFailure `xml:"rerunFailure,omitempty"`
   291  	Skipped      *jUnitXMLSkipped       `xml:"skipped,omitempty"`
   292  	Stdout       string                 `xml:"system-out,omitempty"`
   293  	Stderr       string                 `xml:"system-err,omitempty"`
   294  }
   295  
   296  type jUnitXMLProperties struct {
   297  	Property []jUnitXMLProperty `xml:"property"`
   298  }
   299  
   300  type jUnitXMLProperty struct {
   301  	Name  string `xml:"name,attr"`
   302  	Value string `xml:"value,attr"`
   303  }
   304  
   305  type jUnitXMLError struct {
   306  	Message string `xml:"message,attr,omitempty"`
   307  	Type    string `xml:"type,attr"`
   308  
   309  	Traceback string `xml:",chardata"`
   310  }
   311  
   312  type jUnitXMLFailure struct {
   313  	Message string `xml:"message,attr,omitempty"`
   314  	Type    string `xml:"type,attr"`
   315  
   316  	Traceback string `xml:",chardata"`
   317  }
   318  
   319  type jUnitXMLFlaky struct {
   320  	Message string `xml:"message,attr,omitempty"`
   321  	Type    string `xml:"type,attr"`
   322  
   323  	Traceback string `xml:",chardata"`
   324  	Stdout    string `xml:"system-out,omitempty"`
   325  	Stderr    string `xml:"system-err,omitempty"`
   326  }
   327  
   328  type jUnitXMLRerunError struct {
   329  	Message string `xml:"message,attr,omitempty"`
   330  	Type    string `xml:"type,attr"`
   331  
   332  	Traceback string `xml:",chardata"`
   333  	Stdout    string `xml:"system-out,omitempty"`
   334  	Stderr    string `xml:"system-err,omitempty"`
   335  }
   336  
   337  type jUnitXMLRerunFailure struct {
   338  	Message string `xml:"message,attr,omitempty"`
   339  	timed   `xml:"time,attr"`
   340  	Type    string `xml:"type,attr"`
   341  
   342  	Traceback string `xml:",chardata"`
   343  	Stdout    string `xml:"system-out,omitempty"`
   344  	Stderr    string `xml:"system-err,omitempty"`
   345  }
   346  
   347  type jUnitXMLSkipped struct {
   348  	Message string `xml:"message,attr,omitempty"`
   349  }
   350  
   351  type timed struct {
   352  	Time float64 `xml:"time,attr"`
   353  }
   354  
   355  func (t timed) Duration() time.Duration {
   356  	return time.Duration(t.Time * float64(time.Second))
   357  }
   358  
   359  func (j jUnitXMLTest) WasSuccessful() bool {
   360  	return j.Skipped == nil &&
   361  		j.Error == nil &&
   362  		j.Failure == nil
   363  }
   364  
   365  type unitTestXMLTest struct {
   366  	Suite   string  `xml:"suite,attr"`
   367  	Name    string  `xml:"name,attr"`
   368  	Elapsed float64 `xml:"elapsed,attr"`
   369  
   370  	Failure *unitTestXMLFailure `xml:"failure,omitempty"`
   371  }
   372  
   373  func (uxmlTest *unitTestXMLTest) toJUnitXMLTest() jUnitXMLTest {
   374  	var failure *jUnitXMLFailure
   375  	if uxmlTest.Failure != nil {
   376  		failure = &jUnitXMLFailure{
   377  			Message: uxmlTest.Failure.Message,
   378  		}
   379  	}
   380  	return jUnitXMLTest{
   381  		Name:      uxmlTest.Name,
   382  		ClassName: uxmlTest.Suite,
   383  		timed:     timed{uxmlTest.Elapsed},
   384  		Failure:   failure,
   385  	}
   386  }
   387  
   388  type unitTestXMLFailure struct {
   389  	Message string `xml:"message,attr"`
   390  }
   391  
   392  // WriteResultsToFileOrDie writes test results out to a file in xUnit format. Dies on any errors.
   393  func WriteResultsToFileOrDie(graph *core.BuildGraph, filename string) {
   394  	if err := os.MkdirAll(path.Dir(filename), core.DirPermissions); err != nil {
   395  		log.Fatalf("Failed to create directory for test output")
   396  	}
   397  	xmlTestResults := jUnitXMLTestSuites{}
   398  	xmlTestResults.XMLName.Local = "testsuites"
   399  
   400  	// Collapse any testsuite with the same name
   401  	xmlSuites := make(map[string]jUnitXMLTestSuite)
   402  	for _, target := range graph.AllTargets() {
   403  		if target.IsTest {
   404  			testSuite := target.Results
   405  			if len(testSuite.TestCases) > 0 {
   406  				var xmlTestSuite jUnitXMLTestSuite
   407  				if _, ok := xmlSuites[testSuite.JavaStyleName()]; ok {
   408  					xmlTestSuite = xmlSuites[testSuite.Name]
   409  					xmlTestSuite.Tests += testSuite.Tests()
   410  					xmlTestSuite.Errors += testSuite.Errors()
   411  					xmlTestSuite.Failures += testSuite.Failures()
   412  					xmlTestSuite.Skipped += testSuite.Skips()
   413  					xmlTestSuite.timed.Time += testSuite.Duration.Seconds()
   414  				} else {
   415  					xmlTestSuite = jUnitXMLTestSuite{
   416  						Name:       testSuite.Name,
   417  						Package:    testSuite.Package,
   418  						Timestamp:  testSuite.Timestamp,
   419  						Tests:      testSuite.Tests(),
   420  						Errors:     testSuite.Errors(),
   421  						Failures:   testSuite.Failures(),
   422  						Skipped:    testSuite.Skips(),
   423  						timed:      timed{testSuite.Duration.Seconds()},
   424  						Properties: toXmlProperties(testSuite.Properties),
   425  					}
   426  				}
   427  				for _, testCase := range testSuite.TestCases {
   428  					xmlTest := toXmlTestCase(testCase)
   429  					if xmlTest.ClassName == "" {
   430  						xmlTest.ClassName = testSuite.JavaStyleName()
   431  					}
   432  					xmlTestSuite.TestCases = append(xmlTestSuite.TestCases, xmlTest)
   433  				}
   434  				xmlSuites[testSuite.JavaStyleName()] = xmlTestSuite
   435  				for _, testCase := range testSuite.TestCases {
   436  					xmlTest := toXmlTestCase(testCase)
   437  					xmlTestSuite.TestCases = append(xmlTestSuite.TestCases, xmlTest)
   438  				}
   439  			}
   440  			xmlTestResults.Time += testSuite.Duration.Seconds()
   441  		}
   442  	}
   443  	for _, xmlTestSuite := range xmlSuites {
   444  		xmlTestResults.TestSuites = append(xmlTestResults.TestSuites, xmlTestSuite)
   445  	}
   446  	if b, err := xml.MarshalIndent(xmlTestResults, "", "    "); err != nil {
   447  		log.Fatalf("Failed to serialise XML: %s", err)
   448  	} else if err = ioutil.WriteFile(filename, b, 0644); err != nil {
   449  		log.Fatalf("Failed to write XML to %s: %s", filename, err)
   450  	}
   451  }
   452  
   453  func toXmlProperties(props map[string]string) jUnitXMLProperties {
   454  	out := jUnitXMLProperties{}
   455  	for k, v := range props {
   456  		out.Property = append(out.Property, jUnitXMLProperty{
   457  			Name:  k,
   458  			Value: v,
   459  		})
   460  	}
   461  	return out
   462  }
   463  
   464  func toXmlTestCase(result core.TestCase) jUnitXMLTest {
   465  	testcase := jUnitXMLTest{
   466  		ClassName: result.ClassName,
   467  		Name:      result.Name,
   468  	}
   469  	success := result.Success()
   470  	failures := result.Failures()
   471  	errors := result.Errors()
   472  	skip := result.Skip()
   473  	if success != nil {
   474  		// We passed but we might have had flakes
   475  		testcase.Stderr = success.Stderr
   476  		testcase.Stdout = success.Stdout
   477  		testcase.Time = success.Duration.Seconds()
   478  		for _, execution := range failures {
   479  			testcase.FlakyFailure = append(testcase.FlakyFailure, jUnitXMLFlaky{
   480  				Message:   execution.Failure.Message,
   481  				Stderr:    execution.Stderr,
   482  				Stdout:    execution.Stdout,
   483  				Traceback: execution.Failure.Traceback,
   484  				Type:      execution.Failure.Type,
   485  			})
   486  		}
   487  		for _, execution := range errors {
   488  			testcase.FlakyError = append(testcase.FlakyError, jUnitXMLFlaky{
   489  				Message:   execution.Error.Message,
   490  				Stderr:    execution.Stderr,
   491  				Stdout:    execution.Stdout,
   492  				Traceback: execution.Error.Traceback,
   493  				Type:      execution.Error.Type,
   494  			})
   495  		}
   496  	} else if skip != nil {
   497  		testcase.Skipped = &jUnitXMLSkipped{
   498  			Message: skip.Skip.Message,
   499  		}
   500  	} else {
   501  		// We didn't have a single pass, everything is darkness
   502  		// See if we 'failed' or 'errored' first.
   503  		doneFirst := false
   504  		setDuration := false
   505  		for _, execution := range result.Executions {
   506  			if execution.Error != nil {
   507  				if !doneFirst {
   508  					testcase.Error = &jUnitXMLError{
   509  						Message:   execution.Error.Message,
   510  						Traceback: execution.Error.Traceback,
   511  						Type:      execution.Error.Type,
   512  					}
   513  					testcase.Stderr = execution.Stderr
   514  					testcase.Stdout = execution.Stdout
   515  					doneFirst = true
   516  				} else {
   517  					testcase.RerunError = append(testcase.RerunError, jUnitXMLRerunError{
   518  						Message:   execution.Error.Message,
   519  						Stderr:    execution.Stderr,
   520  						Stdout:    execution.Stdout,
   521  						Traceback: execution.Error.Traceback,
   522  						Type:      execution.Error.Type,
   523  					})
   524  				}
   525  			} else if execution.Failure != nil {
   526  				if !doneFirst {
   527  					testcase.Failure = &jUnitXMLFailure{
   528  						Message:   execution.Failure.Message,
   529  						Traceback: execution.Failure.Traceback,
   530  						Type:      execution.Failure.Type,
   531  					}
   532  					testcase.Stderr = execution.Stderr
   533  					testcase.Stdout = execution.Stdout
   534  					doneFirst = true
   535  				} else {
   536  					testcase.RerunFailure = append(testcase.RerunFailure, jUnitXMLRerunFailure{
   537  						Message:   execution.Failure.Message,
   538  						Stderr:    execution.Stderr,
   539  						Stdout:    execution.Stdout,
   540  						timed:     timed{execution.Duration.Seconds()},
   541  						Traceback: execution.Failure.Traceback,
   542  						Type:      execution.Failure.Type,
   543  					})
   544  				}
   545  				if !setDuration && execution.Duration != nil {
   546  					testcase.Time = execution.Duration.Seconds()
   547  					setDuration = true
   548  				}
   549  			}
   550  		}
   551  	}
   552  	return testcase
   553  }