stenographer.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. /*
  2. The stenographer is used by Ginkgo's reporters to generate output.
  3. Move along, nothing to see here.
  4. */
  5. package stenographer
  6. import (
  7. "fmt"
  8. "io"
  9. "runtime"
  10. "strings"
  11. "github.com/onsi/ginkgo/types"
  12. )
  13. const defaultStyle = "\x1b[0m"
  14. const boldStyle = "\x1b[1m"
  15. const redColor = "\x1b[91m"
  16. const greenColor = "\x1b[32m"
  17. const yellowColor = "\x1b[33m"
  18. const cyanColor = "\x1b[36m"
  19. const grayColor = "\x1b[90m"
  20. const lightGrayColor = "\x1b[37m"
  21. type cursorStateType int
  22. const (
  23. cursorStateTop cursorStateType = iota
  24. cursorStateStreaming
  25. cursorStateMidBlock
  26. cursorStateEndBlock
  27. )
  28. type Stenographer interface {
  29. AnnounceSuite(description string, randomSeed int64, randomizingAll bool, succinct bool)
  30. AnnounceAggregatedParallelRun(nodes int, succinct bool)
  31. AnnounceParallelRun(node int, nodes int, succinct bool)
  32. AnnounceTotalNumberOfSpecs(total int, succinct bool)
  33. AnnounceNumberOfSpecs(specsToRun int, total int, succinct bool)
  34. AnnounceSpecRunCompletion(summary *types.SuiteSummary, succinct bool)
  35. AnnounceSpecWillRun(spec *types.SpecSummary)
  36. AnnounceBeforeSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool)
  37. AnnounceAfterSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool)
  38. AnnounceCapturedOutput(output string)
  39. AnnounceSuccesfulSpec(spec *types.SpecSummary)
  40. AnnounceSuccesfulSlowSpec(spec *types.SpecSummary, succinct bool)
  41. AnnounceSuccesfulMeasurement(spec *types.SpecSummary, succinct bool)
  42. AnnouncePendingSpec(spec *types.SpecSummary, noisy bool)
  43. AnnounceSkippedSpec(spec *types.SpecSummary, succinct bool, fullTrace bool)
  44. AnnounceSpecTimedOut(spec *types.SpecSummary, succinct bool, fullTrace bool)
  45. AnnounceSpecPanicked(spec *types.SpecSummary, succinct bool, fullTrace bool)
  46. AnnounceSpecFailed(spec *types.SpecSummary, succinct bool, fullTrace bool)
  47. SummarizeFailures(summaries []*types.SpecSummary)
  48. }
  49. func New(color bool, enableFlakes bool, writer io.Writer) Stenographer {
  50. denoter := "•"
  51. if runtime.GOOS == "windows" {
  52. denoter = "+"
  53. }
  54. return &consoleStenographer{
  55. color: color,
  56. denoter: denoter,
  57. cursorState: cursorStateTop,
  58. enableFlakes: enableFlakes,
  59. w: writer,
  60. }
  61. }
  62. type consoleStenographer struct {
  63. color bool
  64. denoter string
  65. cursorState cursorStateType
  66. enableFlakes bool
  67. w io.Writer
  68. }
  69. var alternatingColors = []string{defaultStyle, grayColor}
  70. func (s *consoleStenographer) AnnounceSuite(description string, randomSeed int64, randomizingAll bool, succinct bool) {
  71. if succinct {
  72. s.print(0, "[%d] %s ", randomSeed, s.colorize(boldStyle, description))
  73. return
  74. }
  75. s.printBanner(fmt.Sprintf("Running Suite: %s", description), "=")
  76. s.print(0, "Random Seed: %s", s.colorize(boldStyle, "%d", randomSeed))
  77. if randomizingAll {
  78. s.print(0, " - Will randomize all specs")
  79. }
  80. s.printNewLine()
  81. }
  82. func (s *consoleStenographer) AnnounceParallelRun(node int, nodes int, succinct bool) {
  83. if succinct {
  84. s.print(0, "- node #%d ", node)
  85. return
  86. }
  87. s.println(0,
  88. "Parallel test node %s/%s.",
  89. s.colorize(boldStyle, "%d", node),
  90. s.colorize(boldStyle, "%d", nodes),
  91. )
  92. s.printNewLine()
  93. }
  94. func (s *consoleStenographer) AnnounceAggregatedParallelRun(nodes int, succinct bool) {
  95. if succinct {
  96. s.print(0, "- %d nodes ", nodes)
  97. return
  98. }
  99. s.println(0,
  100. "Running in parallel across %s nodes",
  101. s.colorize(boldStyle, "%d", nodes),
  102. )
  103. s.printNewLine()
  104. }
  105. func (s *consoleStenographer) AnnounceNumberOfSpecs(specsToRun int, total int, succinct bool) {
  106. if succinct {
  107. s.print(0, "- %d/%d specs ", specsToRun, total)
  108. s.stream()
  109. return
  110. }
  111. s.println(0,
  112. "Will run %s of %s specs",
  113. s.colorize(boldStyle, "%d", specsToRun),
  114. s.colorize(boldStyle, "%d", total),
  115. )
  116. s.printNewLine()
  117. }
  118. func (s *consoleStenographer) AnnounceTotalNumberOfSpecs(total int, succinct bool) {
  119. if succinct {
  120. s.print(0, "- %d specs ", total)
  121. s.stream()
  122. return
  123. }
  124. s.println(0,
  125. "Will run %s specs",
  126. s.colorize(boldStyle, "%d", total),
  127. )
  128. s.printNewLine()
  129. }
  130. func (s *consoleStenographer) AnnounceSpecRunCompletion(summary *types.SuiteSummary, succinct bool) {
  131. if succinct && summary.SuiteSucceeded {
  132. s.print(0, " %s %s ", s.colorize(greenColor, "SUCCESS!"), summary.RunTime)
  133. return
  134. }
  135. s.printNewLine()
  136. color := greenColor
  137. if !summary.SuiteSucceeded {
  138. color = redColor
  139. }
  140. s.println(0, s.colorize(boldStyle+color, "Ran %d of %d Specs in %.3f seconds", summary.NumberOfSpecsThatWillBeRun, summary.NumberOfTotalSpecs, summary.RunTime.Seconds()))
  141. status := ""
  142. if summary.SuiteSucceeded {
  143. status = s.colorize(boldStyle+greenColor, "SUCCESS!")
  144. } else {
  145. status = s.colorize(boldStyle+redColor, "FAIL!")
  146. }
  147. flakes := ""
  148. if s.enableFlakes {
  149. flakes = " | " + s.colorize(yellowColor+boldStyle, "%d Flaked", summary.NumberOfFlakedSpecs)
  150. }
  151. s.print(0,
  152. "%s -- %s | %s | %s | %s\n",
  153. status,
  154. s.colorize(greenColor+boldStyle, "%d Passed", summary.NumberOfPassedSpecs),
  155. s.colorize(redColor+boldStyle, "%d Failed", summary.NumberOfFailedSpecs)+flakes,
  156. s.colorize(yellowColor+boldStyle, "%d Pending", summary.NumberOfPendingSpecs),
  157. s.colorize(cyanColor+boldStyle, "%d Skipped", summary.NumberOfSkippedSpecs),
  158. )
  159. }
  160. func (s *consoleStenographer) AnnounceSpecWillRun(spec *types.SpecSummary) {
  161. s.startBlock()
  162. for i, text := range spec.ComponentTexts[1 : len(spec.ComponentTexts)-1] {
  163. s.print(0, s.colorize(alternatingColors[i%2], text)+" ")
  164. }
  165. indentation := 0
  166. if len(spec.ComponentTexts) > 2 {
  167. indentation = 1
  168. s.printNewLine()
  169. }
  170. index := len(spec.ComponentTexts) - 1
  171. s.print(indentation, s.colorize(boldStyle, spec.ComponentTexts[index]))
  172. s.printNewLine()
  173. s.print(indentation, s.colorize(lightGrayColor, spec.ComponentCodeLocations[index].String()))
  174. s.printNewLine()
  175. s.midBlock()
  176. }
  177. func (s *consoleStenographer) AnnounceBeforeSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) {
  178. s.announceSetupFailure("BeforeSuite", summary, succinct, fullTrace)
  179. }
  180. func (s *consoleStenographer) AnnounceAfterSuiteFailure(summary *types.SetupSummary, succinct bool, fullTrace bool) {
  181. s.announceSetupFailure("AfterSuite", summary, succinct, fullTrace)
  182. }
  183. func (s *consoleStenographer) announceSetupFailure(name string, summary *types.SetupSummary, succinct bool, fullTrace bool) {
  184. s.startBlock()
  185. var message string
  186. switch summary.State {
  187. case types.SpecStateFailed:
  188. message = "Failure"
  189. case types.SpecStatePanicked:
  190. message = "Panic"
  191. case types.SpecStateTimedOut:
  192. message = "Timeout"
  193. }
  194. s.println(0, s.colorize(redColor+boldStyle, "%s [%.3f seconds]", message, summary.RunTime.Seconds()))
  195. indentation := s.printCodeLocationBlock([]string{name}, []types.CodeLocation{summary.CodeLocation}, summary.ComponentType, 0, summary.State, true)
  196. s.printNewLine()
  197. s.printFailure(indentation, summary.State, summary.Failure, fullTrace)
  198. s.endBlock()
  199. }
  200. func (s *consoleStenographer) AnnounceCapturedOutput(output string) {
  201. if output == "" {
  202. return
  203. }
  204. s.startBlock()
  205. s.println(0, output)
  206. s.midBlock()
  207. }
  208. func (s *consoleStenographer) AnnounceSuccesfulSpec(spec *types.SpecSummary) {
  209. s.print(0, s.colorize(greenColor, s.denoter))
  210. s.stream()
  211. }
  212. func (s *consoleStenographer) AnnounceSuccesfulSlowSpec(spec *types.SpecSummary, succinct bool) {
  213. s.printBlockWithMessage(
  214. s.colorize(greenColor, "%s [SLOW TEST:%.3f seconds]", s.denoter, spec.RunTime.Seconds()),
  215. "",
  216. spec,
  217. succinct,
  218. )
  219. }
  220. func (s *consoleStenographer) AnnounceSuccesfulMeasurement(spec *types.SpecSummary, succinct bool) {
  221. s.printBlockWithMessage(
  222. s.colorize(greenColor, "%s [MEASUREMENT]", s.denoter),
  223. s.measurementReport(spec, succinct),
  224. spec,
  225. succinct,
  226. )
  227. }
  228. func (s *consoleStenographer) AnnouncePendingSpec(spec *types.SpecSummary, noisy bool) {
  229. if noisy {
  230. s.printBlockWithMessage(
  231. s.colorize(yellowColor, "P [PENDING]"),
  232. "",
  233. spec,
  234. false,
  235. )
  236. } else {
  237. s.print(0, s.colorize(yellowColor, "P"))
  238. s.stream()
  239. }
  240. }
  241. func (s *consoleStenographer) AnnounceSkippedSpec(spec *types.SpecSummary, succinct bool, fullTrace bool) {
  242. // Skips at runtime will have a non-empty spec.Failure. All others should be succinct.
  243. if succinct || spec.Failure == (types.SpecFailure{}) {
  244. s.print(0, s.colorize(cyanColor, "S"))
  245. s.stream()
  246. } else {
  247. s.startBlock()
  248. s.println(0, s.colorize(cyanColor+boldStyle, "S [SKIPPING]%s [%.3f seconds]", s.failureContext(spec.Failure.ComponentType), spec.RunTime.Seconds()))
  249. indentation := s.printCodeLocationBlock(spec.ComponentTexts, spec.ComponentCodeLocations, spec.Failure.ComponentType, spec.Failure.ComponentIndex, spec.State, succinct)
  250. s.printNewLine()
  251. s.printSkip(indentation, spec.Failure)
  252. s.endBlock()
  253. }
  254. }
  255. func (s *consoleStenographer) AnnounceSpecTimedOut(spec *types.SpecSummary, succinct bool, fullTrace bool) {
  256. s.printSpecFailure(fmt.Sprintf("%s... Timeout", s.denoter), spec, succinct, fullTrace)
  257. }
  258. func (s *consoleStenographer) AnnounceSpecPanicked(spec *types.SpecSummary, succinct bool, fullTrace bool) {
  259. s.printSpecFailure(fmt.Sprintf("%s! Panic", s.denoter), spec, succinct, fullTrace)
  260. }
  261. func (s *consoleStenographer) AnnounceSpecFailed(spec *types.SpecSummary, succinct bool, fullTrace bool) {
  262. s.printSpecFailure(fmt.Sprintf("%s Failure", s.denoter), spec, succinct, fullTrace)
  263. }
  264. func (s *consoleStenographer) SummarizeFailures(summaries []*types.SpecSummary) {
  265. failingSpecs := []*types.SpecSummary{}
  266. for _, summary := range summaries {
  267. if summary.HasFailureState() {
  268. failingSpecs = append(failingSpecs, summary)
  269. }
  270. }
  271. if len(failingSpecs) == 0 {
  272. return
  273. }
  274. s.printNewLine()
  275. s.printNewLine()
  276. plural := "s"
  277. if len(failingSpecs) == 1 {
  278. plural = ""
  279. }
  280. s.println(0, s.colorize(redColor+boldStyle, "Summarizing %d Failure%s:", len(failingSpecs), plural))
  281. for _, summary := range failingSpecs {
  282. s.printNewLine()
  283. if summary.HasFailureState() {
  284. if summary.TimedOut() {
  285. s.print(0, s.colorize(redColor+boldStyle, "[Timeout...] "))
  286. } else if summary.Panicked() {
  287. s.print(0, s.colorize(redColor+boldStyle, "[Panic!] "))
  288. } else if summary.Failed() {
  289. s.print(0, s.colorize(redColor+boldStyle, "[Fail] "))
  290. }
  291. s.printSpecContext(summary.ComponentTexts, summary.ComponentCodeLocations, summary.Failure.ComponentType, summary.Failure.ComponentIndex, summary.State, true)
  292. s.printNewLine()
  293. s.println(0, s.colorize(lightGrayColor, summary.Failure.Location.String()))
  294. }
  295. }
  296. }
  297. func (s *consoleStenographer) startBlock() {
  298. if s.cursorState == cursorStateStreaming {
  299. s.printNewLine()
  300. s.printDelimiter()
  301. } else if s.cursorState == cursorStateMidBlock {
  302. s.printNewLine()
  303. }
  304. }
  305. func (s *consoleStenographer) midBlock() {
  306. s.cursorState = cursorStateMidBlock
  307. }
  308. func (s *consoleStenographer) endBlock() {
  309. s.printDelimiter()
  310. s.cursorState = cursorStateEndBlock
  311. }
  312. func (s *consoleStenographer) stream() {
  313. s.cursorState = cursorStateStreaming
  314. }
  315. func (s *consoleStenographer) printBlockWithMessage(header string, message string, spec *types.SpecSummary, succinct bool) {
  316. s.startBlock()
  317. s.println(0, header)
  318. indentation := s.printCodeLocationBlock(spec.ComponentTexts, spec.ComponentCodeLocations, types.SpecComponentTypeInvalid, 0, spec.State, succinct)
  319. if message != "" {
  320. s.printNewLine()
  321. s.println(indentation, message)
  322. }
  323. s.endBlock()
  324. }
  325. func (s *consoleStenographer) printSpecFailure(message string, spec *types.SpecSummary, succinct bool, fullTrace bool) {
  326. s.startBlock()
  327. s.println(0, s.colorize(redColor+boldStyle, "%s%s [%.3f seconds]", message, s.failureContext(spec.Failure.ComponentType), spec.RunTime.Seconds()))
  328. indentation := s.printCodeLocationBlock(spec.ComponentTexts, spec.ComponentCodeLocations, spec.Failure.ComponentType, spec.Failure.ComponentIndex, spec.State, succinct)
  329. s.printNewLine()
  330. s.printFailure(indentation, spec.State, spec.Failure, fullTrace)
  331. s.endBlock()
  332. }
  333. func (s *consoleStenographer) failureContext(failedComponentType types.SpecComponentType) string {
  334. switch failedComponentType {
  335. case types.SpecComponentTypeBeforeSuite:
  336. return " in Suite Setup (BeforeSuite)"
  337. case types.SpecComponentTypeAfterSuite:
  338. return " in Suite Teardown (AfterSuite)"
  339. case types.SpecComponentTypeBeforeEach:
  340. return " in Spec Setup (BeforeEach)"
  341. case types.SpecComponentTypeJustBeforeEach:
  342. return " in Spec Setup (JustBeforeEach)"
  343. case types.SpecComponentTypeAfterEach:
  344. return " in Spec Teardown (AfterEach)"
  345. }
  346. return ""
  347. }
  348. func (s *consoleStenographer) printSkip(indentation int, spec types.SpecFailure) {
  349. s.println(indentation, s.colorize(cyanColor, spec.Message))
  350. s.printNewLine()
  351. s.println(indentation, spec.Location.String())
  352. }
  353. func (s *consoleStenographer) printFailure(indentation int, state types.SpecState, failure types.SpecFailure, fullTrace bool) {
  354. if state == types.SpecStatePanicked {
  355. s.println(indentation, s.colorize(redColor+boldStyle, failure.Message))
  356. s.println(indentation, s.colorize(redColor, failure.ForwardedPanic))
  357. s.println(indentation, failure.Location.String())
  358. s.printNewLine()
  359. s.println(indentation, s.colorize(redColor, "Full Stack Trace"))
  360. s.println(indentation, failure.Location.FullStackTrace)
  361. } else {
  362. s.println(indentation, s.colorize(redColor, failure.Message))
  363. s.printNewLine()
  364. s.println(indentation, failure.Location.String())
  365. if fullTrace {
  366. s.printNewLine()
  367. s.println(indentation, s.colorize(redColor, "Full Stack Trace"))
  368. s.println(indentation, failure.Location.FullStackTrace)
  369. }
  370. }
  371. }
  372. func (s *consoleStenographer) printSpecContext(componentTexts []string, componentCodeLocations []types.CodeLocation, failedComponentType types.SpecComponentType, failedComponentIndex int, state types.SpecState, succinct bool) int {
  373. startIndex := 1
  374. indentation := 0
  375. if len(componentTexts) == 1 {
  376. startIndex = 0
  377. }
  378. for i := startIndex; i < len(componentTexts); i++ {
  379. if (state.IsFailure() || state == types.SpecStateSkipped) && i == failedComponentIndex {
  380. color := redColor
  381. if state == types.SpecStateSkipped {
  382. color = cyanColor
  383. }
  384. blockType := ""
  385. switch failedComponentType {
  386. case types.SpecComponentTypeBeforeSuite:
  387. blockType = "BeforeSuite"
  388. case types.SpecComponentTypeAfterSuite:
  389. blockType = "AfterSuite"
  390. case types.SpecComponentTypeBeforeEach:
  391. blockType = "BeforeEach"
  392. case types.SpecComponentTypeJustBeforeEach:
  393. blockType = "JustBeforeEach"
  394. case types.SpecComponentTypeAfterEach:
  395. blockType = "AfterEach"
  396. case types.SpecComponentTypeIt:
  397. blockType = "It"
  398. case types.SpecComponentTypeMeasure:
  399. blockType = "Measurement"
  400. }
  401. if succinct {
  402. s.print(0, s.colorize(color+boldStyle, "[%s] %s ", blockType, componentTexts[i]))
  403. } else {
  404. s.println(indentation, s.colorize(color+boldStyle, "%s [%s]", componentTexts[i], blockType))
  405. s.println(indentation, s.colorize(grayColor, "%s", componentCodeLocations[i]))
  406. }
  407. } else {
  408. if succinct {
  409. s.print(0, s.colorize(alternatingColors[i%2], "%s ", componentTexts[i]))
  410. } else {
  411. s.println(indentation, componentTexts[i])
  412. s.println(indentation, s.colorize(grayColor, "%s", componentCodeLocations[i]))
  413. }
  414. }
  415. indentation++
  416. }
  417. return indentation
  418. }
  419. func (s *consoleStenographer) printCodeLocationBlock(componentTexts []string, componentCodeLocations []types.CodeLocation, failedComponentType types.SpecComponentType, failedComponentIndex int, state types.SpecState, succinct bool) int {
  420. indentation := s.printSpecContext(componentTexts, componentCodeLocations, failedComponentType, failedComponentIndex, state, succinct)
  421. if succinct {
  422. if len(componentTexts) > 0 {
  423. s.printNewLine()
  424. s.print(0, s.colorize(lightGrayColor, "%s", componentCodeLocations[len(componentCodeLocations)-1]))
  425. }
  426. s.printNewLine()
  427. indentation = 1
  428. } else {
  429. indentation--
  430. }
  431. return indentation
  432. }
  433. func (s *consoleStenographer) orderedMeasurementKeys(measurements map[string]*types.SpecMeasurement) []string {
  434. orderedKeys := make([]string, len(measurements))
  435. for key, measurement := range measurements {
  436. orderedKeys[measurement.Order] = key
  437. }
  438. return orderedKeys
  439. }
  440. func (s *consoleStenographer) measurementReport(spec *types.SpecSummary, succinct bool) string {
  441. if len(spec.Measurements) == 0 {
  442. return "Found no measurements"
  443. }
  444. message := []string{}
  445. orderedKeys := s.orderedMeasurementKeys(spec.Measurements)
  446. if succinct {
  447. message = append(message, fmt.Sprintf("%s samples:", s.colorize(boldStyle, "%d", spec.NumberOfSamples)))
  448. for _, key := range orderedKeys {
  449. measurement := spec.Measurements[key]
  450. message = append(message, fmt.Sprintf(" %s - %s: %s%s, %s: %s%s ± %s%s, %s: %s%s",
  451. s.colorize(boldStyle, "%s", measurement.Name),
  452. measurement.SmallestLabel,
  453. s.colorize(greenColor, measurement.PrecisionFmt(), measurement.Smallest),
  454. measurement.Units,
  455. measurement.AverageLabel,
  456. s.colorize(cyanColor, measurement.PrecisionFmt(), measurement.Average),
  457. measurement.Units,
  458. s.colorize(cyanColor, measurement.PrecisionFmt(), measurement.StdDeviation),
  459. measurement.Units,
  460. measurement.LargestLabel,
  461. s.colorize(redColor, measurement.PrecisionFmt(), measurement.Largest),
  462. measurement.Units,
  463. ))
  464. }
  465. } else {
  466. message = append(message, fmt.Sprintf("Ran %s samples:", s.colorize(boldStyle, "%d", spec.NumberOfSamples)))
  467. for _, key := range orderedKeys {
  468. measurement := spec.Measurements[key]
  469. info := ""
  470. if measurement.Info != nil {
  471. message = append(message, fmt.Sprintf("%v", measurement.Info))
  472. }
  473. message = append(message, fmt.Sprintf("%s:\n%s %s: %s%s\n %s: %s%s\n %s: %s%s ± %s%s",
  474. s.colorize(boldStyle, "%s", measurement.Name),
  475. info,
  476. measurement.SmallestLabel,
  477. s.colorize(greenColor, measurement.PrecisionFmt(), measurement.Smallest),
  478. measurement.Units,
  479. measurement.LargestLabel,
  480. s.colorize(redColor, measurement.PrecisionFmt(), measurement.Largest),
  481. measurement.Units,
  482. measurement.AverageLabel,
  483. s.colorize(cyanColor, measurement.PrecisionFmt(), measurement.Average),
  484. measurement.Units,
  485. s.colorize(cyanColor, measurement.PrecisionFmt(), measurement.StdDeviation),
  486. measurement.Units,
  487. ))
  488. }
  489. }
  490. return strings.Join(message, "\n")
  491. }