forked from gruntwork-io/terratest
-
Notifications
You must be signed in to change notification settings - Fork 0
/
parser.go
180 lines (161 loc) · 6.47 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
// 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(`^(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,
reader 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 := ""
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
data := scanner.Text()
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
if isSummaryLine(data) {
logWriter.writeLog(logger, "summary", data)
} else if isStatusLine(data) {
testName := getTestNameFromStatusLine(data)
logWriter.writeLog(logger, testName, data)
} else if 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]
logWriter.writeLog(logger, testName, data)
previousTestName = testName
} else if isIndented && previousTestName != "summary" {
// In a test result block, so collect the line into all the test results we have seen so far.
// Note that previousTestName would only be set to summary if we saw a panic line.
for _, marker := range testResultMarkers {
logWriter.writeLog(logger, marker.TestName, data)
}
} else if isPanicLine(data) {
// When panic, we want all subsequent nonstandard test lines to roll up to the summary
previousTestName = "summary"
logWriter.writeLog(logger, "summary", data)
} else if 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)
} else if !isResultLine(data) {
// Result Lines are handled below
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 := scanner.Err(); err != nil {
logger.Fatalf("Error reading from scanner: %s", err)
}
}