summary.go 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. package testjson
  2. import (
  3. "fmt"
  4. "io"
  5. "strings"
  6. "time"
  7. "unicode"
  8. "unicode/utf8"
  9. "github.com/fatih/color"
  10. )
  11. // Summary enumerates the sections which can be printed by PrintSummary
  12. type Summary int
  13. // nolint: golint
  14. const (
  15. SummarizeNone Summary = 0
  16. SummarizeSkipped Summary = (1 << iota) / 2
  17. SummarizeFailed
  18. SummarizeErrors
  19. SummarizeOutput
  20. SummarizeAll = SummarizeSkipped | SummarizeFailed | SummarizeErrors | SummarizeOutput
  21. )
  22. var summaryValues = map[Summary]string{
  23. SummarizeSkipped: "skipped",
  24. SummarizeFailed: "failed",
  25. SummarizeErrors: "errors",
  26. SummarizeOutput: "output",
  27. }
  28. var summaryFromValue = map[string]Summary{
  29. "none": SummarizeNone,
  30. "skipped": SummarizeSkipped,
  31. "failed": SummarizeFailed,
  32. "errors": SummarizeErrors,
  33. "output": SummarizeOutput,
  34. "all": SummarizeAll,
  35. }
  36. func (s Summary) String() string {
  37. if s == SummarizeNone {
  38. return "none"
  39. }
  40. var result []string
  41. for v := Summary(1); v <= s; v <<= 1 {
  42. if s.Includes(v) {
  43. result = append(result, summaryValues[v])
  44. }
  45. }
  46. return strings.Join(result, ",")
  47. }
  48. // Includes returns true if Summary includes all the values set by other.
  49. func (s Summary) Includes(other Summary) bool {
  50. return s&other == other
  51. }
  52. // NewSummary returns a new Summary from a string value. If the string does not
  53. // match any known values returns false for the second value.
  54. func NewSummary(value string) (Summary, bool) {
  55. s, ok := summaryFromValue[value]
  56. return s, ok
  57. }
  58. // PrintSummary of a test Execution. Prints a section for each summary type
  59. // followed by a DONE line.
  60. func PrintSummary(out io.Writer, execution *Execution, opts Summary) error {
  61. execSummary := newExecSummary(execution, opts)
  62. if opts.Includes(SummarizeSkipped) {
  63. writeTestCaseSummary(out, execSummary, formatSkipped())
  64. }
  65. if opts.Includes(SummarizeFailed) {
  66. writeTestCaseSummary(out, execSummary, formatFailed())
  67. }
  68. errors := execution.Errors()
  69. if opts.Includes(SummarizeErrors) {
  70. writeErrorSummary(out, errors)
  71. }
  72. fmt.Fprintf(out, "\n%s %d tests%s%s%s in %s\n",
  73. "DONE", // TODO: maybe color this?
  74. execution.Total(),
  75. formatTestCount(len(execution.Skipped()), "skipped", ""),
  76. formatTestCount(len(execution.Failed()), "failure", "s"),
  77. formatTestCount(countErrors(errors), "error", "s"),
  78. FormatDurationAsSeconds(execution.Elapsed(), 3))
  79. return nil
  80. }
  81. func formatTestCount(count int, category string, pluralize string) string {
  82. switch count {
  83. case 0:
  84. return ""
  85. case 1:
  86. default:
  87. category += pluralize
  88. }
  89. return fmt.Sprintf(", %d %s", count, category)
  90. }
  91. // FormatDurationAsSeconds formats a time.Duration as a float with an s suffix.
  92. func FormatDurationAsSeconds(d time.Duration, precision int) string {
  93. return fmt.Sprintf("%.[2]*[1]fs", d.Seconds(), precision)
  94. }
  95. func writeErrorSummary(out io.Writer, errors []string) {
  96. if len(errors) > 0 {
  97. fmt.Fprintln(out, color.MagentaString("\n=== Errors"))
  98. }
  99. for _, err := range errors {
  100. fmt.Fprintln(out, err)
  101. }
  102. }
  103. // countErrors in stderr lines. Build errors may include multiple lines where
  104. // subsequent lines are indented.
  105. // FIXME: Panics will include multiple lines, and are still overcounted.
  106. func countErrors(errors []string) int {
  107. var count int
  108. for _, line := range errors {
  109. r, _ := utf8.DecodeRuneInString(line)
  110. if !unicode.IsSpace(r) {
  111. count++
  112. }
  113. }
  114. return count
  115. }
  116. type executionSummary interface {
  117. Failed() []TestCase
  118. Skipped() []TestCase
  119. OutputLines(pkg, test string) []string
  120. }
  121. type noOutputSummary struct {
  122. Execution
  123. }
  124. func (s *noOutputSummary) OutputLines(_, _ string) []string {
  125. return nil
  126. }
  127. func newExecSummary(execution *Execution, opts Summary) executionSummary {
  128. if opts.Includes(SummarizeOutput) {
  129. return execution
  130. }
  131. return &noOutputSummary{Execution: *execution}
  132. }
  133. func writeTestCaseSummary(out io.Writer, execution executionSummary, conf testCaseFormatConfig) {
  134. testCases := conf.getter(execution)
  135. if len(testCases) == 0 {
  136. return
  137. }
  138. fmt.Fprintln(out, "\n=== "+conf.header)
  139. for _, tc := range testCases {
  140. fmt.Fprintf(out, "=== %s: %s %s (%s)\n",
  141. conf.prefix,
  142. relativePackagePath(tc.Package),
  143. tc.Test,
  144. FormatDurationAsSeconds(tc.Elapsed, 2))
  145. for _, line := range execution.OutputLines(tc.Package, tc.Test) {
  146. if isRunLine(line) || conf.filter(line) {
  147. continue
  148. }
  149. fmt.Fprint(out, line)
  150. }
  151. fmt.Fprintln(out)
  152. }
  153. }
  154. type testCaseFormatConfig struct {
  155. header string
  156. prefix string
  157. filter func(string) bool
  158. getter func(executionSummary) []TestCase
  159. }
  160. func formatFailed() testCaseFormatConfig {
  161. withColor := color.RedString
  162. return testCaseFormatConfig{
  163. header: withColor("Failed"),
  164. prefix: withColor("FAIL"),
  165. filter: func(line string) bool {
  166. return strings.HasPrefix(line, "--- FAIL: Test")
  167. },
  168. getter: func(execution executionSummary) []TestCase {
  169. return execution.Failed()
  170. },
  171. }
  172. }
  173. func formatSkipped() testCaseFormatConfig {
  174. withColor := color.YellowString
  175. return testCaseFormatConfig{
  176. header: withColor("Skipped"),
  177. prefix: withColor("SKIP"),
  178. filter: func(line string) bool {
  179. return strings.HasPrefix(line, "--- SKIP: Test")
  180. },
  181. getter: func(execution executionSummary) []TestCase {
  182. return execution.Skipped()
  183. },
  184. }
  185. }
  186. func isRunLine(line string) bool {
  187. return strings.HasPrefix(line, "=== RUN Test")
  188. }