package parser import ( "encoding/json" "fmt" "strconv" "strings" "github.com/smartystreets/goconvey/convey/reporting" "github.com/smartystreets/goconvey/web/server/contract" ) type testParser struct { test *contract.TestResult line string index int inJson bool jsonLines []string otherLines []string } func parseTestOutput(test *contract.TestResult) *contract.TestResult { parser := newTestParser(test) parser.parseTestFunctionOutput() return test } func newTestParser(test *contract.TestResult) *testParser { self := new(testParser) self.test = test return self } func (self *testParser) parseTestFunctionOutput() { if len(self.test.RawLines) > 0 { self.processLines() self.deserializeJson() self.composeCapturedOutput() } } func (self *testParser) processLines() { for self.index, self.line = range self.test.RawLines { if !self.processLine() { break } } } func (self *testParser) processLine() bool { if strings.HasSuffix(self.line, reporting.OpenJson) { self.inJson = true self.accountForOutputWithoutNewline() } else if self.line == reporting.CloseJson { self.inJson = false } else if self.inJson { self.jsonLines = append(self.jsonLines, self.line) } else if isPanic(self.line) { self.parsePanicOutput() return false } else if isGoTestLogOutput(self.line) { self.parseLogLocation() } else { self.otherLines = append(self.otherLines, self.line) } return true } // If fmt.Print(f) produces output with no \n and that output // is that last output before the framework spits out json // (which starts with ''>>>>>'') then without this code // all of the json is counted as output, not as json to be // parsed and displayed by the web UI. func (self *testParser) accountForOutputWithoutNewline() { prefix := strings.Split(self.line, reporting.OpenJson)[0] if prefix != "" { self.otherLines = append(self.otherLines, prefix) } } func (self *testParser) deserializeJson() { formatted := createArrayForJsonItems(self.jsonLines) var scopes []reporting.ScopeResult err := json.Unmarshal(formatted, &scopes) if err != nil { panic(fmt.Sprintf(bugReportRequest, err, formatted)) } self.test.Stories = scopes } func (self *testParser) parsePanicOutput() { for index, line := range self.test.RawLines[self.index:] { self.parsePanicLocation(index, line) self.preserveStackTraceIndentation(index, line) } self.test.Error = strings.Join(self.test.RawLines, "\n") } func (self *testParser) parsePanicLocation(index int, line string) { if !panicLineHasMetadata(line) { return } metaLine := self.test.RawLines[index+4] fields := strings.Split(metaLine, " ") fileAndLine := strings.Split(fields[0], ":") self.test.File = fileAndLine[0] if len(fileAndLine) >= 2 { self.test.Line, _ = strconv.Atoi(fileAndLine[1]) } } func (self *testParser) preserveStackTraceIndentation(index int, line string) { if panicLineShouldBeIndented(index, line) { self.test.RawLines[index] = "\t" + line } } func (self *testParser) parseLogLocation() { self.otherLines = append(self.otherLines, self.line) lineFields := strings.TrimSpace(self.line) if strings.HasPrefix(lineFields, "Error Trace:") { lineFields = strings.TrimPrefix(lineFields, "Error Trace:") } fields := strings.Split(lineFields, ":") self.test.File = strings.TrimSpace(fields[0]) self.test.Line, _ = strconv.Atoi(fields[1]) } func (self *testParser) composeCapturedOutput() { self.test.Message = strings.Join(self.otherLines, "\n") } func createArrayForJsonItems(lines []string) []byte { jsonArrayItems := strings.Join(lines, "") jsonArrayItems = removeTrailingComma(jsonArrayItems) return []byte(fmt.Sprintf("[%s]\n", jsonArrayItems)) } func removeTrailingComma(rawJson string) string { if trailingComma(rawJson) { return rawJson[:len(rawJson)-1] } return rawJson } func trailingComma(value string) bool { return strings.HasSuffix(value, ",") } func isGoTestLogOutput(line string) bool { return strings.Count(line, ":") == 2 } func isPanic(line string) bool { return strings.HasPrefix(line, "panic: ") } func panicLineHasMetadata(line string) bool { return strings.HasPrefix(line, "goroutine") && strings.Contains(line, "[running]") } func panicLineShouldBeIndented(index int, line string) bool { return strings.Contains(line, "+") || (index > 0 && strings.Contains(line, "panic: ")) } const bugReportRequest = ` Uh-oh! Looks like something went wrong. Please copy the following text and file a bug report at: https://github.com/smartystreets/goconvey/issues?state=open ======= BEGIN BUG REPORT ======= ERROR: %v OUTPUT: %s ======= END BUG REPORT ======= `