github.com/terraform-modules-krish/terratest@v0.29.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(`^(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 logWriter.writeLog(logger, testName, data) 144 145 case strings.HasPrefix(data, "Test"): 146 // Heuristic: `go test` will only execute test functions named `Test.*`, so we assume any line prefixed 147 // with `Test` is a test output for a named test. Also assume that test output will be space delimeted and 148 // test names can't contain spaces (because they are function names). 149 // This must be modified when `logger.DoLog` changes. 150 vals := strings.Split(data, " ") 151 testName := vals[0] 152 logWriter.writeLog(logger, testName, data) 153 previousTestName = testName 154 155 case isIndented && previousTestName != "summary": 156 // In a test result block, so collect the line into all the test results we have seen so far. 157 // Note that previousTestName would only be set to summary if we saw a panic line. 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 previousTestName != "": 168 // Base case: roll up to the previous test line, if it exists. 169 // Handles case where terratest log has entries with newlines in them. 170 logWriter.writeLog(logger, previousTestName, data) 171 172 case !isResultLine(data): 173 // Result Lines are handled below 174 logger.Warnf("Found test line that does not match known cases: %s", data) 175 } 176 177 // This has to happen separately from main if block to handle the special case of nested tests (e.g table driven 178 // tests). For those result lines, we want it to roll up to the parent test, so we need to run the handler in 179 // the `isIndented` section. But for both root and indented result lines, we want to execute the following code, 180 // hence this special block. 181 if isResultLine(data) { 182 testName := getTestNameFromResultLine(data) 183 logWriter.writeLog(logger, testName, data) 184 logWriter.writeLog(logger, "summary", data) 185 186 marker := TestResultMarker{ 187 TestName: testName, 188 IndentLevel: indentLevel, 189 } 190 testResultMarkers = testResultMarkers.push(marker) 191 } 192 } 193 194 if err != nil { 195 break 196 } 197 } 198 if err != io.EOF { 199 logger.Fatalf("Error reading from Reader: %s", err) 200 } 201 }