github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/tools/test_monitor/level2/process_summary2_results.go (about) 1 package main 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "os" 7 "path/filepath" 8 "strings" 9 10 "github.com/onflow/flow-go/tools/test_monitor/common" 11 ) 12 13 const failuresDir = "./failures/" 14 const exceptionsDir = "./exceptions/" 15 16 func generateLevel2SummaryFromStructs(level1Summaries []common.Level1Summary) common.Level2Summary { 17 // create directory to store failure messages 18 err := os.Mkdir(failuresDir, 0755) 19 common.AssertNoError(err, "error creating failures directory") 20 21 // create directory to store exceptions messages 22 err = os.Mkdir(exceptionsDir, 0755) 23 common.AssertNoError(err, "error creating exceptions directory") 24 25 level2Summary := common.Level2Summary{} 26 level2Summary.TestResultsMap = make(map[string]*common.Level2TestResult) 27 28 // go through all level 1 test runs create level 2 summary 29 for i := 0; i < len(level1Summaries); i++ { 30 for _, level1TestResultRow := range level1Summaries[i] { 31 // check if already started collecting summary for this test 32 level2TestResultsMapKey := level1TestResultRow.Package + "/" + level1TestResultRow.Test 33 level2TestResult, level2TestResultExists := level2Summary.TestResultsMap[level2TestResultsMapKey] 34 35 // this test doesn't have a summary so create one 36 // no need to specify other fields explicitly - default values will suffice 37 if !level2TestResultExists { 38 level2TestResult = &common.Level2TestResult{ 39 Test: level1TestResultRow.Test, 40 Package: level1TestResultRow.Package, 41 } 42 } 43 // keep track of each duration so can later calculate average duration 44 level2TestResult.Durations = append(level2TestResult.Durations, level1TestResultRow.Elapsed) 45 46 // increment # of passes, fails, skips or exceptions for this test 47 level2TestResult.Runs++ 48 if level1TestResultRow.Pass == 1 { 49 level2TestResult.Passed++ 50 } else { 51 level2TestResult.Failed++ 52 53 // for tests that don't have a result generated (e.g. using fmt.Printf() with no newline in a test) 54 if level1TestResultRow.Exception == 1 { 55 level2TestResult.Exceptions++ 56 saveExceptionMessage(level1TestResultRow) 57 } else { 58 saveFailureMessage(level1TestResultRow) 59 } 60 } 61 62 level2Summary.TestResultsMap[level2TestResultsMapKey] = level2TestResult 63 } 64 } 65 // calculate averages and other calculations that can only be completed after all test runs have been read 66 postProcessLevel2Summary(level2Summary) 67 return level2Summary 68 } 69 70 // process level 1 summary files in a single directory and output level 2 summary 71 func generateLevel2Summary(level1Directory string) common.Level2Summary { 72 level1Summaries := buildLevel1SummariesFromJSON(level1Directory) 73 level2Summary := generateLevel2SummaryFromStructs(level1Summaries) 74 return level2Summary 75 } 76 77 // buildLevel1SummariesFromJSON creates level 1 summaries so the same function can be used to process level 1 78 // summaries whether they were created from JSON files (used in production) or from pre-constructed level 1 summary structs (used by testing) 79 func buildLevel1SummariesFromJSON(level1Directory string) []common.Level1Summary { 80 var level1Summaries []common.Level1Summary 81 dirEntries, err := os.ReadDir(filepath.Join(level1Directory)) 82 common.AssertNoError(err, "error reading level 1 directory") 83 84 for i := 0; i < len(dirEntries); i++ { 85 // read in each level 1 summary 86 var level1Summary common.Level1Summary 87 88 level1JsonBytes, err := os.ReadFile(filepath.Join(level1Directory, dirEntries[i].Name())) 89 common.AssertNoError(err, "error reading level 1 json") 90 91 err = json.Unmarshal(level1JsonBytes, &level1Summary) 92 common.AssertNoError(err, "error unmarshalling level 1 test run: "+dirEntries[i].Name()) 93 94 level1Summaries = append(level1Summaries, level1Summary) 95 } 96 return level1Summaries 97 } 98 99 func saveFailureMessage(testResult common.Level1TestResult) { 100 saveMessageHelper(testResult, failuresDir, "failure") 101 } 102 103 func saveExceptionMessage(testResult common.Level1TestResult) { 104 saveMessageHelper(testResult, exceptionsDir, "exception") 105 } 106 107 // for each failed / exception test, we want to save the raw output message as a text file 108 // there could be multiple failures / exceptions of the same test, so we want to save each failed / exception message in a separate text file 109 // each test with failures / exceptions will have a uniquely named (based on test name and package) subdirectory where failed / exception messages are saved 110 // e.g. "failures/TestSanitySha3_256+github.com-onflow-crypto-hash" will store failed messages text files 111 // from test TestSanitySha3_256 from the "github.com/onflow/crypto/hash" package 112 // failure and exception messages are saved in a similar way so this helper function 113 // handles saving both types of messages 114 func saveMessageHelper(testResult common.Level1TestResult, messagesDir string, messageFileStem string) { 115 // each subdirectory corresponds to a failed / exception test name and package name 116 messagesDirFullPath := messagesDir + testResult.Test + "+" + strings.ReplaceAll(testResult.Package, "/", "-") + "/" 117 118 // there could already be previous failures / exceptions for this test, so it's important 119 // to check if failed test / exception folder exists 120 if !common.DirExists(messagesDirFullPath) { 121 err := os.Mkdir(messagesDirFullPath, 0755) 122 common.AssertNoError(err, "error creating sub-dir under failures / exceptions dir") 123 } 124 125 // under each sub-directory, there should be 1 or more text files 126 // (failure1.txt / exception1.txt, failure2.txt / exception2.txt, etc) 127 // that holds the raw failure / exception message for that test 128 dirEntries, err := os.ReadDir(messagesDirFullPath) 129 common.AssertNoError(err, "error reading sub-dir entries under failures / exceptions dir") 130 131 // failure text files will be named "failure1.txt", "failure2.txt", etc 132 // exception text files will be named "exception1.txt", "exception2.txt", etc 133 // need to know how many failure / exception text files already exist in the sub-directory before creating the next one 134 messageFile, err := os.Create(messagesDirFullPath + fmt.Sprintf(messageFileStem+"%d.txt", len(dirEntries)+1)) 135 common.AssertNoError(err, "error creating failure / exception file") 136 defer messageFile.Close() 137 138 for _, output := range testResult.Output { 139 _, err = messageFile.WriteString(output) 140 common.AssertNoError(err, "error writing to failure / exception file") 141 } 142 } 143 144 // postProcessLevel2Summary calculates average duration and failure rate for each test over multiple level 1 summaries 145 func postProcessLevel2Summary(testSummary2 common.Level2Summary) { 146 for _, level2TestResult := range testSummary2.TestResultsMap { 147 // calculate average duration for each test summary 148 var durationSum float64 = 0 149 for _, duration := range level2TestResult.Durations { 150 durationSum += duration 151 } 152 level2TestResult.AverageDuration = common.ConvertToNDecimalPlaces2(2, durationSum, level2TestResult.Runs) 153 154 // calculate failure rate for each test summary 155 level2TestResult.FailureRate = common.ConvertToNDecimalPlaces(2, level2TestResult.Failed, level2TestResult.Runs) 156 } 157 158 // check if there are no failures so can delete failures sub-directory 159 if common.IsDirEmpty(failuresDir) { 160 err := os.RemoveAll(failuresDir) 161 common.AssertNoError(err, "error removing failures directory") 162 } 163 164 // check if there are no exceptions so can delete exceptions sub-directory 165 if common.IsDirEmpty(exceptionsDir) { 166 err := os.RemoveAll(exceptionsDir) 167 common.AssertNoError(err, "error removing exceptions directory") 168 } 169 } 170 171 // level 2 flaky test summary processor 172 // input: command line argument of directory where level 1 summary files exist 173 // output: level 2 summary json file that will be used as input for level 3 summary processor 174 func main() { 175 // need to pass in single argument of where level 1 summary files exist 176 if len(os.Args[1:]) != 1 { 177 panic("wrong number of arguments - expected single argument with directory of level 1 files") 178 } 179 180 if !common.DirExists(os.Args[1]) { 181 panic("directory doesn't exist") 182 } 183 184 testSummary2 := generateLevel2Summary(os.Args[1]) 185 common.SaveToFile("level2-summary.json", testSummary2) 186 }