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