github.com/darmach/terratest@v0.34.8-0.20210517103231-80931f95e3ff/modules/logger/parser/parser.go (about) 1 // Package logger/parser contains methods to parse and restructure log output from go testing and terratest 2 package parser 3 4 import ( 5 "bufio" 6 "io" 7 "os" 8 "regexp" 9 "strings" 10 "sync" 11 12 junitparser "github.com/jstemmer/go-junit-report/parser" 13 "github.com/sirupsen/logrus" 14 ) 15 16 // SpawnParsers will spawn the log parser and junit report parsers off of a single reader. 17 func SpawnParsers(logger *logrus.Logger, reader io.Reader, outputDir string) { 18 forkedReader, forkedWriter := io.Pipe() 19 teedReader := io.TeeReader(reader, forkedWriter) 20 var waitForParsers sync.WaitGroup 21 waitForParsers.Add(2) 22 go func() { 23 // close pipe writer, because this section drains the tee reader indicating reader is done draining 24 defer forkedWriter.Close() 25 defer waitForParsers.Done() 26 parseAndStoreTestOutput(logger, teedReader, outputDir) 27 }() 28 go func() { 29 defer waitForParsers.Done() 30 report, err := junitparser.Parse(forkedReader, "") 31 if err == nil { 32 storeJunitReport(logger, outputDir, report) 33 } else { 34 logger.Errorf("Error parsing test output into junit report: %s", err) 35 } 36 }() 37 waitForParsers.Wait() 38 } 39 40 // RegEx for parsing test status lines. Pulled from jstemmer/go-junit-report 41 var ( 42 regexResult = regexp.MustCompile(`--- (PASS|FAIL|SKIP): (.+) \((\d+\.\d+)(?: ?seconds|s)\)`) 43 regexStatus = regexp.MustCompile(`=== (RUN|PAUSE|CONT)\s+(.+)`) 44 regexSummary = regexp.MustCompile(`(^FAIL$)|(^(ok|FAIL)\s+([^ ]+)\s+(?:(\d+\.\d+)s|\(cached\)|(\[\w+ failed]))(?:\s+coverage:\s+(\d+\.\d+)%\sof\sstatements(?:\sin\s.+)?)?$)`) 45 regexPanic = regexp.MustCompile(`^panic:`) 46 ) 47 48 // getIndent takes a line and returns the indent string 49 // Example: 50 // in: " --- FAIL: TestSnafu" 51 // out: " " 52 func getIndent(data string) string { 53 re := regexp.MustCompile(`^\s+`) 54 indent := re.FindString(data) 55 return indent 56 } 57 58 // getTestNameFromResultLine takes a go testing result line and extracts out the test name 59 // Example: 60 // in: --- FAIL: TestSnafu 61 // out: TestSnafu 62 func getTestNameFromResultLine(text string) string { 63 m := regexResult.FindStringSubmatch(text) 64 return m[2] 65 } 66 67 // isResultLine checks if a line of text matches a test result (begins with "--- FAIL" or "--- PASS") 68 func isResultLine(text string) bool { 69 return regexResult.MatchString(text) 70 } 71 72 // getTestNameFromStatusLine takes a go testing status line and extracts out the test name 73 // Example: 74 // in: === RUN TestSnafu 75 // out: TestSnafu 76 func getTestNameFromStatusLine(text string) string { 77 m := regexStatus.FindStringSubmatch(text) 78 return m[2] 79 } 80 81 // isStatusLine checks if a line of text matches a test status 82 func isStatusLine(text string) bool { 83 return regexStatus.MatchString(text) 84 } 85 86 // isSummaryLine checks if a line of text matches the test summary 87 func isSummaryLine(text string) bool { 88 return regexSummary.MatchString(text) 89 } 90 91 // isPanicLine checks if a line of text matches a panic 92 func isPanicLine(text string) bool { 93 return regexPanic.MatchString(text) 94 } 95 96 // parseAndStoreTestOutput will take test log entries from terratest and aggregate the output by test. Takes advantage 97 // of the fact that terratest logs are prefixed by the test name. This will store the broken out logs into files under 98 // the outputDir, named by test name. 99 // Additionally will take test result lines and collect them under a summary log file named `summary.log`. 100 // See the `fixtures` directory for some examples. 101 func parseAndStoreTestOutput( 102 logger *logrus.Logger, 103 read io.Reader, 104 outputDir string, 105 ) { 106 logWriter := LogWriter{ 107 lookup: make(map[string]*os.File), 108 outputDir: outputDir, 109 } 110 defer logWriter.closeFiles(logger) 111 112 // Track some state that persists across lines 113 testResultMarkers := TestResultMarkerStack{} 114 previousTestName := "" 115 116 var err error 117 reader := bufio.NewReader(read) 118 for { 119 var data string 120 data, err = reader.ReadString('\n') 121 if len(data) == 0 && err == io.EOF { 122 break 123 } 124 125 data = strings.TrimSuffix(data, "\n") 126 127 // separate block so that we do not overwrite the err variable that we need afterwards to check if we're done 128 { 129 indentLevel := len(getIndent(data)) 130 isIndented := indentLevel > 0 131 132 // Garbage collection of test result markers. Primary purpose is to detect when we dedent out, which can only be 133 // detected when we reach a dedented line. 134 testResultMarkers = testResultMarkers.removeDedentedTestResultMarkers(indentLevel) 135 136 // Handle each possible category of test lines 137 switch { 138 case isSummaryLine(data): 139 logWriter.writeLog(logger, "summary", data) 140 141 case isStatusLine(data): 142 testName := getTestNameFromStatusLine(data) 143 previousTestName = testName 144 logWriter.writeLog(logger, testName, data) 145 146 case strings.HasPrefix(data, "Test"): 147 // Heuristic: `go test` will only execute test functions named `Test.*`, so we assume any line prefixed 148 // with `Test` is a test output for a named test. Also assume that test output will be space delimeted and 149 // test names can't contain spaces (because they are function names). 150 // This must be modified when `logger.DoLog` changes. 151 vals := strings.Split(data, " ") 152 testName := vals[0] 153 previousTestName = testName 154 logWriter.writeLog(logger, testName, data) 155 156 case isIndented && isResultLine(data): 157 // In a nested test result block, so collect the line into all the test results we have seen so far. 158 for _, marker := range testResultMarkers { 159 logWriter.writeLog(logger, marker.TestName, data) 160 } 161 162 case isPanicLine(data): 163 // When panic, we want all subsequent nonstandard test lines to roll up to the summary 164 previousTestName = "summary" 165 logWriter.writeLog(logger, "summary", data) 166 167 case isResultLine(data): 168 // We ignore result lines, because that is handled specially below. 169 170 case previousTestName != "": 171 // Base case: roll up to the previous test line, if it exists. 172 // Handles case where terratest log has entries with newlines in them. 173 logWriter.writeLog(logger, previousTestName, data) 174 175 default: 176 logger.Warnf("Found test line that does not match known cases: %s", data) 177 } 178 179 // This has to happen separately from main if block to handle the special case of nested tests (e.g table driven 180 // tests). For those result lines, we want it to roll up to the parent test, so we need to run the handler in 181 // the `isIndented` section. But for both root and indented result lines, we want to execute the following code, 182 // hence this special block. 183 if isResultLine(data) { 184 testName := getTestNameFromResultLine(data) 185 logWriter.writeLog(logger, testName, data) 186 logWriter.writeLog(logger, "summary", data) 187 188 marker := TestResultMarker{ 189 TestName: testName, 190 IndentLevel: indentLevel, 191 } 192 testResultMarkers = testResultMarkers.push(marker) 193 } 194 } 195 196 if err != nil { 197 break 198 } 199 } 200 if err != io.EOF { 201 logger.Fatalf("Error reading from Reader: %s", err) 202 } 203 }