github.com/billybanfield/evergreen@v0.0.0-20170525200750-eeee692790f7/plugin/builtin/gotest/parser.go (about) 1 package gotest 2 3 import ( 4 "bufio" 5 "io" 6 "regexp" 7 "strings" 8 "time" 9 10 "github.com/pkg/errors" 11 ) 12 13 const ( 14 PASS = "PASS" 15 FAIL = "FAIL" 16 SKIP = "SKIP" 17 18 // Match the start prefix and save the group of non-space characters following the word "RUN" 19 StartRegexString = `=== RUN\s+(\S+)` 20 21 // Match the end prefix, save PASS/FAIL/SKIP, save the decimal value for number of seconds 22 EndRegexString = `--- (PASS|SKIP|FAIL): (\S+) \(([0-9.]+[ ]*s)` 23 24 // Match the start prefix and save the group of non-space characters following the word "RUN" 25 GocheckStartRegexString = `START: .*.go:[0-9]+: (\S+)` 26 27 // Match the end prefix, save PASS/FAIL/SKIP, save the decimal value for number of seconds 28 GocheckEndRegexString = `(PASS|SKIP|FAIL): .*.go:[0-9]+: (\S+)\s*([0-9.]+[ ]*s)?` 29 ) 30 31 var startRegex = regexp.MustCompile(StartRegexString) 32 var endRegex = regexp.MustCompile(EndRegexString) 33 var gocheckStartRegex = regexp.MustCompile(GocheckStartRegexString) 34 var gocheckEndRegex = regexp.MustCompile(GocheckEndRegexString) 35 36 // Parser is an interface for parsing go test output, producing 37 // test logs and test results 38 type Parser interface { 39 // Parse takes a reader for test output, and reads 40 // until the reader is exhausted. Any parsing erros 41 // are returned. 42 Parse(io.Reader) error 43 44 // Logs returns an array of strings, each entry a line in 45 // the test output. 46 Logs() []string 47 48 // Results returns an array of test results. Parse() must be called 49 // before this. 50 Results() []*TestResult 51 } 52 53 // This test result implementation maps more idiomatically to Go's test output 54 // than the TestResult type in the model package. Results are converted to the 55 // model type before being sent to the server. 56 type TestResult struct { 57 // The name of the test 58 Name string 59 // The name of the test suite the test is a part of. 60 // Currently, for this plugin, this is the name of the package 61 // being tested, prefixed with a unique number to avoid 62 // collisions when packages have the same name 63 SuiteName string 64 // The result status of the test 65 Status string 66 // How long the test took to run 67 RunTime time.Duration 68 // Number representing the starting log line number of the test 69 // in the test's logged output 70 StartLine int 71 // Number representing the last line of the test in log output 72 EndLine int 73 74 // Can be set to mark the id of the server-side log that this 75 // results corresponds to 76 LogId string 77 } 78 79 // VanillaParser parses tests following go test output format. 80 // This should cover regular go tests as well as those written with the 81 // popular testing packages goconvey and gocheck. 82 type VanillaParser struct { 83 Suite string 84 logs []string 85 // map for storing tests during parsing 86 tests map[string]*TestResult 87 order []string 88 } 89 90 // Logs returns an array of logs captured during test execution. 91 func (vp *VanillaParser) Logs() []string { 92 return vp.logs 93 } 94 95 // Results returns an array of test results parsed during test execution. 96 func (vp *VanillaParser) Results() []*TestResult { 97 out := []*TestResult{} 98 99 for _, name := range vp.order { 100 out = append(out, vp.tests[name]) 101 } 102 103 return out 104 } 105 106 // Parse reads in a test's output and stores the results and logs. 107 func (vp *VanillaParser) Parse(testOutput io.Reader) error { 108 testScanner := bufio.NewScanner(testOutput) 109 vp.tests = map[string]*TestResult{} 110 for testScanner.Scan() { 111 if err := testScanner.Err(); err != nil { 112 return errors.Wrap(err, "error reading test output") 113 } 114 // logs are appended at the start of the loop, allowing 115 // len(vp.logs) to represent the current line number [1...] 116 logLine := testScanner.Text() 117 vp.logs = append(vp.logs, logLine) 118 if err := vp.handleLine(logLine); err != nil { 119 return errors.WithStack(err) 120 } 121 } 122 return nil 123 } 124 125 // handleLine attempts to parse and store any test updates from the given line. 126 func (vp *VanillaParser) handleLine(line string) error { 127 // This is gross, and could all go away with the resolution of 128 // https://code.google.com/p/go/issues/detail?id=2981 129 switch { 130 case startRegex.MatchString(line): 131 return vp.handleStart(line, startRegex, true) 132 case gocheckStartRegex.MatchString(line): 133 return vp.handleStart(line, gocheckStartRegex, false) 134 case endRegex.MatchString(line): 135 return vp.handleEnd(line, endRegex) 136 case gocheckEndRegex.MatchString(line): 137 return vp.handleEnd(line, gocheckEndRegex) 138 } 139 return nil 140 } 141 142 // handleEnd gets the end data from an ending line and stores it. 143 func (vp *VanillaParser) handleEnd(line string, rgx *regexp.Regexp) error { 144 name, status, duration, err := endInfoFromLogLine(line, rgx) 145 if err != nil { 146 return errors.Wrapf(err, "error parsing end line '%s'", line) 147 } 148 t := vp.tests[name] 149 if t == nil || t.Name == "" { 150 // if there's no existing test, just stub one out 151 t = vp.newTestResult(name) 152 vp.order = append(vp.order, name) 153 vp.tests[name] = t 154 } 155 t.Status = status 156 t.RunTime = duration 157 t.EndLine = len(vp.logs) 158 159 return nil 160 } 161 162 // handleStart gets the data from a start line and stores it. 163 func (vp *VanillaParser) handleStart(line string, rgx *regexp.Regexp, defaultFail bool) error { 164 name, err := startInfoFromLogLine(line, rgx) 165 if err != nil { 166 return errors.Wrapf(err, "error parsing start line '%s'", line) 167 } 168 t := vp.newTestResult(name) 169 170 // tasks should start out failed unless they're marked 171 // passing/skipped, although gocheck can't support this 172 if defaultFail { 173 t.Status = FAIL 174 } else { 175 t.Status = PASS 176 } 177 178 vp.tests[name] = t 179 vp.order = append(vp.order, name) 180 181 return nil 182 } 183 184 // newTestResult populates a test result type with the given 185 // test name, current suite, and current line number. 186 func (vp *VanillaParser) newTestResult(name string) *TestResult { 187 return &TestResult{ 188 Name: name, 189 SuiteName: vp.Suite, 190 StartLine: len(vp.logs), 191 } 192 } 193 194 // startInfoFromLogLine gets the test name from a log line 195 // indicating the start of a test. Returns test name 196 // and an error if one occurs. 197 func startInfoFromLogLine(line string, rgx *regexp.Regexp) (string, error) { 198 matches := rgx.FindStringSubmatch(line) 199 if len(matches) < 2 { 200 // futureproofing -- this can't happen as long as we 201 // check Match() before calling startInfoFromLogLine 202 return "", errors.Errorf( 203 "unable to match start line regular expression on line: %s", line) 204 } 205 return matches[1], nil 206 } 207 208 // endInfoFromLogLine gets the test name, result status, and Duration 209 // from a log line. Returns those matched elements, as well as any error 210 // in regex or duration parsing. 211 func endInfoFromLogLine(line string, rgx *regexp.Regexp) (string, string, time.Duration, error) { 212 matches := rgx.FindStringSubmatch(line) 213 if len(matches) < 4 { 214 // this block should never be reached if we call endRegex.Match() 215 // before entering this function 216 return "", "", 0, errors.Errorf( 217 "unable to match end line regular expression on line: %s", line) 218 } 219 status := matches[1] 220 name := matches[2] 221 var duration time.Duration 222 if matches[3] != "" { 223 var err error 224 duration, err = time.ParseDuration(strings.Replace(matches[3], " ", "", -1)) 225 if err != nil { 226 return "", "", 0, errors.Wrap(err, "error parsing test runtime") 227 } 228 } 229 return name, status, duration, nil 230 }