github.com/koko1123/flow-go-1@v0.29.6/tools/test_monitor/level1/process_summary1_results.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/json"
     6  	"fmt"
     7  	"os"
     8  
     9  	"github.com/koko1123/flow-go-1/tools/test_monitor/common"
    10  	"github.com/koko1123/flow-go-1/utils/unittest"
    11  )
    12  
    13  // ResultReader gives us the flexibility to read test results in multiple ways - from stdin (for production) and from a local file (for unit testing)
    14  type ResultReader interface {
    15  	getReader() *os.File
    16  	close()
    17  
    18  	// where to save results - will be different for unit tests vs production
    19  	getResultsFileName() string
    20  }
    21  
    22  type StdinResultReader struct {
    23  }
    24  
    25  // return reader for reading from stdin - for production
    26  func (stdinResultReader StdinResultReader) getReader() *os.File {
    27  	return os.Stdin
    28  }
    29  
    30  // nothing to close when reading from stdin
    31  func (stdinResultReader StdinResultReader) close() {
    32  }
    33  
    34  func (stdinResultReader StdinResultReader) getResultsFileName() string {
    35  	return os.Getenv("RESULTS_FILE")
    36  }
    37  
    38  func generateLevel1Summary(resultReader ResultReader) (common.Level1Summary, map[string]*common.SkippedTestEntry) {
    39  	reader := resultReader.getReader()
    40  	scanner := bufio.NewScanner(reader)
    41  
    42  	defer resultReader.close()
    43  
    44  	testResultMap := processTestRunLineByLine(scanner)
    45  
    46  	err := scanner.Err()
    47  	common.AssertNoError(err, "error returning EOF for scanner")
    48  
    49  	testRun, skippedTests := finalizeLevel1Summary(testResultMap)
    50  
    51  	return testRun, skippedTests
    52  }
    53  
    54  // Raw JSON result step from `go test -json` execution
    55  // Sequence of result steps (specified by Action value) per test:
    56  // 1. run (once)
    57  // 2. output (one to many)
    58  // 3. pause (zero or once) - for tests using t.Parallel()
    59  // 4. cont (zero or once) - for tests using t.Parallel()
    60  // 5. pass OR fail OR skip (once)
    61  func processTestRunLineByLine(scanner *bufio.Scanner) map[string][]*common.Level1TestResult {
    62  	// test map holds all the tests
    63  	testResultMap := make(map[string][]*common.Level1TestResult)
    64  
    65  	for scanner.Scan() {
    66  		var rawTestStep common.RawTestStep
    67  		err := json.Unmarshal(scanner.Bytes(), &rawTestStep)
    68  		common.AssertNoError(err, "error unmarshalling raw test step")
    69  
    70  		// each test name needs to be unique, so we add package name in case there are
    71  		// tests with the same name across different packages
    72  		testResultMapKey := rawTestStep.Package + "/" + rawTestStep.Test
    73  
    74  		// most raw test steps will have Test value - only package specific steps won't have a Test value
    75  		// we're not storing package specific data
    76  		if rawTestStep.Test != "" {
    77  
    78  			// "run" is the very first test step and it needs special treatment - to create all the data structures that will be used by subsequent test steps for the same test
    79  			if rawTestStep.Action == "run" {
    80  				var newTestResult common.Level1TestResult
    81  				newTestResult.Test = rawTestStep.Test
    82  				newTestResult.Package = rawTestStep.Package
    83  
    84  				// each test holds specific data, irrespective of what the test result
    85  				newTestResult.CommitDate = common.GetCommitDate()
    86  				newTestResult.JobRunDate = common.GetJobRunDate()
    87  				newTestResult.CommitSha = common.GetCommitSha()
    88  				newTestResult.RunID = common.GetRunID()
    89  
    90  				// store outputs as a slice of strings - that's how "go test -json" outputs each output string on a separate line
    91  				// for passing tests, there are usually 2 outputs for a passing test and more outputs for a failing test
    92  				newTestResult.Output = make([]string, 0)
    93  
    94  				// append to test result slice, whether it's the first or subsequent test result
    95  				testResultMap[testResultMapKey] = append(testResultMap[testResultMapKey], &newTestResult)
    96  				continue
    97  			}
    98  
    99  			lastTestResultIndex := len(testResultMap[testResultMapKey]) - 1
   100  			if lastTestResultIndex < 0 {
   101  				lastTestResultIndex = 0
   102  			}
   103  
   104  			testResults, ok := testResultMap[testResultMapKey]
   105  			if !ok {
   106  				panic(fmt.Sprintf("no test result for test %s", rawTestStep.Test))
   107  			}
   108  			lastTestResultPointer := testResults[lastTestResultIndex]
   109  
   110  			// subsequent raw json outputs will have different data about the test - whether it passed/failed, what the test output was, etc
   111  			switch rawTestStep.Action {
   112  			case "output":
   113  				// keep appending output to the test
   114  				lastTestResultPointer.Output = append(lastTestResultPointer.Output, rawTestStep.Output)
   115  
   116  			// we need to convert pass / fail result into a numerical value so it can averaged and tracked on a graph
   117  			// pass is counted as 1, fail is counted as a 0
   118  			case "pass":
   119  				lastTestResultPointer.Pass = 1
   120  				lastTestResultPointer.Action = "pass"
   121  				lastTestResultPointer.Elapsed = rawTestStep.Elapsed
   122  				lastTestResultPointer.Exception = 0
   123  
   124  			case "fail":
   125  				lastTestResultPointer.Pass = 0
   126  				lastTestResultPointer.Action = "fail"
   127  				lastTestResultPointer.Elapsed = rawTestStep.Elapsed
   128  				lastTestResultPointer.Exception = 0
   129  
   130  			// skipped tests will be removed after all test results are gathered,
   131  			// since it would be more complicated to remove it here
   132  			case "skip":
   133  				lastTestResultPointer.Action = "skip"
   134  				lastTestResultPointer.Elapsed = rawTestStep.Elapsed
   135  
   136  			case "pause", "cont":
   137  				// tests using t.Parallel() will have these values
   138  				// nothing to do - test will continue to run normally and have a pass/fail result at the end
   139  
   140  			default:
   141  				panic(fmt.Sprintf("unexpected action: %s", rawTestStep.Action))
   142  			}
   143  		}
   144  	}
   145  	return testResultMap
   146  }
   147  
   148  func parseSkipReason(output []string) (unittest.SkipReason, bool) {
   149  	// skip reason is usually in the last output line, except when there is
   150  	// output from a test suite tear down function
   151  	for i := len(output) - 2; i >= 0; i-- {
   152  		skipReason, ok := unittest.ParseSkipReason(output[i])
   153  		if ok {
   154  			return skipReason, true
   155  		}
   156  	}
   157  	return 0, false
   158  }
   159  
   160  func finalizeLevel1Summary(testResultMap map[string][]*common.Level1TestResult) (common.Level1Summary, map[string]*common.SkippedTestEntry) {
   161  	var level1Summary common.Level1Summary
   162  	skippedTests := make(map[string]*common.SkippedTestEntry)
   163  	trackSkippedTests := getSkippedTestFile() != ""
   164  	testCategory := getTestCategory()
   165  
   166  	for _, testResults := range testResultMap {
   167  		for _, testResult := range testResults {
   168  			if trackSkippedTests {
   169  				skippedTestEntry := &common.SkippedTestEntry{
   170  					Test:       testResult.Test,
   171  					Package:    testResult.Package,
   172  					CommitDate: testResult.CommitDate,
   173  					CommitSHA:  testResult.CommitSha,
   174  					Category:   testCategory,
   175  				}
   176  				skippedTests[testResult.Test] = skippedTestEntry
   177  
   178  				if testResult.Action == "skip" {
   179  					skipReason, ok := parseSkipReason(testResult.Output)
   180  					if ok {
   181  						skippedTestEntry.SkipReason = skipReason
   182  					} else {
   183  						panic("could not parse Skip Reason from output for test: " + testResult.Test)
   184  					}
   185  				}
   186  			}
   187  
   188  			if testResult.Action == "skip" {
   189  				// don't add skipped tests to summary since they can't be used to compute an average pass rate
   190  				continue
   191  			}
   192  
   193  			// for tests that don't have a result generated (e.g. using fmt.Printf() with no newline in a test)
   194  			// we want to highlight these tests in Grafana
   195  			// we do this by setting the Exception field to true, so we can filter by that field in Grafana
   196  			if testResult.Action == "" {
   197  				// count exception as a failure
   198  				testResult.Pass = 0
   199  				testResult.Exception = 1
   200  			}
   201  
   202  			testResult.Action = ""
   203  
   204  			// only save output for failed tests
   205  			if testResult.Pass == 1 {
   206  				testResult.Output = nil
   207  			}
   208  
   209  			// only include passed or failed tests - don't include skipped tests
   210  			// this is needed to have accurate Grafana metrics for average pass rate
   211  			level1Summary = append(level1Summary, *testResult)
   212  		}
   213  	}
   214  
   215  	return level1Summary, skippedTests
   216  }
   217  
   218  func getSkippedTestFile() string {
   219  	return os.Getenv("SKIPPED_TESTS_FILE")
   220  }
   221  
   222  func getTestCategory() string {
   223  	return os.Getenv("TEST_CATEGORY")
   224  }
   225  
   226  // level 1 flaky test summary processor
   227  // input: json formatted test results from `go test -json` from console (not file)
   228  // output: level 1 summary json file that will be used as input for level 2 summary processor
   229  func main() {
   230  	resultReader := StdinResultReader{}
   231  
   232  	testRun, skippedTestMap := generateLevel1Summary(resultReader)
   233  
   234  	resultsFile := resultReader.getResultsFileName()
   235  	if resultsFile != "" {
   236  		common.SaveLinesToFile(resultsFile, testRun)
   237  	}
   238  
   239  	skippedTestsFile := getSkippedTestFile()
   240  	if skippedTestsFile != "" {
   241  		var skippedTests []*common.SkippedTestEntry
   242  		for _, skippedTestEntry := range skippedTestMap {
   243  			skippedTests = append(skippedTests, skippedTestEntry)
   244  		}
   245  		common.SaveLinesToFile(skippedTestsFile, skippedTests)
   246  	}
   247  }