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  }