github.com/mponton/terratest@v0.44.0/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 // 51 // in: " --- FAIL: TestSnafu" 52 // out: " " 53 func getIndent(data string) string { 54 re := regexp.MustCompile(`^\s+`) 55 indent := re.FindString(data) 56 return indent 57 } 58 59 // getTestNameFromResultLine takes a go testing result line and extracts out the test name 60 // Example: 61 // 62 // in: --- FAIL: TestSnafu 63 // out: TestSnafu 64 func getTestNameFromResultLine(text string) string { 65 m := regexResult.FindStringSubmatch(text) 66 return m[2] 67 } 68 69 // isResultLine checks if a line of text matches a test result (begins with "--- FAIL" or "--- PASS") 70 func isResultLine(text string) bool { 71 return regexResult.MatchString(text) 72 } 73 74 // getTestNameFromStatusLine takes a go testing status line and extracts out the test name 75 // Example: 76 // 77 // in: === RUN TestSnafu 78 // out: TestSnafu 79 func getTestNameFromStatusLine(text string) string { 80 m := regexStatus.FindStringSubmatch(text) 81 return m[2] 82 } 83 84 // isStatusLine checks if a line of text matches a test status 85 func isStatusLine(text string) bool { 86 return regexStatus.MatchString(text) 87 } 88 89 // isSummaryLine checks if a line of text matches the test summary 90 func isSummaryLine(text string) bool { 91 return regexSummary.MatchString(text) 92 } 93 94 // isPanicLine checks if a line of text matches a panic 95 func isPanicLine(text string) bool { 96 return regexPanic.MatchString(text) 97 } 98 99 // parseAndStoreTestOutput will take test log entries from terratest and aggregate the output by test. Takes advantage 100 // of the fact that terratest logs are prefixed by the test name. This will store the broken out logs into files under 101 // the outputDir, named by test name. 102 // Additionally will take test result lines and collect them under a summary log file named `summary.log`. 103 // See the `fixtures` directory for some examples. 104 func parseAndStoreTestOutput( 105 logger *logrus.Logger, 106 read io.Reader, 107 outputDir string, 108 ) { 109 logWriter := LogWriter{ 110 lookup: make(map[string]*os.File), 111 outputDir: outputDir, 112 } 113 defer logWriter.closeFiles(logger) 114 115 // Track some state that persists across lines 116 testResultMarkers := TestResultMarkerStack{} 117 previousTestName := "" 118 119 var err error 120 reader := bufio.NewReader(read) 121 for { 122 var data string 123 data, err = reader.ReadString('\n') 124 if len(data) == 0 && err == io.EOF { 125 break 126 } 127 128 data = strings.TrimSuffix(data, "\n") 129 130 // separate block so that we do not overwrite the err variable that we need afterwards to check if we're done 131 { 132 indentLevel := len(getIndent(data)) 133 isIndented := indentLevel > 0 134 135 // Garbage collection of test result markers. Primary purpose is to detect when we dedent out, which can only be 136 // detected when we reach a dedented line. 137 testResultMarkers = testResultMarkers.removeDedentedTestResultMarkers(indentLevel) 138 139 // Handle each possible category of test lines 140 switch { 141 case isSummaryLine(data): 142 logWriter.writeLog(logger, "summary", data) 143 144 case isStatusLine(data): 145 testName := getTestNameFromStatusLine(data) 146 previousTestName = testName 147 logWriter.writeLog(logger, testName, data) 148 149 case strings.HasPrefix(data, "Test"): 150 // Heuristic: `go test` will only execute test functions named `Test.*`, so we assume any line prefixed 151 // with `Test` is a test output for a named test. Also assume that test output will be space delimeted and 152 // test names can't contain spaces (because they are function names). 153 // This must be modified when `logger.DoLog` changes. 154 vals := strings.Split(data, " ") 155 testName := vals[0] 156 previousTestName = testName 157 logWriter.writeLog(logger, testName, data) 158 159 case isIndented && isResultLine(data): 160 // In a nested test result block, so collect the line into all the test results we have seen so far. 161 for _, marker := range testResultMarkers { 162 logWriter.writeLog(logger, marker.TestName, data) 163 } 164 165 case isPanicLine(data): 166 // When panic, we want all subsequent nonstandard test lines to roll up to the summary 167 previousTestName = "summary" 168 logWriter.writeLog(logger, "summary", data) 169 170 case isResultLine(data): 171 // We ignore result lines, because that is handled specially below. 172 173 case previousTestName != "": 174 // Base case: roll up to the previous test line, if it exists. 175 // Handles case where terratest log has entries with newlines in them. 176 logWriter.writeLog(logger, previousTestName, data) 177 178 default: 179 logger.Warnf("Found test line that does not match known cases: %s", data) 180 } 181 182 // This has to happen separately from main if block to handle the special case of nested tests (e.g table driven 183 // tests). For those result lines, we want it to roll up to the parent test, so we need to run the handler in 184 // the `isIndented` section. But for both root and indented result lines, we want to execute the following code, 185 // hence this special block. 186 if isResultLine(data) { 187 testName := getTestNameFromResultLine(data) 188 logWriter.writeLog(logger, testName, data) 189 logWriter.writeLog(logger, "summary", data) 190 191 marker := TestResultMarker{ 192 TestName: testName, 193 IndentLevel: indentLevel, 194 } 195 testResultMarkers = testResultMarkers.push(marker) 196 } 197 } 198 199 if err != nil { 200 break 201 } 202 } 203 if err != io.EOF { 204 logger.Fatalf("Error reading from Reader: %s", err) 205 } 206 }