parser.go 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. package parser
  2. import (
  3. "bufio"
  4. "io"
  5. "regexp"
  6. "strconv"
  7. "strings"
  8. "time"
  9. )
  10. // Result represents a test result.
  11. type Result int
  12. // Test result constants
  13. const (
  14. PASS Result = iota
  15. FAIL
  16. SKIP
  17. )
  18. // Report is a collection of package tests.
  19. type Report struct {
  20. Packages []Package
  21. }
  22. // Package contains the test results of a single package.
  23. type Package struct {
  24. Name string
  25. Duration time.Duration
  26. Tests []*Test
  27. Benchmarks []*Benchmark
  28. CoveragePct string
  29. // Time is deprecated, use Duration instead.
  30. Time int // in milliseconds
  31. }
  32. // Test contains the results of a single test.
  33. type Test struct {
  34. Name string
  35. Duration time.Duration
  36. Result Result
  37. Output []string
  38. SubtestIndent string
  39. // Time is deprecated, use Duration instead.
  40. Time int // in milliseconds
  41. }
  42. // Benchmark contains the results of a single benchmark.
  43. type Benchmark struct {
  44. Name string
  45. Duration time.Duration
  46. // number of B/op
  47. Bytes int
  48. // number of allocs/op
  49. Allocs int
  50. }
  51. var (
  52. regexStatus = regexp.MustCompile(`--- (PASS|FAIL|SKIP): (.+) \((\d+\.\d+)(?: seconds|s)\)`)
  53. regexIndent = regexp.MustCompile(`^([ \t]+)---`)
  54. regexCoverage = regexp.MustCompile(`^coverage:\s+(\d+\.\d+)%\s+of\s+statements(?:\sin\s.+)?$`)
  55. regexResult = regexp.MustCompile(`^(ok|FAIL)\s+([^ ]+)\s+(?:(\d+\.\d+)s|\(cached\)|(\[\w+ failed]))(?:\s+coverage:\s+(\d+\.\d+)%\sof\sstatements(?:\sin\s.+)?)?$`)
  56. // regexBenchmark captures 3-5 groups: benchmark name, number of times ran, ns/op (with or without decimal), B/op (optional), and allocs/op (optional).
  57. regexBenchmark = regexp.MustCompile(`^(Benchmark[^ -]+)(?:-\d+\s+|\s+)(\d+)\s+(\d+|\d+\.\d+)\sns/op(?:\s+(\d+)\sB/op)?(?:\s+(\d+)\sallocs/op)?`)
  58. regexOutput = regexp.MustCompile(`( )*\t(.*)`)
  59. regexSummary = regexp.MustCompile(`^(PASS|FAIL|SKIP)$`)
  60. )
  61. // Parse parses go test output from reader r and returns a report with the
  62. // results. An optional pkgName can be given, which is used in case a package
  63. // result line is missing.
  64. func Parse(r io.Reader, pkgName string) (*Report, error) {
  65. reader := bufio.NewReader(r)
  66. report := &Report{make([]Package, 0)}
  67. // keep track of tests we find
  68. var tests []*Test
  69. // keep track of benchmarks we find
  70. var benchmarks []*Benchmark
  71. // sum of tests' time, use this if current test has no result line (when it is compiled test)
  72. var testsTime time.Duration
  73. // current test
  74. var cur string
  75. // keep track if we've already seen a summary for the current test
  76. var seenSummary bool
  77. // coverage percentage report for current package
  78. var coveragePct string
  79. // stores mapping between package name and output of build failures
  80. var packageCaptures = map[string][]string{}
  81. // the name of the package which it's build failure output is being captured
  82. var capturedPackage string
  83. // capture any non-test output
  84. var buffers = map[string][]string{}
  85. // parse lines
  86. for {
  87. l, _, err := reader.ReadLine()
  88. if err != nil && err == io.EOF {
  89. break
  90. } else if err != nil {
  91. return nil, err
  92. }
  93. line := string(l)
  94. if strings.HasPrefix(line, "=== RUN ") {
  95. // new test
  96. cur = strings.TrimSpace(line[8:])
  97. tests = append(tests, &Test{
  98. Name: cur,
  99. Result: FAIL,
  100. Output: make([]string, 0),
  101. })
  102. // clear the current build package, so output lines won't be added to that build
  103. capturedPackage = ""
  104. seenSummary = false
  105. } else if matches := regexBenchmark.FindStringSubmatch(line); len(matches) == 6 {
  106. bytes, _ := strconv.Atoi(matches[4])
  107. allocs, _ := strconv.Atoi(matches[5])
  108. benchmarks = append(benchmarks, &Benchmark{
  109. Name: matches[1],
  110. Duration: parseNanoseconds(matches[3]),
  111. Bytes: bytes,
  112. Allocs: allocs,
  113. })
  114. } else if strings.HasPrefix(line, "=== PAUSE ") {
  115. continue
  116. } else if strings.HasPrefix(line, "=== CONT ") {
  117. cur = strings.TrimSpace(line[8:])
  118. continue
  119. } else if matches := regexResult.FindStringSubmatch(line); len(matches) == 6 {
  120. if matches[5] != "" {
  121. coveragePct = matches[5]
  122. }
  123. if strings.HasSuffix(matches[4], "failed]") {
  124. // the build of the package failed, inject a dummy test into the package
  125. // which indicate about the failure and contain the failure description.
  126. tests = append(tests, &Test{
  127. Name: matches[4],
  128. Result: FAIL,
  129. Output: packageCaptures[matches[2]],
  130. })
  131. } else if matches[1] == "FAIL" && len(tests) == 0 && len(buffers[cur]) > 0 {
  132. // This package didn't have any tests, but it failed with some
  133. // output. Create a dummy test with the output.
  134. tests = append(tests, &Test{
  135. Name: "Failure",
  136. Result: FAIL,
  137. Output: buffers[cur],
  138. })
  139. buffers[cur] = buffers[cur][0:0]
  140. }
  141. // all tests in this package are finished
  142. report.Packages = append(report.Packages, Package{
  143. Name: matches[2],
  144. Duration: parseSeconds(matches[3]),
  145. Tests: tests,
  146. Benchmarks: benchmarks,
  147. CoveragePct: coveragePct,
  148. Time: int(parseSeconds(matches[3]) / time.Millisecond), // deprecated
  149. })
  150. buffers[cur] = buffers[cur][0:0]
  151. tests = make([]*Test, 0)
  152. benchmarks = make([]*Benchmark, 0)
  153. coveragePct = ""
  154. cur = ""
  155. testsTime = 0
  156. } else if matches := regexStatus.FindStringSubmatch(line); len(matches) == 4 {
  157. cur = matches[2]
  158. test := findTest(tests, cur)
  159. if test == nil {
  160. continue
  161. }
  162. // test status
  163. if matches[1] == "PASS" {
  164. test.Result = PASS
  165. } else if matches[1] == "SKIP" {
  166. test.Result = SKIP
  167. } else {
  168. test.Result = FAIL
  169. }
  170. if matches := regexIndent.FindStringSubmatch(line); len(matches) == 2 {
  171. test.SubtestIndent = matches[1]
  172. }
  173. test.Output = buffers[cur]
  174. test.Name = matches[2]
  175. test.Duration = parseSeconds(matches[3])
  176. testsTime += test.Duration
  177. test.Time = int(test.Duration / time.Millisecond) // deprecated
  178. } else if matches := regexCoverage.FindStringSubmatch(line); len(matches) == 2 {
  179. coveragePct = matches[1]
  180. } else if matches := regexOutput.FindStringSubmatch(line); capturedPackage == "" && len(matches) == 3 {
  181. // Sub-tests start with one or more series of 4-space indents, followed by a hard tab,
  182. // followed by the test output
  183. // Top-level tests start with a hard tab.
  184. test := findTest(tests, cur)
  185. if test == nil {
  186. continue
  187. }
  188. test.Output = append(test.Output, matches[2])
  189. } else if strings.HasPrefix(line, "# ") {
  190. // indicates a capture of build output of a package. set the current build package.
  191. capturedPackage = line[2:]
  192. } else if capturedPackage != "" {
  193. // current line is build failure capture for the current built package
  194. packageCaptures[capturedPackage] = append(packageCaptures[capturedPackage], line)
  195. } else if regexSummary.MatchString(line) {
  196. // don't store any output after the summary
  197. seenSummary = true
  198. } else if !seenSummary {
  199. // buffer anything else that we didn't recognize
  200. buffers[cur] = append(buffers[cur], line)
  201. // if we have a current test, also append to its output
  202. test := findTest(tests, cur)
  203. if test != nil {
  204. if strings.HasPrefix(line, test.SubtestIndent+" ") {
  205. test.Output = append(test.Output, strings.TrimPrefix(line, test.SubtestIndent+" "))
  206. }
  207. }
  208. }
  209. }
  210. if len(tests) > 0 {
  211. // no result line found
  212. report.Packages = append(report.Packages, Package{
  213. Name: pkgName,
  214. Duration: testsTime,
  215. Time: int(testsTime / time.Millisecond),
  216. Tests: tests,
  217. Benchmarks: benchmarks,
  218. CoveragePct: coveragePct,
  219. })
  220. }
  221. return report, nil
  222. }
  223. func parseSeconds(t string) time.Duration {
  224. if t == "" {
  225. return time.Duration(0)
  226. }
  227. // ignore error
  228. d, _ := time.ParseDuration(t + "s")
  229. return d
  230. }
  231. func parseNanoseconds(t string) time.Duration {
  232. // note: if input < 1 ns precision, result will be 0s.
  233. if t == "" {
  234. return time.Duration(0)
  235. }
  236. // ignore error
  237. d, _ := time.ParseDuration(t + "ns")
  238. return d
  239. }
  240. func findTest(tests []*Test, name string) *Test {
  241. for i := len(tests) - 1; i >= 0; i-- {
  242. if tests[i].Name == name {
  243. return tests[i]
  244. }
  245. }
  246. return nil
  247. }
  248. // Failures counts the number of failed tests in this report
  249. func (r *Report) Failures() int {
  250. count := 0
  251. for _, p := range r.Packages {
  252. for _, t := range p.Tests {
  253. if t.Result == FAIL {
  254. count++
  255. }
  256. }
  257. }
  258. return count
  259. }