parser.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  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. regexPackageWithTest = regexp.MustCompile(`^# ([^\[\]]+) \[[^\]]+\]$`)
  61. )
  62. // Parse parses go test output from reader r and returns a report with the
  63. // results. An optional pkgName can be given, which is used in case a package
  64. // result line is missing.
  65. func Parse(r io.Reader, pkgName string) (*Report, error) {
  66. reader := bufio.NewReader(r)
  67. report := &Report{make([]Package, 0)}
  68. // keep track of tests we find
  69. var tests []*Test
  70. // keep track of benchmarks we find
  71. var benchmarks []*Benchmark
  72. // sum of tests' time, use this if current test has no result line (when it is compiled test)
  73. var testsTime time.Duration
  74. // current test
  75. var cur string
  76. // coverage percentage report for current package
  77. var coveragePct string
  78. // stores mapping between package name and output of build failures
  79. var packageCaptures = map[string][]string{}
  80. // the name of the package which it's build failure output is being captured
  81. var capturedPackage string
  82. // capture any non-test output
  83. var buffers = map[string][]string{}
  84. // parse lines
  85. for {
  86. l, _, err := reader.ReadLine()
  87. if err != nil && err == io.EOF {
  88. break
  89. } else if err != nil {
  90. return nil, err
  91. }
  92. line := string(l)
  93. if strings.HasPrefix(line, "=== RUN ") {
  94. // new test
  95. cur = strings.TrimSpace(line[8:])
  96. tests = append(tests, &Test{
  97. Name: cur,
  98. Result: FAIL,
  99. Output: make([]string, 0),
  100. })
  101. // clear the current build package, so output lines won't be added to that build
  102. capturedPackage = ""
  103. } else if matches := regexBenchmark.FindStringSubmatch(line); len(matches) == 6 {
  104. bytes, _ := strconv.Atoi(matches[4])
  105. allocs, _ := strconv.Atoi(matches[5])
  106. benchmarks = append(benchmarks, &Benchmark{
  107. Name: matches[1],
  108. Duration: parseNanoseconds(matches[3]),
  109. Bytes: bytes,
  110. Allocs: allocs,
  111. })
  112. } else if strings.HasPrefix(line, "=== PAUSE ") {
  113. continue
  114. } else if strings.HasPrefix(line, "=== CONT ") {
  115. cur = strings.TrimSpace(line[8:])
  116. continue
  117. } else if matches := regexResult.FindStringSubmatch(line); len(matches) == 6 {
  118. if matches[5] != "" {
  119. coveragePct = matches[5]
  120. }
  121. if strings.HasSuffix(matches[4], "failed]") {
  122. // the build of the package failed, inject a dummy test into the package
  123. // which indicate about the failure and contain the failure description.
  124. tests = append(tests, &Test{
  125. Name: matches[4],
  126. Result: FAIL,
  127. Output: packageCaptures[matches[2]],
  128. })
  129. } else if matches[1] == "FAIL" && !containsFailures(tests) && len(buffers[cur]) > 0 {
  130. // This package didn't have any failing tests, but still it
  131. // failed with some output. Create a dummy test with the
  132. // output.
  133. tests = append(tests, &Test{
  134. Name: "Failure",
  135. Result: FAIL,
  136. Output: buffers[cur],
  137. })
  138. buffers[cur] = buffers[cur][0:0]
  139. }
  140. // all tests in this package are finished
  141. report.Packages = append(report.Packages, Package{
  142. Name: matches[2],
  143. Duration: parseSeconds(matches[3]),
  144. Tests: tests,
  145. Benchmarks: benchmarks,
  146. CoveragePct: coveragePct,
  147. Time: int(parseSeconds(matches[3]) / time.Millisecond), // deprecated
  148. })
  149. buffers[cur] = buffers[cur][0:0]
  150. tests = make([]*Test, 0)
  151. benchmarks = make([]*Benchmark, 0)
  152. coveragePct = ""
  153. cur = ""
  154. testsTime = 0
  155. } else if matches := regexStatus.FindStringSubmatch(line); len(matches) == 4 {
  156. cur = matches[2]
  157. test := findTest(tests, cur)
  158. if test == nil {
  159. continue
  160. }
  161. // test status
  162. if matches[1] == "PASS" {
  163. test.Result = PASS
  164. } else if matches[1] == "SKIP" {
  165. test.Result = SKIP
  166. } else {
  167. test.Result = FAIL
  168. }
  169. if matches := regexIndent.FindStringSubmatch(line); len(matches) == 2 {
  170. test.SubtestIndent = matches[1]
  171. }
  172. test.Output = buffers[cur]
  173. test.Name = matches[2]
  174. test.Duration = parseSeconds(matches[3])
  175. testsTime += test.Duration
  176. test.Time = int(test.Duration / time.Millisecond) // deprecated
  177. } else if matches := regexCoverage.FindStringSubmatch(line); len(matches) == 2 {
  178. coveragePct = matches[1]
  179. } else if matches := regexOutput.FindStringSubmatch(line); capturedPackage == "" && len(matches) == 3 {
  180. // Sub-tests start with one or more series of 4-space indents, followed by a hard tab,
  181. // followed by the test output
  182. // Top-level tests start with a hard tab.
  183. test := findTest(tests, cur)
  184. if test == nil {
  185. continue
  186. }
  187. test.Output = append(test.Output, matches[2])
  188. } else if strings.HasPrefix(line, "# ") {
  189. // indicates a capture of build output of a package. set the current build package.
  190. packageWithTestBinary := regexPackageWithTest.FindStringSubmatch(line)
  191. if packageWithTestBinary != nil {
  192. // Sometimes, the text after "# " shows the name of the test binary
  193. // ("<package>.test") in addition to the package
  194. // e.g.: "# package/name [package/name.test]"
  195. capturedPackage = packageWithTestBinary[1]
  196. } else {
  197. capturedPackage = line[2:]
  198. }
  199. } else if capturedPackage != "" {
  200. // current line is build failure capture for the current built package
  201. packageCaptures[capturedPackage] = append(packageCaptures[capturedPackage], line)
  202. } else if regexSummary.MatchString(line) {
  203. // unset current test name so any additional output after the
  204. // summary is captured separately.
  205. cur = ""
  206. } else {
  207. // buffer anything else that we didn't recognize
  208. buffers[cur] = append(buffers[cur], line)
  209. // if we have a current test, also append to its output
  210. test := findTest(tests, cur)
  211. if test != nil {
  212. if strings.HasPrefix(line, test.SubtestIndent+" ") {
  213. test.Output = append(test.Output, strings.TrimPrefix(line, test.SubtestIndent+" "))
  214. }
  215. }
  216. }
  217. }
  218. if len(tests) > 0 {
  219. // no result line found
  220. report.Packages = append(report.Packages, Package{
  221. Name: pkgName,
  222. Duration: testsTime,
  223. Time: int(testsTime / time.Millisecond),
  224. Tests: tests,
  225. Benchmarks: benchmarks,
  226. CoveragePct: coveragePct,
  227. })
  228. }
  229. return report, nil
  230. }
  231. func parseSeconds(t string) time.Duration {
  232. if t == "" {
  233. return time.Duration(0)
  234. }
  235. // ignore error
  236. d, _ := time.ParseDuration(t + "s")
  237. return d
  238. }
  239. func parseNanoseconds(t string) time.Duration {
  240. // note: if input < 1 ns precision, result will be 0s.
  241. if t == "" {
  242. return time.Duration(0)
  243. }
  244. // ignore error
  245. d, _ := time.ParseDuration(t + "ns")
  246. return d
  247. }
  248. func findTest(tests []*Test, name string) *Test {
  249. for i := len(tests) - 1; i >= 0; i-- {
  250. if tests[i].Name == name {
  251. return tests[i]
  252. }
  253. }
  254. return nil
  255. }
  256. func containsFailures(tests []*Test) bool {
  257. for _, test := range tests {
  258. if test.Result == FAIL {
  259. return true
  260. }
  261. }
  262. return false
  263. }
  264. // Failures counts the number of failed tests in this report
  265. func (r *Report) Failures() int {
  266. count := 0
  267. for _, p := range r.Packages {
  268. for _, t := range p.Tests {
  269. if t.Result == FAIL {
  270. count++
  271. }
  272. }
  273. }
  274. return count
  275. }