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

     1  package core
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	"github.com/thought-machine/please/src/fs"
    10  )
    11  
    12  // TestSuites describes a collection of test results for a set of targets.
    13  type TestSuites struct {
    14  	TestSuites []TestSuite // The test results for each separate target
    15  }
    16  
    17  // TestSuite describes all the test results for a target.
    18  type TestSuite struct {
    19  	Package    string            // The package name of the test suite (usually the first part of the target label).
    20  	Name       string            // The name of the test suite (usually the last part of the target label).
    21  	Cached     bool              // True if the test results were retrieved from cache.
    22  	Duration   time.Duration     // The length of time it took to run this target (may be different from the sum of times of test cases).
    23  	TimedOut   bool              // True if the test failed because we timed it out.
    24  	TestCases  TestCases         // The test cases that ran during execution of this target.
    25  	Properties map[string]string // The system properties at the time of the test.
    26  	Timestamp  string            // ISO8601 formatted datetime when the test ran.
    27  }
    28  
    29  // JavaStyleName pretends we are using a language that has package names and classnames etc.
    30  func (testSuite TestSuite) JavaStyleName() string {
    31  	return fmt.Sprintf("%s.%s", testSuite.Package, testSuite.Name)
    32  }
    33  
    34  // Collapse adds the results of one test suite to the current one.
    35  func (testSuite *TestSuite) Collapse(incoming TestSuite) {
    36  	testSuite.TestCases = append(testSuite.TestCases, incoming.TestCases...)
    37  	testSuite.Duration += incoming.Duration
    38  	testSuite.TimedOut = testSuite.TimedOut || incoming.TimedOut
    39  	if testSuite.Properties == nil {
    40  		testSuite.Properties = make(map[string]string)
    41  	}
    42  	testSuite.Properties = addAll(testSuite.Properties, incoming.Properties)
    43  }
    44  
    45  func addAll(map1 map[string]string, map2 map[string]string) map[string]string {
    46  	for k, v := range map2 {
    47  		map1[k] = v
    48  	}
    49  	return map1
    50  }
    51  
    52  // Tests returns the number of TestCases.
    53  func (testSuite *TestSuite) Tests() int {
    54  	return len(testSuite.TestCases)
    55  }
    56  
    57  // FlakyPasses returns the number of TestCases which succeeded after some number of executions.
    58  func (testSuite TestSuite) FlakyPasses() int {
    59  	flakyPasses := 0
    60  
    61  	for _, result := range testSuite.TestCases {
    62  		if result.Success() != nil && len(result.Executions) > 1 {
    63  			flakyPasses++
    64  		}
    65  	}
    66  
    67  	return flakyPasses
    68  }
    69  
    70  // Passes returns the number of TestCases which succeeded (not skipped).
    71  func (testSuite TestSuite) Passes() int {
    72  	passes := 0
    73  
    74  	for _, result := range testSuite.TestCases {
    75  		if len(result.Failures()) == 0 && len(result.Errors()) == 0 && result.Skip() == nil {
    76  			passes++
    77  		}
    78  	}
    79  
    80  	return passes
    81  }
    82  
    83  // Errors returns the number of TestCases which did not succeed and returned some abnormal error.
    84  func (testSuite *TestSuite) Errors() int {
    85  	errors := 0
    86  
    87  	for _, result := range testSuite.TestCases {
    88  		// No success result, not skipped, some errors (don't care about the presence of failures)
    89  		if result.Success() == nil && result.Skip() == nil && len(result.Errors()) > 0 {
    90  			errors++
    91  		}
    92  	}
    93  
    94  	return errors
    95  }
    96  
    97  // Failures returns the number of TestCases which did not succeed and returned some failure.
    98  func (testSuite *TestSuite) Failures() int {
    99  	failures := 0
   100  
   101  	for _, result := range testSuite.TestCases {
   102  		// No success result, not skipped, no errors, but some failures.
   103  		if result.Success() == nil && result.Skip() == nil && len(result.Errors()) == 0 && len(result.Failures()) > 0 {
   104  			failures++
   105  		}
   106  	}
   107  
   108  	return failures
   109  }
   110  
   111  // Skips returns the number of TestCases that were skipped.
   112  func (testSuite *TestSuite) Skips() int {
   113  	skips := 0
   114  
   115  	for _, result := range testSuite.TestCases {
   116  		if result.Skip() != nil {
   117  			skips++
   118  		}
   119  	}
   120  
   121  	return skips
   122  }
   123  
   124  // Add puts test cases together if they have the same name and classname, allowing callers to treat
   125  // multiple test cases as if they were merely multiple executions of the same test.
   126  func (testSuite *TestSuite) Add(cases ...TestCase) {
   127  	for _, testCase := range cases {
   128  		idx := findMatchingTestCase(&testCase, &testSuite.TestCases)
   129  
   130  		if idx >= 0 {
   131  			testSuite.TestCases[idx].Executions = append(testSuite.TestCases[idx].Executions, testCase.Executions...)
   132  		} else {
   133  			testSuite.TestCases = append(testSuite.TestCases, testCase)
   134  		}
   135  	}
   136  }
   137  
   138  func findMatchingTestCase(testCase *TestCase, testCases *TestCases) int {
   139  	for idx := range *testCases {
   140  		originalTestCase := (*testCases)[idx]
   141  		if originalTestCase.Name == testCase.Name && originalTestCase.ClassName == testCase.ClassName {
   142  			return idx
   143  		}
   144  	}
   145  	return -1
   146  }
   147  
   148  // TestCase describes a set of test results for a test method.
   149  type TestCase struct {
   150  	ClassName  string          // ClassName of test (optional, for languages that don't have classes)
   151  	Name       string          // Name of test
   152  	Executions []TestExecution // The results of executing the test, possibly multiple times
   153  }
   154  
   155  // Success returns either the successful execution of a test case, or nil if it was never successfully executed.
   156  func (testCase *TestCase) Success() *TestExecution {
   157  	for _, execution := range testCase.Executions {
   158  		if execution.Failure == nil &&
   159  			execution.Error == nil &&
   160  			execution.Skip == nil {
   161  			return &execution
   162  		}
   163  	}
   164  	return nil
   165  }
   166  
   167  // Skip returns the either the skipped execution of a test case, or nil if it was never skipped.
   168  func (testCase *TestCase) Skip() *TestExecution {
   169  	for _, execution := range testCase.Executions {
   170  		if execution.Skip != nil {
   171  			return &execution
   172  		}
   173  	}
   174  	return nil
   175  }
   176  
   177  // Failures returns all failing executions of a test case.
   178  func (testCase *TestCase) Failures() []TestExecution {
   179  	failures := make([]TestExecution, 0)
   180  	for _, execution := range testCase.Executions {
   181  		if execution.Failure != nil {
   182  			failures = append(failures, execution)
   183  		}
   184  	}
   185  	return failures
   186  }
   187  
   188  // Errors returns all abnormal executions of a test case.
   189  func (testCase *TestCase) Errors() []TestExecution {
   190  	errors := make([]TestExecution, 0)
   191  	for _, execution := range testCase.Executions {
   192  		if execution.Error != nil {
   193  			errors = append(errors, execution)
   194  		}
   195  	}
   196  	return errors
   197  }
   198  
   199  // Duration calculates how long the test case took to run to success or failure (or nil if skipped or abnormal exit).
   200  func (testCase *TestCase) Duration() *time.Duration {
   201  	if testCase.Success() != nil {
   202  		return testCase.Success().Duration
   203  	} else if failures := testCase.Failures(); len(failures) > 0 {
   204  		return failures[0].Duration
   205  	}
   206  	// Unable to determine duration of this test case.
   207  	return nil
   208  }
   209  
   210  // TestCases is named so we can add a method to it.
   211  type TestCases []TestCase
   212  
   213  // AllSucceeded checks that every test case either passed or was skipped.
   214  func (testCases TestCases) AllSucceeded() bool {
   215  	for _, testCase := range testCases {
   216  		if testCase.Success() == nil && testCase.Skip() == nil {
   217  			return false
   218  		}
   219  	}
   220  	return true
   221  }
   222  
   223  // TestExecution represents one execution of a test method. The absence of a Failure, Error or Skip implies the test
   224  // executed successfully.
   225  type TestExecution struct {
   226  	Failure  *TestResultFailure // The failure, if any, running the test (usually an assertion that failed)
   227  	Error    *TestResultFailure // The error, if any, running the test (usually some other abnormal exit)
   228  	Skip     *TestResultSkip    // The reason for skipping the test, if it was skipped
   229  	Stdout   string             // Standard output during test
   230  	Stderr   string             // Standard error during test
   231  	Duration *time.Duration     // How long the test took (if it did not fail abnormally)
   232  }
   233  
   234  // TestResultFailure stores the information related to the failure - stack trace, exception type etc.
   235  type TestResultFailure struct {
   236  	Type      string // The type of error (e.g. "AssertionError")
   237  	Message   string // The reason for error (e.g. "1 != 2")
   238  	Traceback string // The trace of the error (if known)
   239  }
   240  
   241  // TestResultSkip stores the reason for skipping a test.
   242  type TestResultSkip struct {
   243  	Message string // The reason for skipping the test
   244  }
   245  
   246  // A LineCoverage represents a single line of coverage, which can be in one of several states.
   247  // Note that Please doesn't support sub-line coverage at present.
   248  type LineCoverage uint8
   249  
   250  // Constants representing the states that a single line can be in for coverage.
   251  const (
   252  	NotExecutable LineCoverage = iota // Line isn't executable (eg. comment, blank)
   253  	Unreachable   LineCoverage = iota // Line is executable but we've determined it can't be reached. So far not used.
   254  	Uncovered     LineCoverage = iota // Line is executable but isn't covered.
   255  	Covered       LineCoverage = iota // Line is executable and covered.
   256  )
   257  
   258  var lineCoverageOutput = [...]rune{'N', 'X', 'U', 'C'} // Corresponds to ordering of enum.
   259  
   260  // TestCoverage implements a pretty simple coverage format; we record one int for each line
   261  // stating what its coverage is.
   262  type TestCoverage struct {
   263  	Tests map[BuildLabel]map[string][]LineCoverage
   264  	Files map[string][]LineCoverage
   265  }
   266  
   267  // Aggregate aggregates results from that coverage object into this one.
   268  func (coverage *TestCoverage) Aggregate(cov *TestCoverage) {
   269  	if coverage.Tests == nil {
   270  		coverage.Tests = map[BuildLabel]map[string][]LineCoverage{}
   271  	}
   272  	if coverage.Files == nil {
   273  		coverage.Files = map[string][]LineCoverage{}
   274  	}
   275  
   276  	// Assume that tests are independent (will currently always be the case).
   277  	for label, c := range cov.Tests {
   278  		coverage.Tests[label] = c
   279  	}
   280  	// Files are more complex since multiple tests can cover the same file.
   281  	// We take the best result for each line from each test.
   282  	for filename, c := range cov.Files {
   283  		coverage.Files[filename] = MergeCoverageLines(coverage.Files[filename], c)
   284  	}
   285  }
   286  
   287  // MergeCoverageLines merges two sets of coverage results together, taking
   288  // the superset of the two results.
   289  func MergeCoverageLines(existing, coverage []LineCoverage) []LineCoverage {
   290  	ret := make([]LineCoverage, len(existing))
   291  	copy(ret, existing)
   292  	for i, line := range coverage {
   293  		if i >= len(ret) {
   294  			ret = append(ret, line)
   295  		} else if coverage[i] > ret[i] {
   296  			ret[i] = coverage[i]
   297  		}
   298  	}
   299  	return ret
   300  }
   301  
   302  // OrderedFiles returns an ordered slice of all the files we have coverage information for.
   303  // Note that files are ordered non-trivially such that each directory remains together.
   304  func (coverage *TestCoverage) OrderedFiles() []string {
   305  	files := make([]string, 0, len(coverage.Files))
   306  	for file := range coverage.Files {
   307  		if strings.HasPrefix(file, RepoRoot) {
   308  			file = strings.TrimLeft(file[len(RepoRoot):], "/")
   309  		}
   310  		files = append(files, file)
   311  	}
   312  	fs.SortPaths(files)
   313  	return files
   314  }
   315  
   316  // NewTestCoverage constructs and returns a new TestCoverage instance.
   317  func NewTestCoverage() TestCoverage {
   318  	return TestCoverage{
   319  		Tests: map[BuildLabel]map[string][]LineCoverage{},
   320  		Files: map[string][]LineCoverage{},
   321  	}
   322  }
   323  
   324  // TestCoverageString produces a string representation of coverage for serialising to file so we don't
   325  // expose the internal enum values (ordering is important so we may want to insert
   326  // new ones later). This format happens to be the same as the one Phabricator uses,
   327  // which is mildly useful to us since we want to integrate with it anyway. See
   328  // https://secure.phabricator.com/book/phabricator/article/arcanist_coverage/
   329  // for more detail of how it works.
   330  func TestCoverageString(lines []LineCoverage) string {
   331  	var buffer bytes.Buffer
   332  	for _, line := range lines {
   333  		buffer.WriteRune(lineCoverageOutput[line])
   334  	}
   335  	return buffer.String()
   336  }