testParser.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. package parser
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "strconv"
  6. "strings"
  7. "github.com/smartystreets/goconvey/convey/reporting"
  8. "github.com/smartystreets/goconvey/web/server/contract"
  9. )
  10. type testParser struct {
  11. test *contract.TestResult
  12. line string
  13. index int
  14. inJson bool
  15. jsonLines []string
  16. otherLines []string
  17. }
  18. func parseTestOutput(test *contract.TestResult) *contract.TestResult {
  19. parser := newTestParser(test)
  20. parser.parseTestFunctionOutput()
  21. return test
  22. }
  23. func newTestParser(test *contract.TestResult) *testParser {
  24. self := new(testParser)
  25. self.test = test
  26. return self
  27. }
  28. func (self *testParser) parseTestFunctionOutput() {
  29. if len(self.test.RawLines) > 0 {
  30. self.processLines()
  31. self.deserializeJson()
  32. self.composeCapturedOutput()
  33. }
  34. }
  35. func (self *testParser) processLines() {
  36. for self.index, self.line = range self.test.RawLines {
  37. if !self.processLine() {
  38. break
  39. }
  40. }
  41. }
  42. func (self *testParser) processLine() bool {
  43. if strings.HasSuffix(self.line, reporting.OpenJson) {
  44. self.inJson = true
  45. self.accountForOutputWithoutNewline()
  46. } else if self.line == reporting.CloseJson {
  47. self.inJson = false
  48. } else if self.inJson {
  49. self.jsonLines = append(self.jsonLines, self.line)
  50. } else if isPanic(self.line) {
  51. self.parsePanicOutput()
  52. return false
  53. } else if isGoTestLogOutput(self.line) {
  54. self.parseLogLocation()
  55. } else {
  56. self.otherLines = append(self.otherLines, self.line)
  57. }
  58. return true
  59. }
  60. // If fmt.Print(f) produces output with no \n and that output
  61. // is that last output before the framework spits out json
  62. // (which starts with ''>>>>>'') then without this code
  63. // all of the json is counted as output, not as json to be
  64. // parsed and displayed by the web UI.
  65. func (self *testParser) accountForOutputWithoutNewline() {
  66. prefix := strings.Split(self.line, reporting.OpenJson)[0]
  67. if prefix != "" {
  68. self.otherLines = append(self.otherLines, prefix)
  69. }
  70. }
  71. func (self *testParser) deserializeJson() {
  72. formatted := createArrayForJsonItems(self.jsonLines)
  73. var scopes []reporting.ScopeResult
  74. err := json.Unmarshal(formatted, &scopes)
  75. if err != nil {
  76. panic(fmt.Sprintf(bugReportRequest, err, formatted))
  77. }
  78. self.test.Stories = scopes
  79. }
  80. func (self *testParser) parsePanicOutput() {
  81. for index, line := range self.test.RawLines[self.index:] {
  82. self.parsePanicLocation(index, line)
  83. self.preserveStackTraceIndentation(index, line)
  84. }
  85. self.test.Error = strings.Join(self.test.RawLines, "\n")
  86. }
  87. func (self *testParser) parsePanicLocation(index int, line string) {
  88. if !panicLineHasMetadata(line) {
  89. return
  90. }
  91. metaLine := self.test.RawLines[index+4]
  92. fields := strings.Split(metaLine, " ")
  93. fileAndLine := strings.Split(fields[0], ":")
  94. self.test.File = fileAndLine[0]
  95. if len(fileAndLine) >= 2 {
  96. self.test.Line, _ = strconv.Atoi(fileAndLine[1])
  97. }
  98. }
  99. func (self *testParser) preserveStackTraceIndentation(index int, line string) {
  100. if panicLineShouldBeIndented(index, line) {
  101. self.test.RawLines[index] = "\t" + line
  102. }
  103. }
  104. func (self *testParser) parseLogLocation() {
  105. self.otherLines = append(self.otherLines, self.line)
  106. lineFields := strings.TrimSpace(self.line)
  107. if strings.HasPrefix(lineFields, "Error Trace:") {
  108. lineFields = strings.TrimPrefix(lineFields, "Error Trace:")
  109. }
  110. fields := strings.Split(lineFields, ":")
  111. self.test.File = strings.TrimSpace(fields[0])
  112. self.test.Line, _ = strconv.Atoi(fields[1])
  113. }
  114. func (self *testParser) composeCapturedOutput() {
  115. self.test.Message = strings.Join(self.otherLines, "\n")
  116. }
  117. func createArrayForJsonItems(lines []string) []byte {
  118. jsonArrayItems := strings.Join(lines, "")
  119. jsonArrayItems = removeTrailingComma(jsonArrayItems)
  120. return []byte(fmt.Sprintf("[%s]\n", jsonArrayItems))
  121. }
  122. func removeTrailingComma(rawJson string) string {
  123. if trailingComma(rawJson) {
  124. return rawJson[:len(rawJson)-1]
  125. }
  126. return rawJson
  127. }
  128. func trailingComma(value string) bool {
  129. return strings.HasSuffix(value, ",")
  130. }
  131. func isGoTestLogOutput(line string) bool {
  132. return strings.Count(line, ":") == 2
  133. }
  134. func isPanic(line string) bool {
  135. return strings.HasPrefix(line, "panic: ")
  136. }
  137. func panicLineHasMetadata(line string) bool {
  138. return strings.HasPrefix(line, "goroutine") && strings.Contains(line, "[running]")
  139. }
  140. func panicLineShouldBeIndented(index int, line string) bool {
  141. return strings.Contains(line, "+") || (index > 0 && strings.Contains(line, "panic: "))
  142. }
  143. const bugReportRequest = `
  144. Uh-oh! Looks like something went wrong. Please copy the following text and file a bug report at:
  145. https://github.com/smartystreets/goconvey/issues?state=open
  146. ======= BEGIN BUG REPORT =======
  147. ERROR: %v
  148. OUTPUT: %s
  149. ======= END BUG REPORT =======
  150. `