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 }