modestbench 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (357) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +131 -34
  3. package/dist/cli/commands/analyze.cjs +60 -0
  4. package/dist/cli/commands/analyze.cjs.map +1 -0
  5. package/dist/cli/commands/analyze.d.cts +35 -0
  6. package/dist/cli/commands/analyze.d.cts.map +1 -0
  7. package/dist/cli/commands/analyze.d.ts +35 -0
  8. package/dist/cli/commands/analyze.d.ts.map +1 -0
  9. package/dist/cli/commands/analyze.js +56 -0
  10. package/dist/cli/commands/analyze.js.map +1 -0
  11. package/dist/cli/commands/baseline.cjs +404 -0
  12. package/dist/cli/commands/baseline.cjs.map +1 -0
  13. package/dist/cli/commands/baseline.d.cts +72 -0
  14. package/dist/cli/commands/baseline.d.cts.map +1 -0
  15. package/dist/cli/commands/baseline.d.ts +72 -0
  16. package/dist/cli/commands/baseline.d.ts.map +1 -0
  17. package/dist/cli/commands/baseline.js +396 -0
  18. package/dist/cli/commands/baseline.js.map +1 -0
  19. package/dist/cli/commands/history.d.cts +1 -1
  20. package/dist/cli/commands/history.d.cts.map +1 -1
  21. package/dist/cli/commands/history.d.ts +1 -1
  22. package/dist/cli/commands/history.d.ts.map +1 -1
  23. package/dist/cli/commands/init.cjs +99 -166
  24. package/dist/cli/commands/init.cjs.map +1 -1
  25. package/dist/cli/commands/init.d.cts +4 -4
  26. package/dist/cli/commands/init.d.cts.map +1 -1
  27. package/dist/cli/commands/init.d.ts +4 -4
  28. package/dist/cli/commands/init.d.ts.map +1 -1
  29. package/dist/cli/commands/init.js +99 -166
  30. package/dist/cli/commands/init.js.map +1 -1
  31. package/dist/cli/commands/run.cjs +146 -127
  32. package/dist/cli/commands/run.cjs.map +1 -1
  33. package/dist/cli/commands/run.d.cts +16 -3
  34. package/dist/cli/commands/run.d.cts.map +1 -1
  35. package/dist/cli/commands/run.d.ts +16 -3
  36. package/dist/cli/commands/run.d.ts.map +1 -1
  37. package/dist/cli/commands/run.js +145 -93
  38. package/dist/cli/commands/run.js.map +1 -1
  39. package/dist/cli/index.cjs +583 -394
  40. package/dist/cli/index.cjs.map +1 -1
  41. package/dist/cli/index.d.cts +4 -16
  42. package/dist/cli/index.d.cts.map +1 -1
  43. package/dist/cli/index.d.ts +4 -16
  44. package/dist/cli/index.d.ts.map +1 -1
  45. package/dist/cli/index.js +575 -386
  46. package/dist/cli/index.js.map +1 -1
  47. package/dist/config/budget-schema.cjs +172 -0
  48. package/dist/config/budget-schema.cjs.map +1 -0
  49. package/dist/config/budget-schema.d.cts +59 -0
  50. package/dist/config/budget-schema.d.cts.map +1 -0
  51. package/dist/config/budget-schema.d.ts +59 -0
  52. package/dist/config/budget-schema.d.ts.map +1 -0
  53. package/dist/config/budget-schema.js +166 -0
  54. package/dist/config/budget-schema.js.map +1 -0
  55. package/dist/config/schema.cjs +182 -2
  56. package/dist/config/schema.cjs.map +1 -1
  57. package/dist/config/schema.d.cts +122 -3
  58. package/dist/config/schema.d.cts.map +1 -1
  59. package/dist/config/schema.d.ts +122 -3
  60. package/dist/config/schema.d.ts.map +1 -1
  61. package/dist/config/schema.js +180 -1
  62. package/dist/config/schema.js.map +1 -1
  63. package/dist/constants.cjs +45 -2
  64. package/dist/constants.cjs.map +1 -1
  65. package/dist/constants.d.cts +41 -0
  66. package/dist/constants.d.cts.map +1 -1
  67. package/dist/constants.d.ts +41 -0
  68. package/dist/constants.d.ts.map +1 -1
  69. package/dist/constants.js +44 -1
  70. package/dist/constants.js.map +1 -1
  71. package/dist/core/engine.cjs +114 -23
  72. package/dist/core/engine.cjs.map +1 -1
  73. package/dist/core/engine.d.cts +7 -7
  74. package/dist/core/engine.d.cts.map +1 -1
  75. package/dist/core/engine.d.ts +7 -7
  76. package/dist/core/engine.d.ts.map +1 -1
  77. package/dist/core/engine.js +115 -24
  78. package/dist/core/engine.js.map +1 -1
  79. package/dist/core/engines/accurate-engine.cjs +171 -36
  80. package/dist/core/engines/accurate-engine.cjs.map +1 -1
  81. package/dist/core/engines/accurate-engine.d.cts +5 -0
  82. package/dist/core/engines/accurate-engine.d.cts.map +1 -1
  83. package/dist/core/engines/accurate-engine.d.ts +5 -0
  84. package/dist/core/engines/accurate-engine.d.ts.map +1 -1
  85. package/dist/core/engines/accurate-engine.js +171 -36
  86. package/dist/core/engines/accurate-engine.js.map +1 -1
  87. package/dist/core/engines/tinybench-engine.cjs +3 -2
  88. package/dist/core/engines/tinybench-engine.cjs.map +1 -1
  89. package/dist/core/engines/tinybench-engine.d.cts.map +1 -1
  90. package/dist/core/engines/tinybench-engine.d.ts.map +1 -1
  91. package/dist/core/engines/tinybench-engine.js +3 -2
  92. package/dist/core/engines/tinybench-engine.js.map +1 -1
  93. package/dist/core/output-path-resolver.cjs +8 -1
  94. package/dist/core/output-path-resolver.cjs.map +1 -1
  95. package/dist/core/output-path-resolver.d.cts.map +1 -1
  96. package/dist/core/output-path-resolver.d.ts.map +1 -1
  97. package/dist/core/output-path-resolver.js +9 -2
  98. package/dist/core/output-path-resolver.js.map +1 -1
  99. package/dist/errors/base.cjs +12 -3
  100. package/dist/errors/base.cjs.map +1 -1
  101. package/dist/errors/base.d.cts +7 -0
  102. package/dist/errors/base.d.cts.map +1 -1
  103. package/dist/errors/base.d.ts +7 -0
  104. package/dist/errors/base.d.ts.map +1 -1
  105. package/dist/errors/base.js +10 -2
  106. package/dist/errors/base.js.map +1 -1
  107. package/dist/errors/budget.cjs +37 -0
  108. package/dist/errors/budget.cjs.map +1 -0
  109. package/dist/errors/budget.d.cts +31 -0
  110. package/dist/errors/budget.d.cts.map +1 -0
  111. package/dist/errors/budget.d.ts +31 -0
  112. package/dist/errors/budget.d.ts.map +1 -0
  113. package/dist/errors/budget.js +33 -0
  114. package/dist/errors/budget.js.map +1 -0
  115. package/dist/errors/index.cjs +4 -1
  116. package/dist/errors/index.cjs.map +1 -1
  117. package/dist/errors/index.d.cts +1 -0
  118. package/dist/errors/index.d.cts.map +1 -1
  119. package/dist/errors/index.d.ts +1 -0
  120. package/dist/errors/index.d.ts.map +1 -1
  121. package/dist/errors/index.js +2 -0
  122. package/dist/errors/index.js.map +1 -1
  123. package/dist/index.cjs +13 -1
  124. package/dist/index.cjs.map +1 -1
  125. package/dist/index.d.cts +5 -0
  126. package/dist/index.d.cts.map +1 -1
  127. package/dist/index.d.ts +5 -0
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +7 -0
  130. package/dist/index.js.map +1 -1
  131. package/dist/reporters/csv.cjs +37 -17
  132. package/dist/reporters/csv.cjs.map +1 -1
  133. package/dist/reporters/csv.d.cts +3 -6
  134. package/dist/reporters/csv.d.cts.map +1 -1
  135. package/dist/reporters/csv.d.ts +3 -6
  136. package/dist/reporters/csv.d.ts.map +1 -1
  137. package/dist/reporters/csv.js +37 -17
  138. package/dist/reporters/csv.js.map +1 -1
  139. package/dist/reporters/human.cjs +290 -67
  140. package/dist/reporters/human.cjs.map +1 -1
  141. package/dist/reporters/human.d.cts +25 -13
  142. package/dist/reporters/human.d.cts.map +1 -1
  143. package/dist/reporters/human.d.ts +25 -13
  144. package/dist/reporters/human.d.ts.map +1 -1
  145. package/dist/reporters/human.js +290 -67
  146. package/dist/reporters/human.js.map +1 -1
  147. package/dist/reporters/json.cjs +23 -48
  148. package/dist/reporters/json.cjs.map +1 -1
  149. package/dist/reporters/json.d.cts +2 -28
  150. package/dist/reporters/json.d.cts.map +1 -1
  151. package/dist/reporters/json.d.ts +2 -28
  152. package/dist/reporters/json.d.ts.map +1 -1
  153. package/dist/reporters/json.js +25 -50
  154. package/dist/reporters/json.js.map +1 -1
  155. package/dist/reporters/profile-human.cjs +154 -0
  156. package/dist/reporters/profile-human.cjs.map +1 -0
  157. package/dist/reporters/profile-human.d.cts +44 -0
  158. package/dist/reporters/profile-human.d.cts.map +1 -0
  159. package/dist/reporters/profile-human.d.ts +44 -0
  160. package/dist/reporters/profile-human.d.ts.map +1 -0
  161. package/dist/reporters/profile-human.js +147 -0
  162. package/dist/reporters/profile-human.js.map +1 -0
  163. package/dist/reporters/simple.cjs +67 -45
  164. package/dist/reporters/simple.cjs.map +1 -1
  165. package/dist/reporters/simple.d.cts +14 -14
  166. package/dist/reporters/simple.d.cts.map +1 -1
  167. package/dist/reporters/simple.d.ts +14 -14
  168. package/dist/reporters/simple.d.ts.map +1 -1
  169. package/dist/reporters/simple.js +67 -45
  170. package/dist/reporters/simple.js.map +1 -1
  171. package/dist/schema/modestbench-config.schema.json +153 -0
  172. package/dist/services/baseline-storage.cjs +151 -0
  173. package/dist/services/baseline-storage.cjs.map +1 -0
  174. package/dist/services/baseline-storage.d.cts +55 -0
  175. package/dist/services/baseline-storage.d.cts.map +1 -0
  176. package/dist/services/baseline-storage.d.ts +55 -0
  177. package/dist/services/baseline-storage.d.ts.map +1 -0
  178. package/dist/services/baseline-storage.js +147 -0
  179. package/dist/services/baseline-storage.js.map +1 -0
  180. package/dist/services/budget-evaluator.cjs +146 -0
  181. package/dist/services/budget-evaluator.cjs.map +1 -0
  182. package/dist/services/budget-evaluator.d.cts +29 -0
  183. package/dist/services/budget-evaluator.d.cts.map +1 -0
  184. package/dist/services/budget-evaluator.d.ts +29 -0
  185. package/dist/services/budget-evaluator.d.ts.map +1 -0
  186. package/dist/services/budget-evaluator.js +142 -0
  187. package/dist/services/budget-evaluator.js.map +1 -0
  188. package/dist/services/config-manager.cjs +24 -10
  189. package/dist/services/config-manager.cjs.map +1 -1
  190. package/dist/services/config-manager.d.cts +6 -1
  191. package/dist/services/config-manager.d.cts.map +1 -1
  192. package/dist/services/config-manager.d.ts +6 -1
  193. package/dist/services/config-manager.d.ts.map +1 -1
  194. package/dist/services/config-manager.js +24 -10
  195. package/dist/services/config-manager.js.map +1 -1
  196. package/dist/services/file-loader.cjs +3 -6
  197. package/dist/services/file-loader.cjs.map +1 -1
  198. package/dist/services/file-loader.d.cts.map +1 -1
  199. package/dist/services/file-loader.d.ts.map +1 -1
  200. package/dist/services/file-loader.js +3 -6
  201. package/dist/services/file-loader.js.map +1 -1
  202. package/dist/services/profiler/profile-filter.cjs +116 -0
  203. package/dist/services/profiler/profile-filter.cjs.map +1 -0
  204. package/dist/services/profiler/profile-filter.d.cts +20 -0
  205. package/dist/services/profiler/profile-filter.d.cts.map +1 -0
  206. package/dist/services/profiler/profile-filter.d.ts +20 -0
  207. package/dist/services/profiler/profile-filter.d.ts.map +1 -0
  208. package/dist/services/profiler/profile-filter.js +112 -0
  209. package/dist/services/profiler/profile-filter.js.map +1 -0
  210. package/dist/services/profiler/profile-parser.cjs +139 -0
  211. package/dist/services/profiler/profile-parser.cjs.map +1 -0
  212. package/dist/services/profiler/profile-parser.d.cts +18 -0
  213. package/dist/services/profiler/profile-parser.d.cts.map +1 -0
  214. package/dist/services/profiler/profile-parser.d.ts +18 -0
  215. package/dist/services/profiler/profile-parser.d.ts.map +1 -0
  216. package/dist/services/profiler/profile-parser.js +132 -0
  217. package/dist/services/profiler/profile-parser.js.map +1 -0
  218. package/dist/services/profiler/profile-runner.cjs +90 -0
  219. package/dist/services/profiler/profile-runner.cjs.map +1 -0
  220. package/dist/services/profiler/profile-runner.d.cts +29 -0
  221. package/dist/services/profiler/profile-runner.d.cts.map +1 -0
  222. package/dist/services/profiler/profile-runner.d.ts +29 -0
  223. package/dist/services/profiler/profile-runner.d.ts.map +1 -0
  224. package/dist/services/profiler/profile-runner.js +86 -0
  225. package/dist/services/profiler/profile-runner.js.map +1 -0
  226. package/dist/services/progress-manager.cjs +10 -2
  227. package/dist/services/progress-manager.cjs.map +1 -1
  228. package/dist/services/progress-manager.d.cts +2 -0
  229. package/dist/services/progress-manager.d.cts.map +1 -1
  230. package/dist/services/progress-manager.d.ts +2 -0
  231. package/dist/services/progress-manager.d.ts.map +1 -1
  232. package/dist/services/progress-manager.js +10 -2
  233. package/dist/services/progress-manager.js.map +1 -1
  234. package/dist/services/reporter-registry.cjs +18 -24
  235. package/dist/services/reporter-registry.cjs.map +1 -1
  236. package/dist/services/reporter-registry.d.cts +18 -40
  237. package/dist/services/reporter-registry.d.cts.map +1 -1
  238. package/dist/services/reporter-registry.d.ts +18 -40
  239. package/dist/services/reporter-registry.d.ts.map +1 -1
  240. package/dist/services/reporter-registry.js +18 -24
  241. package/dist/services/reporter-registry.js.map +1 -1
  242. package/dist/types/budgets.cjs +8 -0
  243. package/dist/types/budgets.cjs.map +1 -0
  244. package/dist/types/budgets.d.cts +149 -0
  245. package/dist/types/budgets.d.cts.map +1 -0
  246. package/dist/types/budgets.d.ts +149 -0
  247. package/dist/types/budgets.d.ts.map +1 -0
  248. package/dist/types/budgets.js +7 -0
  249. package/dist/types/budgets.js.map +1 -0
  250. package/dist/types/cli.cjs +2 -11
  251. package/dist/types/cli.cjs.map +1 -1
  252. package/dist/types/cli.d.cts +3 -227
  253. package/dist/types/cli.d.cts.map +1 -1
  254. package/dist/types/cli.d.ts +3 -227
  255. package/dist/types/cli.d.ts.map +1 -1
  256. package/dist/types/cli.js +2 -11
  257. package/dist/types/cli.js.map +1 -1
  258. package/dist/types/core.cjs +6 -1
  259. package/dist/types/core.cjs.map +1 -1
  260. package/dist/types/core.d.cts +15 -2
  261. package/dist/types/core.d.cts.map +1 -1
  262. package/dist/types/core.d.ts +15 -2
  263. package/dist/types/core.d.ts.map +1 -1
  264. package/dist/types/core.js +2 -1
  265. package/dist/types/core.js.map +1 -1
  266. package/dist/types/index.cjs +5 -0
  267. package/dist/types/index.cjs.map +1 -1
  268. package/dist/types/index.d.cts +2 -0
  269. package/dist/types/index.d.cts.map +1 -1
  270. package/dist/types/index.d.ts +2 -0
  271. package/dist/types/index.d.ts.map +1 -1
  272. package/dist/types/index.js +2 -0
  273. package/dist/types/index.js.map +1 -1
  274. package/dist/types/interfaces.d.cts +19 -8
  275. package/dist/types/interfaces.d.cts.map +1 -1
  276. package/dist/types/interfaces.d.ts +19 -8
  277. package/dist/types/interfaces.d.ts.map +1 -1
  278. package/dist/types/profiler.cjs +11 -0
  279. package/dist/types/profiler.cjs.map +1 -0
  280. package/dist/types/profiler.d.cts +102 -0
  281. package/dist/types/profiler.d.cts.map +1 -0
  282. package/dist/types/profiler.d.ts +102 -0
  283. package/dist/types/profiler.d.ts.map +1 -0
  284. package/dist/types/profiler.js +10 -0
  285. package/dist/types/profiler.js.map +1 -0
  286. package/dist/types/utility.cjs.map +1 -1
  287. package/dist/types/utility.d.cts +0 -8
  288. package/dist/types/utility.d.cts.map +1 -1
  289. package/dist/types/utility.d.ts +0 -8
  290. package/dist/types/utility.d.ts.map +1 -1
  291. package/dist/types/utility.js.map +1 -1
  292. package/dist/utils/identifiers.cjs +32 -0
  293. package/dist/utils/identifiers.cjs.map +1 -0
  294. package/dist/utils/identifiers.d.cts +32 -0
  295. package/dist/utils/identifiers.d.cts.map +1 -0
  296. package/dist/utils/identifiers.d.ts +32 -0
  297. package/dist/utils/identifiers.d.ts.map +1 -0
  298. package/dist/utils/identifiers.js +27 -0
  299. package/dist/utils/identifiers.js.map +1 -0
  300. package/dist/utils/package.cjs +40 -0
  301. package/dist/utils/package.cjs.map +1 -0
  302. package/dist/utils/package.d.cts +15 -0
  303. package/dist/utils/package.d.cts.map +1 -0
  304. package/dist/utils/package.d.ts +15 -0
  305. package/dist/utils/package.d.ts.map +1 -0
  306. package/dist/utils/package.js +33 -0
  307. package/dist/utils/package.js.map +1 -0
  308. package/dist/utils/type-guards.cjs +48 -0
  309. package/dist/utils/type-guards.cjs.map +1 -0
  310. package/dist/utils/type-guards.d.cts +22 -0
  311. package/dist/utils/type-guards.d.cts.map +1 -0
  312. package/dist/utils/type-guards.d.ts +22 -0
  313. package/dist/utils/type-guards.d.ts.map +1 -0
  314. package/dist/utils/type-guards.js +43 -0
  315. package/dist/utils/type-guards.js.map +1 -0
  316. package/package.json +18 -19
  317. package/src/cli/commands/analyze.ts +101 -0
  318. package/src/cli/commands/baseline.ts +577 -0
  319. package/src/cli/commands/history.ts +1 -1
  320. package/src/cli/commands/init.ts +116 -194
  321. package/src/cli/commands/run.ts +183 -113
  322. package/src/cli/index.ts +425 -183
  323. package/src/config/budget-schema.ts +189 -0
  324. package/src/config/schema.ts +260 -1
  325. package/src/constants.ts +53 -1
  326. package/src/core/engine.ts +169 -22
  327. package/src/core/engines/accurate-engine.ts +195 -44
  328. package/src/core/engines/tinybench-engine.ts +3 -2
  329. package/src/core/output-path-resolver.ts +10 -2
  330. package/src/errors/base.ts +11 -2
  331. package/src/errors/budget.ts +38 -0
  332. package/src/errors/index.ts +3 -0
  333. package/src/index.ts +9 -0
  334. package/src/reporters/csv.ts +54 -25
  335. package/src/reporters/human.ts +434 -115
  336. package/src/reporters/json.ts +26 -71
  337. package/src/reporters/profile-human.ts +210 -0
  338. package/src/reporters/simple.ts +88 -54
  339. package/src/services/baseline-storage.ts +199 -0
  340. package/src/services/budget-evaluator.ts +182 -0
  341. package/src/services/config-manager.ts +24 -9
  342. package/src/services/file-loader.ts +3 -6
  343. package/src/services/profiler/profile-filter.ts +147 -0
  344. package/src/services/profiler/profile-parser.ts +194 -0
  345. package/src/services/profiler/profile-runner.ts +121 -0
  346. package/src/services/progress-manager.ts +12 -2
  347. package/src/services/reporter-registry.ts +46 -81
  348. package/src/types/budgets.ts +180 -0
  349. package/src/types/cli.ts +5 -238
  350. package/src/types/core.ts +52 -10
  351. package/src/types/index.ts +5 -0
  352. package/src/types/interfaces.ts +24 -6
  353. package/src/types/profiler.ts +135 -0
  354. package/src/types/utility.ts +0 -10
  355. package/src/utils/identifiers.ts +58 -0
  356. package/src/utils/package.ts +35 -0
  357. package/src/utils/type-guards.ts +51 -0
@@ -9,6 +9,7 @@ import path from 'node:path';
9
9
 
10
10
  import type {
11
11
  BenchmarkRun,
12
+ BudgetSummary,
12
13
  FileResult,
13
14
  ProgressState,
14
15
  SuiteResult,
@@ -26,6 +27,8 @@ export class HumanReporter extends BaseReporter {
26
27
 
27
28
  private currentSuite = '';
28
29
 
30
+ private currentSuiteMaxNameLen = 0; // Track max name length for current suite alignment
31
+
29
32
  private failures: Array<{
30
33
  error: string;
31
34
  file: string;
@@ -35,6 +38,8 @@ export class HumanReporter extends BaseReporter {
35
38
 
36
39
  private lastProgressLine = '';
37
40
 
41
+ private maxTimePadWidth = 0; // Track maximum time padding width to prevent jitter
42
+
38
43
  private progressWindowActive = false; // Track if progress window is rendered
39
44
 
40
45
  private readonly quiet: boolean;
@@ -71,6 +76,84 @@ export class HumanReporter extends BaseReporter {
71
76
  this.showProgress = options.progress ?? true;
72
77
  }
73
78
 
79
+ /**
80
+ * Format bytes in human-readable format
81
+ */
82
+ private static formatBytes(this: void, bytes: number): string {
83
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
84
+ let size = bytes;
85
+ let unitIndex = 0;
86
+
87
+ while (size >= 1024 && unitIndex < units.length - 1) {
88
+ size /= 1024;
89
+ unitIndex++;
90
+ }
91
+
92
+ return `${size.toFixed(1)} ${units[unitIndex]}`;
93
+ }
94
+
95
+ /**
96
+ * Format file path - show relative path if within CWD, otherwise absolute
97
+ */
98
+ private static formatPath(this: void, filePath: string): string {
99
+ const cwd = process.cwd();
100
+ const absolutePath = path.resolve(filePath);
101
+
102
+ // Check if the file is within the current working directory
103
+ if (absolutePath.startsWith(cwd + path.sep) || absolutePath === cwd) {
104
+ return path.relative(cwd, absolutePath);
105
+ }
106
+
107
+ return absolutePath;
108
+ }
109
+
110
+ /**
111
+ * Simple pluralization helper
112
+ */
113
+ private static pluralize(this: void, str: string, count: number): string {
114
+ return count === 1 ? str : `${str}s`;
115
+ }
116
+
117
+ onBudgetResult(summary: BudgetSummary): void {
118
+ if (summary.total === 0 || this.quiet) {
119
+ return;
120
+ }
121
+
122
+ this.clearProgress();
123
+
124
+ this.printLine();
125
+ const budgetHeader = `${this.colorize('magenta', ansiChars.block.full.repeat(2))} ${this.colorize('brightWhite', this.colorize('bold', 'Performance Budgets'))}`;
126
+ this.printLine(budgetHeader);
127
+ this.printLine();
128
+
129
+ for (const result of summary.results) {
130
+ const icon = result.passed ? ansiChars.checkmark : ansiChars.cross;
131
+ const iconColor = result.passed ? 'brightCyan' : 'brightRed';
132
+
133
+ this.printLine(
134
+ ` ${this.colorize(iconColor, icon)} ${this.colorize('white', result.taskId)}`,
135
+ );
136
+
137
+ if (!result.passed && result.violations.length > 0) {
138
+ for (const violation of result.violations) {
139
+ this.printLine(
140
+ ` ${this.colorize('brightRed', violation.message)}`,
141
+ );
142
+ }
143
+ }
144
+ }
145
+
146
+ this.printLine();
147
+
148
+ const statusText =
149
+ summary.failed === 0
150
+ ? `${this.colorize('brightCyan', ansiChars.checkmark)} All ${summary.total} budget(s) passed`
151
+ : `${this.colorize('brightRed', ansiChars.cross)} ${summary.failed} of ${summary.total} budget(s) failed`;
152
+
153
+ this.printLine(` ${statusText}`);
154
+ this.printLine();
155
+ }
156
+
74
157
  onEnd(run: BenchmarkRun): void {
75
158
  if (this.quiet) {
76
159
  return;
@@ -85,12 +168,16 @@ export class HumanReporter extends BaseReporter {
85
168
  let totalSuites = 0;
86
169
  let totalPassed = 0;
87
170
  let totalFailed = 0;
171
+ let totalAborted = 0;
88
172
 
89
173
  for (const file of run.files) {
90
174
  totalSuites += file.suites.length;
91
175
  for (const suite of file.suites) {
92
- totalPassed += suite.tasks.filter((t: TaskResult) => !t.error).length;
176
+ totalPassed += suite.tasks.filter(
177
+ (t: TaskResult) => !t.error && !t.aborted,
178
+ ).length;
93
179
  totalFailed += suite.tasks.filter((t: TaskResult) => t.error).length;
180
+ totalAborted += suite.tasks.filter((t: TaskResult) => t.aborted).length;
94
181
  }
95
182
  }
96
183
 
@@ -106,22 +193,31 @@ export class HumanReporter extends BaseReporter {
106
193
  `${this.colorize('brightBlue', ' Suites:')} ${this.colorize('brightWhite', String(totalSuites))}`,
107
194
  );
108
195
  this.printLine(
109
- `${this.colorize('brightBlue', ' Tasks:')} ${this.colorize('brightWhite', String(totalPassed + totalFailed))}`,
196
+ `${this.colorize('brightBlue', ' Tasks:')} ${this.colorize('brightWhite', String(totalPassed + totalFailed + totalAborted))}`,
110
197
  );
111
- if (totalFailed > 0) {
112
- this.printLine(
113
- `${this.colorize('brightRed', ansiChars.cross + ' Failed:')} ${this.colorize('brightWhite', String(totalFailed))}`,
114
- );
115
- this.printLine(
116
- `${this.colorize('brightCyan', ansiChars.checkmark + ' Passed:')} ${this.colorize('brightWhite', String(totalPassed))}`,
117
- );
198
+ if (totalFailed > 0 || totalAborted > 0) {
199
+ if (totalFailed > 0) {
200
+ this.printLine(
201
+ `${this.colorize('brightRed', ansiChars.cross + ' Failed:')} ${this.colorize('brightWhite', String(totalFailed))}`,
202
+ );
203
+ }
204
+ if (totalPassed > 0) {
205
+ this.printLine(
206
+ `${this.colorize('brightCyan', ansiChars.checkmark + ' Passed:')} ${this.colorize('brightWhite', String(totalPassed))}`,
207
+ );
208
+ }
209
+ if (totalAborted > 0) {
210
+ this.printLine(
211
+ `${this.colorize('brightYellow', ansiChars.approx + ' Aborted:')} ${this.colorize('brightWhite', String(totalAborted))}`,
212
+ );
213
+ }
118
214
  } else {
119
215
  this.printLine(
120
216
  `${this.colorize('brightCyan', ansiChars.checkmark + ' All tasks passed:')} ${this.colorize('brightWhite', String(totalPassed))}`,
121
217
  );
122
218
  }
123
219
  this.printLine(
124
- `${this.colorize('cyan', ansiChars.approx + ' Duration:')} ${this.colorize('brightWhite', this.formatDuration(duration * 1000000))}`,
220
+ `${this.colorize('cyan', ansiChars.approx + ' Duration:')} ${this.colorize('brightWhite', BaseReporter.formatDuration(duration * 1000000))}`,
125
221
  );
126
222
  this.printLine();
127
223
 
@@ -135,7 +231,7 @@ export class HumanReporter extends BaseReporter {
135
231
  this.printLine();
136
232
 
137
233
  for (const failure of this.failures) {
138
- const displayPath = this.formatPath(failure.file);
234
+ const displayPath = HumanReporter.formatPath(failure.file);
139
235
  this.printLine(
140
236
  ` ${this.colorize('dim', displayPath)} ${this.colorize('dim', '›')} ${this.colorize('white', failure.suite)} ${this.colorize('dim', '›')} ${this.colorize('brightWhite', failure.task)}`,
141
237
  );
@@ -143,7 +239,8 @@ export class HumanReporter extends BaseReporter {
143
239
  this.printLine();
144
240
  }
145
241
  }
146
- } else {
242
+ } else if (totalAborted === 0) {
243
+ // Only show "Rad" if no failures AND no aborts
147
244
  const successMessage = `${this.colorize('brightMagenta', 'Rad. ☮')}`;
148
245
  this.printLine(successMessage);
149
246
  }
@@ -189,7 +286,7 @@ export class HumanReporter extends BaseReporter {
189
286
  );
190
287
  } else {
191
288
  this.printLine(
192
- ` ${this.colorize('magenta', ansiChars.checkmark)} ${totalPassed > 1 ? this.colorize('brightMagenta', 'All ') : ''}${this.colorize('bold', this.colorize('brightMagenta', `${totalPassed}`))} ${this.colorize('brightMagenta', `${this.pluralize('task', totalPassed)} passed`)}`,
289
+ ` ${this.colorize('magenta', ansiChars.checkmark)} ${totalPassed > 1 ? this.colorize('brightMagenta', 'All ') : ''}${this.colorize('bold', this.colorize('brightMagenta', `${totalPassed}`))} ${this.colorize('brightMagenta', `${HumanReporter.pluralize('task', totalPassed)} passed`)}`,
193
290
  );
194
291
  }
195
292
 
@@ -203,7 +300,7 @@ export class HumanReporter extends BaseReporter {
203
300
  return;
204
301
  }
205
302
 
206
- const displayPath = this.formatPath(file);
303
+ const displayPath = HumanReporter.formatPath(file);
207
304
  const fileMarker = `${colors.magenta}${ansiChars.block.dark}${ansiChars.block.dark}${colors.reset}`;
208
305
  this.printLine(
209
306
  `${fileMarker} ${colors.underline}${this.colorize('brightMagenta', this.colorize('bold', displayPath))}${colors.reset}`,
@@ -221,7 +318,8 @@ export class HumanReporter extends BaseReporter {
221
318
  return;
222
319
  }
223
320
 
224
- const { elapsed, percentage, tasksCompleted, totalTasks } = state;
321
+ const { currentTask, elapsed, percentage, tasksCompleted, totalTasks } =
322
+ state;
225
323
 
226
324
  // Pad task counts for alignment
227
325
  const totalTasksWidth = String(totalTasks).length;
@@ -236,7 +334,7 @@ export class HumanReporter extends BaseReporter {
236
334
 
237
335
  // Calculate ETA if we have completed tasks and determine padding width
238
336
  let etaStr = '';
239
- let padWidth = elapsedStrRaw.length;
337
+ let padWidth = Math.max(this.maxTimePadWidth, elapsedStrRaw.length);
240
338
  if (tasksCompleted > 0) {
241
339
  const avgTimePerTask = elapsed / tasksCompleted;
242
340
  const remainingTasks = totalTasks - tasksCompleted;
@@ -244,14 +342,27 @@ export class HumanReporter extends BaseReporter {
244
342
  const etaSeconds = Math.round(etaMs / 1000);
245
343
  const etaTimeStr = this.formatTimeRemaining(etaSeconds);
246
344
  padWidth = Math.max(padWidth, etaTimeStr.length);
247
- etaStr = ` ${this.colorize('dim', '|')} ${this.colorize('dim', 'ETA:')} ${this.colorize('brightBlue', etaTimeStr)}`;
345
+ etaStr = ` ${this.colorize('gray', '|')} ${this.colorize('gray', 'ETA:')} ${this.colorize('brightBlue', etaTimeStr)}`;
248
346
  }
249
347
 
348
+ // Remember the maximum width we've ever used to prevent jitter
349
+ this.maxTimePadWidth = Math.max(this.maxTimePadWidth, padWidth);
350
+
250
351
  // Pad elapsed time to match the longest time string
251
- const elapsedStr = elapsedStrRaw.padStart(padWidth, ' ');
352
+ const elapsedStr = elapsedStrRaw.padStart(this.maxTimePadWidth, ' ');
252
353
 
253
354
  const roundedPercentage = percentage.toFixed(2);
254
- const line = `${this.colorize('brightCyan', ansiChars.approx)} ${this.colorize('white', paddedTasksCompleted)}${this.colorize('dim', '/')}${this.colorize('white', String(totalTasks))} ${this.colorize('dim', 'tasks')} ${this.colorize('dim', '(')}${this.colorize('brightBlue', roundedPercentage + '%')}${this.colorize('dim', ')')} ${this.colorize('dim', '|')} ${this.colorize('dim', 'Elapsed:')} ${this.colorize('cyan', elapsedStr)}${etaStr}`;
355
+
356
+ // Build progress line with current task if available
357
+ let line = `${this.colorize('brightCyan', ansiChars.approx)} ${this.colorize('white', paddedTasksCompleted)}${this.colorize('gray', '/')}${this.colorize('white', String(totalTasks))} ${this.colorize('gray', 'tasks')} ${this.colorize('gray', '(')}${this.colorize('brightBlue', roundedPercentage + '%')}${this.colorize('gray', ')')} ${this.colorize('gray', '|')} ${this.colorize('gray', 'Elapsed:')} ${this.colorize('cyan', elapsedStr)}${etaStr}`;
358
+
359
+ if (currentTask) {
360
+ const truncatedTask =
361
+ currentTask.length > 60
362
+ ? currentTask.substring(0, 57) + '...'
363
+ : currentTask;
364
+ line += ` ${this.colorize('gray', '|')} ${this.colorize('white', truncatedTask)}`;
365
+ }
255
366
 
256
367
  this.lastProgressLine = line;
257
368
  this.renderProgressWindow();
@@ -261,6 +372,7 @@ export class HumanReporter extends BaseReporter {
261
372
  this.startTime = Date.now();
262
373
  this.failures = []; // Reset failures for new run
263
374
  this.lastProgressLine = ''; // Reset for new run
375
+ this.maxTimePadWidth = 0; // Reset time padding width for new run
264
376
 
265
377
  if (this.quiet) {
266
378
  return;
@@ -278,7 +390,7 @@ export class HumanReporter extends BaseReporter {
278
390
  \x1b[48;5;0m \x1b[48;5;14m \x1b[38;5;30;48;5;38m▄\x1b[38;5;14;48;5;14m▄\x1b[48;5;14m \x1b[38;5;45;48;5;14m▄\x1b[38;5;89;48;5;14m▄\x1b[38;5;89;48;5;89m▄\x1b[38;5;14;48;5;31m▄\x1b[48;5;14m \x1b[38;5;37;48;5;89m▄\x1b[48;5;198m \x1b[38;5;198;48;5;198m▄\x1b[38;5;31;48;5;14m▄\x1b[48;5;14m \x1b[48;5;0m \x1b[m \x1b[2mnode.js:\x1b[m \x1b[36m${run.environment.nodeVersion} \x1b[m
279
391
  \x1b[48;5;0m \x1b[48;5;14m \x1b[38;5;44;48;5;31m▄\x1b[48;5;14m \x1b[38;5;126;48;5;38m▄\x1b[38;5;198;48;5;237m▄\x1b[38;5;237;48;5;37m▄\x1b[48;5;14m \x1b[38;5;14;48;5;14m▄\x1b[38;5;162;48;5;198m▄▄\x1b[38;5;53;48;5;240m▄\x1b[48;5;14m \x1b[48;5;0m \x1b[m \x1b[2mplatform:\x1b[m \x1b[36m${run.environment.platform} ${run.environment.arch} \x1b[m
280
392
  \x1b[48;5;0m \x1b[38;5;45;48;5;14m▄\x1b[48;5;14m \x1b[38;5;14;48;5;37m▄\x1b[38;5;14;48;5;5m▄\x1b[38;5;14;48;5;44m▄\x1b[48;5;14m \x1b[38;5;45;48;5;14m▄\x1b[48;5;0m \x1b[m \x1b[2mcpu:\x1b[m \x1b[36m${run.environment.cpu.model} \x1b[2m(\x1b[m\x1b[36m${run.environment.cpu.cores} cores\x1b[2m) \x1b[m
281
- \x1b[49;38;5;0m▀▀\x1b[38;5;0;48;5;6m▄\x1b[38;5;232;48;5;14m▄\x1b[38;5;38;48;5;14m▄\x1b[48;5;14m \x1b[38;5;30;48;5;14m▄\x1b[38;5;0;48;5;14m▄\x1b[38;5;0;48;5;23m▄\x1b[49;38;5;0m▀▀\x1b[m \x1b[2mmem:\x1b[m \x1b[36m${this.formatBytes(run.environment.memory.total)} \x1b[m
393
+ \x1b[49;38;5;0m▀▀\x1b[38;5;0;48;5;6m▄\x1b[38;5;232;48;5;14m▄\x1b[38;5;38;48;5;14m▄\x1b[48;5;14m \x1b[38;5;30;48;5;14m▄\x1b[38;5;0;48;5;14m▄\x1b[38;5;0;48;5;23m▄\x1b[49;38;5;0m▀▀\x1b[m \x1b[2mmem:\x1b[m \x1b[36m${HumanReporter.formatBytes(run.environment.memory.total)} \x1b[m
282
394
  \x1b[49m \x1b[49;38;5;0m▀\x1b[38;5;0;48;5;236m▄\x1b[38;5;0;48;5;45m▄\x1b[38;5;23;48;5;14m▄\x1b[48;5;14m \x1b[38;5;236;48;5;14m▄\x1b[38;5;0;48;5;44m▄\x1b[38;5;0;48;5;232m▄\x1b[49;38;5;0m▀\x1b[49m \x1b[m
283
395
  \x1b[49m \x1b[49;38;5;0m▀▀\x1b[38;5;0;48;5;37m▄\x1b[38;5;0;48;5;14m▄\x1b[38;5;0;48;5;30m▄\x1b[49;38;5;0m▀▀\x1b[49m \x1b[m
284
396
  `;
@@ -316,29 +428,70 @@ export class HumanReporter extends BaseReporter {
316
428
  return;
317
429
  }
318
430
 
319
- // Print all buffered task results with aligned columns
320
- this.printAlignedSuiteResults();
431
+ // Tasks are printed immediately in onTaskResult, so just print suite summary
321
432
 
322
433
  // Skip displaying summary for the implicit "default" suite
323
434
  if (result.name === 'default') {
324
435
  return;
325
436
  }
326
437
 
327
- const passed = result.tasks.filter((t) => !t.error).length;
438
+ const passed = result.tasks.filter((t) => !t.error && !t.aborted).length;
328
439
  const failed = result.tasks.filter((t) => t.error).length;
440
+ const aborted = result.tasks.filter((t) => t.aborted).length;
441
+ const durationStr = BaseReporter.formatDuration(result.duration * 1000000); // ms to ns
442
+
443
+ // Build summary parts
444
+ const parts: string[] = [];
329
445
 
330
446
  if (failed > 0) {
331
- this.printLine(
332
- ` ${this.colorize('red', `${ansiChars.cross} ${failed} failed`)}, ${this.colorize('green', `${passed} passed`)}`,
333
- );
447
+ parts.push(this.colorize('red', `${ansiChars.cross} ${failed} failed`));
448
+ }
449
+ if (passed > 0) {
450
+ parts.push(this.colorize('green', `${passed} passed`));
451
+ }
452
+ if (aborted > 0) {
453
+ parts.push(this.colorize('brightYellow', `${aborted} aborted`));
454
+ }
455
+
456
+ const summary = parts.join(', ');
457
+ const timeInfo = `${this.colorize('gray', 'in')} ${this.colorize('cyan', durationStr)}`;
458
+
459
+ if (failed > 0 || aborted > 0) {
460
+ this.printLine(` ${summary} ${timeInfo}`);
334
461
  } else {
335
462
  this.printLine(
336
- ` ${this.colorize('magenta', ansiChars.checkmark)} ${this.colorize('bold', this.colorize('brightWhite', `${passed}`))} ${this.colorize('brightWhite', `${this.pluralize('task', passed)} passed`)}`,
463
+ ` ${this.colorize('magenta', ansiChars.checkmark)} ${this.colorize('bold', this.colorize('brightWhite', `${passed}`))} ${this.colorize('brightWhite', `${HumanReporter.pluralize('task', passed)} passed`)} ${timeInfo}`,
337
464
  );
338
465
  }
339
466
  this.printLine();
340
467
  }
341
468
 
469
+ onSuiteInit(suite: string, taskNames: readonly string[]): void {
470
+ // Pre-calculate max name length for optimal alignment
471
+ const terminalWidth = process.stdout.columns || 80;
472
+ const STATS_RESERVED_WIDTH = 70;
473
+ const MAX_NAME_WIDTH = Math.max(
474
+ 40,
475
+ Math.min(
476
+ 60,
477
+ terminalWidth - 4 - 2 - 2 - STATS_RESERVED_WIDTH, // BASE_INDENT(4) + status(1) + space(1) + ": "(2)
478
+ ),
479
+ );
480
+
481
+ // Calculate the actual max name length from non-wrapped names
482
+ let maxLen = 0;
483
+ for (const name of taskNames) {
484
+ const nameLen = this.getVisibleLength(name.trim());
485
+ // Only count names that won't wrap
486
+ if (nameLen <= MAX_NAME_WIDTH) {
487
+ maxLen = Math.max(maxLen, nameLen);
488
+ }
489
+ }
490
+
491
+ // Use the max of actual names or MAX_NAME_WIDTH for consistency
492
+ this.currentSuiteMaxNameLen = Math.max(maxLen, MAX_NAME_WIDTH);
493
+ }
494
+
342
495
  onSuiteStart(suite: string): void {
343
496
  this.currentSuite = suite;
344
497
 
@@ -365,8 +518,16 @@ export class HumanReporter extends BaseReporter {
365
518
  return;
366
519
  }
367
520
 
368
- // Buffer the result for later printing with proper alignment
521
+ // Always buffer the result for suite summary (including aborted tasks)
369
522
  this.suiteResults.push(result);
523
+
524
+ // Skip printing aborted tasks (they're counted in summary but not shown individually)
525
+ if (result.aborted) {
526
+ return;
527
+ }
528
+
529
+ // Print immediately with current alignment
530
+ this.printTaskResult(result);
370
531
  }
371
532
 
372
533
  onTaskStart(task: string): void {
@@ -414,37 +575,6 @@ export class HumanReporter extends BaseReporter {
414
575
  return `${colors[color]}${text}${colors.reset}`;
415
576
  }
416
577
 
417
- /**
418
- * Format bytes in human-readable format
419
- */
420
- private formatBytes(bytes: number): string {
421
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
422
- let size = bytes;
423
- let unitIndex = 0;
424
-
425
- while (size >= 1024 && unitIndex < units.length - 1) {
426
- size /= 1024;
427
- unitIndex++;
428
- }
429
-
430
- return `${size.toFixed(1)} ${units[unitIndex]}`;
431
- }
432
-
433
- /**
434
- * Format file path - show relative path if within CWD, otherwise absolute
435
- */
436
- private formatPath(filePath: string): string {
437
- const cwd = process.cwd();
438
- const absolutePath = path.resolve(filePath);
439
-
440
- // Check if the file is within the current working directory
441
- if (absolutePath.startsWith(cwd + path.sep) || absolutePath === cwd) {
442
- return path.relative(cwd, absolutePath);
443
- }
444
-
445
- return absolutePath;
446
- }
447
-
448
578
  /**
449
579
  * Format duration in human-readable format for progress display
450
580
  */
@@ -471,13 +601,6 @@ export class HumanReporter extends BaseReporter {
471
601
  return str.replace(/\x1b\[[0-9;]*m/g, '').length;
472
602
  }
473
603
 
474
- /**
475
- * Simple pluralization helper
476
- */
477
- private pluralize(str: string, count: number): string {
478
- return count === 1 ? str : `${str}s`;
479
- }
480
-
481
604
  /**
482
605
  * Print all task results in a suite with aligned columns
483
606
  */
@@ -486,13 +609,24 @@ export class HumanReporter extends BaseReporter {
486
609
  return;
487
610
  }
488
611
 
489
- const MAX_NAME_WIDTH = 60;
490
612
  const BASE_INDENT = ' '; // 4 spaces
491
613
  const bullet = this.colorize(
492
614
  'dim',
493
615
  this.colorize('gray', ansiChars.bullet),
494
616
  );
495
617
 
618
+ // Calculate maximum name width based on terminal width
619
+ // Reserve space for: indent (4) + status (1) + space (1) + name + ": " (2) + stats (~60 chars)
620
+ const terminalWidth = process.stdout.columns || 80;
621
+ const STATS_RESERVED_WIDTH = 70; // Approx space for duration + rme + ops/sec with padding
622
+ const MAX_NAME_WIDTH = Math.max(
623
+ 40,
624
+ Math.min(
625
+ 60,
626
+ terminalWidth - BASE_INDENT.length - 2 - 2 - STATS_RESERVED_WIDTH,
627
+ ),
628
+ );
629
+
496
630
  // Prepare formatted data for each task
497
631
  interface FormattedTask {
498
632
  durationLen: number;
@@ -509,49 +643,52 @@ export class HumanReporter extends BaseReporter {
509
643
  status: string;
510
644
  }
511
645
 
512
- const formatted: FormattedTask[] = this.suiteResults.map((result) => {
513
- const status = result.error
514
- ? this.colorize('red', ansiChars.cross)
515
- : this.colorize('brightCyan', ansiChars.checkmark);
646
+ // Filter out aborted tasks (they're counted in suite summary but not printed)
647
+ const formatted: FormattedTask[] = this.suiteResults
648
+ .filter((result) => !result.aborted)
649
+ .map((result) => {
650
+ const status = result.error
651
+ ? this.colorize('red', ansiChars.cross)
652
+ : this.colorize('brightCyan', ansiChars.checkmark);
653
+
654
+ const name = result.name.trim();
655
+ const nameLength = this.getVisibleLength(name);
656
+
657
+ if (result.error) {
658
+ return {
659
+ durationLen: 0,
660
+ durationStr: '',
661
+ error: true,
662
+ errorMessage: result.error?.message || String(result.error),
663
+ iterations: 0,
664
+ name,
665
+ nameLength,
666
+ opsPerSecLen: 0,
667
+ opsPerSecStr: '',
668
+ rmeLen: 0,
669
+ rmeStr: '',
670
+ status,
671
+ };
672
+ }
516
673
 
517
- const name = result.name.trim();
518
- const nameLength = this.getVisibleLength(name);
674
+ const duration = BaseReporter.formatDuration(result.mean); // already in nanoseconds
675
+ const opsPerSec = BaseReporter.formatOpsPerSecond(result.opsPerSecond);
676
+ const rme = BaseReporter.formatPercentage(result.marginOfError); // already a percentage
519
677
 
520
- if (result.error) {
521
678
  return {
522
- durationLen: 0,
523
- durationStr: '',
524
- error: true,
525
- errorMessage: result.error?.message || String(result.error),
526
- iterations: 0,
679
+ durationLen: this.getVisibleLength(duration),
680
+ durationStr: duration,
681
+ error: false,
682
+ iterations: result.iterations,
527
683
  name,
528
684
  nameLength,
529
- opsPerSecLen: 0,
530
- opsPerSecStr: '',
531
- rmeLen: 0,
532
- rmeStr: '',
685
+ opsPerSecLen: this.getVisibleLength(opsPerSec),
686
+ opsPerSecStr: opsPerSec,
687
+ rmeLen: this.getVisibleLength(rme),
688
+ rmeStr: rme,
533
689
  status,
534
690
  };
535
- }
536
-
537
- const duration = this.formatDuration(result.mean); // already in nanoseconds
538
- const opsPerSec = this.formatOpsPerSecond(result.opsPerSecond);
539
- const rme = this.formatPercentage(result.marginOfError * 100);
540
-
541
- return {
542
- durationLen: this.getVisibleLength(duration),
543
- durationStr: duration,
544
- error: false,
545
- iterations: result.iterations,
546
- name,
547
- nameLength,
548
- opsPerSecLen: this.getVisibleLength(opsPerSec),
549
- opsPerSecStr: opsPerSec,
550
- rmeLen: this.getVisibleLength(rme),
551
- rmeStr: rme,
552
- status,
553
- };
554
- });
691
+ });
555
692
 
556
693
  // Find max widths
557
694
  const nonWrappingTasks = formatted.filter(
@@ -575,10 +712,6 @@ export class HumanReporter extends BaseReporter {
575
712
  0,
576
713
  );
577
714
 
578
- // Calculate the position where numbers start for unwrapped lines
579
- // BASE_INDENT (4) + status (1 char) + space (1) + maxNameLen + ": " (2) = 8 + maxNameLen
580
- const numbersStartPos = BASE_INDENT.length + 2 + maxNameLen + 2;
581
-
582
715
  // Print each task with aligned columns
583
716
  for (const task of formatted) {
584
717
  if (task.error) {
@@ -594,22 +727,45 @@ export class HumanReporter extends BaseReporter {
594
727
  `${BASE_INDENT}${task.status} ${this.colorize('white', task.name)} ${this.colorize('red', 'FAILED')}`,
595
728
  );
596
729
  } else if (task.nameLength > MAX_NAME_WIDTH) {
597
- // Long name - wrap to next line, but align numbers with unwrapped lines
598
- this.printLine(
599
- `${BASE_INDENT}${task.status} ${this.colorize('white', task.name)}:`,
600
- );
730
+ // Long name - wrap to multiple lines, align last line with short names
731
+ const wrappedLines = this.wrapText(task.name, MAX_NAME_WIDTH);
732
+ const continueIndent = BASE_INDENT + ' '; // 6 spaces for continuation lines
601
733
 
602
- // Calculate padding to align with unwrapped lines
603
- // We need to get to numbersStartPos from the beginning of the line
604
- const leadingPad = ' '.repeat(numbersStartPos);
734
+ // Format stats string
605
735
  const durationPad = ' '.repeat(maxDurationLen - task.durationLen);
606
736
  const rmePad = ' '.repeat(maxRmeLen - task.rmeLen);
607
737
  const opsPad = ' '.repeat(maxOpsLen - task.opsPerSecLen);
738
+ const statsStr = `${durationPad}${this.colorize('cyan', task.durationStr)} ${bullet} ${ansiChars.plusMinus}${rmePad}${this.colorize('brightBlue', task.rmeStr)} ${bullet} ${opsPad}${this.colorize('magenta', task.opsPerSecStr)}`;
608
739
 
740
+ // Print first line with status
609
741
  this.printLine(
610
- `${leadingPad}${durationPad}${this.colorize('cyan', task.durationStr)} ${bullet} ${ansiChars.plusMinus}${rmePad}${this.colorize('brightBlue', task.rmeStr)} ${bullet} ${opsPad}${this.colorize('magenta', task.opsPerSecStr)}`,
742
+ `${BASE_INDENT}${task.status} ${this.colorize('white', wrappedLines[0]!)}`,
611
743
  );
612
744
 
745
+ // Print middle continuation lines (all but first and last)
746
+ for (let i = 1; i < wrappedLines.length - 1; i++) {
747
+ this.printLine(
748
+ `${continueIndent}${this.colorize('white', wrappedLines[i]!)}`,
749
+ );
750
+ }
751
+
752
+ // Print last line with colon and stats aligned with short names
753
+ if (wrappedLines.length > 1) {
754
+ const lastLine = wrappedLines[wrappedLines.length - 1]!;
755
+ const lastLineLen = this.getVisibleLength(lastLine);
756
+ // Pad the last line to align the ':' with short names
757
+ const lastLinePad = ' '.repeat(Math.max(0, maxNameLen - lastLineLen));
758
+ this.printLine(
759
+ `${continueIndent}${this.colorize('white', lastLine)}${lastLinePad}: ${statsStr}`,
760
+ );
761
+ } else {
762
+ // Single wrapped line
763
+ const lastLinePad = ' '.repeat(maxNameLen - task.nameLength);
764
+ this.printLine(
765
+ `${BASE_INDENT}${task.status} ${this.colorize('white', task.name)}${lastLinePad}: ${statsStr}`,
766
+ );
767
+ }
768
+
613
769
  if (this.verbose && task.iterations > 0) {
614
770
  this.printLine(
615
771
  ` ${this.colorize('dim', `${task.iterations} iterations`)}`,
@@ -646,6 +802,128 @@ export class HumanReporter extends BaseReporter {
646
802
  this.renderProgressWindow();
647
803
  }
648
804
 
805
+ /**
806
+ * Print a single task result immediately with current alignment
807
+ */
808
+ private printTaskResult(result: TaskResult): void {
809
+ // Clear progress bar temporarily
810
+ this.clearProgress();
811
+
812
+ const BASE_INDENT = ' '; // 4 spaces
813
+ const bullet = this.colorize(
814
+ 'dim',
815
+ this.colorize('gray', ansiChars.bullet),
816
+ );
817
+
818
+ // Calculate terminal width constraints
819
+ const terminalWidth = process.stdout.columns || 80;
820
+ const STATS_RESERVED_WIDTH = 70;
821
+ const MAX_NAME_WIDTH = Math.max(
822
+ 40,
823
+ Math.min(
824
+ 60,
825
+ terminalWidth - BASE_INDENT.length - 2 - 2 - STATS_RESERVED_WIDTH,
826
+ ),
827
+ );
828
+
829
+ // Status marker
830
+ const status = result.error
831
+ ? this.colorize('red', ansiChars.cross)
832
+ : this.colorize('brightCyan', ansiChars.checkmark);
833
+
834
+ const name = result.name.trim();
835
+ const nameLength = this.getVisibleLength(name);
836
+
837
+ // Handle errors
838
+ if (result.error) {
839
+ this.failures.push({
840
+ error: result.error?.message || String(result.error),
841
+ file: this.currentFile,
842
+ suite: this.currentSuite,
843
+ task: name,
844
+ });
845
+
846
+ this.printLine(
847
+ `${BASE_INDENT}${status} ${this.colorize('white', name)} ${this.colorize('red', 'FAILED')}`,
848
+ );
849
+ return;
850
+ }
851
+
852
+ // Format stats
853
+ const duration = BaseReporter.formatDuration(result.mean);
854
+ const opsPerSec = BaseReporter.formatOpsPerSecond(result.opsPerSecond);
855
+ const rme = BaseReporter.formatPercentage(result.marginOfError);
856
+
857
+ // Use fixed widths for stats columns (reasonable maximums)
858
+ const DURATION_WIDTH = 10; // "999.99ms" max
859
+ const RME_WIDTH = 8; // "±999.99%" max
860
+ const OPS_WIDTH = 15; // "999.99K ops/sec" max
861
+
862
+ const durationLen = this.getVisibleLength(duration);
863
+ const rmeLen = this.getVisibleLength(rme);
864
+ const opsLen = this.getVisibleLength(opsPerSec);
865
+
866
+ // Stats formatting with fixed widths
867
+ const durationPad = ' '.repeat(DURATION_WIDTH - durationLen);
868
+ const rmePad = ' '.repeat(RME_WIDTH - rmeLen);
869
+ const opsPad = ' '.repeat(OPS_WIDTH - opsLen);
870
+ const statsStr = `${durationPad}${this.colorize('cyan', duration)} ${bullet} ${ansiChars.plusMinus}${rmePad}${this.colorize('brightBlue', rme)} ${bullet} ${opsPad}${this.colorize('magenta', opsPerSec)}`;
871
+
872
+ // Handle long names (wrap)
873
+ if (nameLength > MAX_NAME_WIDTH) {
874
+ const wrappedLines = this.wrapText(name, MAX_NAME_WIDTH);
875
+ const continueIndent = BASE_INDENT + ' '; // 6 spaces for continuation lines
876
+
877
+ // Print first line with status
878
+ this.printLine(
879
+ `${BASE_INDENT}${status} ${this.colorize('white', wrappedLines[0]!)}`,
880
+ );
881
+
882
+ // Print middle lines (all but first and last)
883
+ for (let i = 1; i < wrappedLines.length - 1; i++) {
884
+ this.printLine(
885
+ `${continueIndent}${this.colorize('white', wrappedLines[i]!)}`,
886
+ );
887
+ }
888
+
889
+ // Print last line with colon and stats aligned
890
+ // Use pre-calculated currentSuiteMaxNameLen for perfect alignment
891
+ if (wrappedLines.length > 1) {
892
+ const lastLine = wrappedLines[wrappedLines.length - 1]!;
893
+ const lastLineLen = this.getVisibleLength(lastLine);
894
+ const lastLinePad = ' '.repeat(
895
+ Math.max(0, this.currentSuiteMaxNameLen - lastLineLen),
896
+ );
897
+ this.printLine(
898
+ `${continueIndent}${this.colorize('white', lastLine)}${lastLinePad}: ${statsStr}`,
899
+ );
900
+ } else {
901
+ // Single wrapped line (shouldn't happen if nameLength > MAX but handle it)
902
+ const lastLinePad = ' '.repeat(
903
+ Math.max(0, this.currentSuiteMaxNameLen - nameLength),
904
+ );
905
+ this.printLine(
906
+ `${BASE_INDENT}${status} ${this.colorize('white', name)}${lastLinePad}: ${statsStr}`,
907
+ );
908
+ }
909
+ } else {
910
+ // Normal length - print on same line with pre-calculated alignment
911
+ const namePad = ' '.repeat(
912
+ Math.max(0, this.currentSuiteMaxNameLen - nameLength),
913
+ );
914
+
915
+ this.printLine(
916
+ `${BASE_INDENT}${status} ${this.colorize('white', name)}${namePad}: ${statsStr}`,
917
+ );
918
+ }
919
+
920
+ if (this.verbose && result.iterations > 0) {
921
+ this.printLine(
922
+ ` ${this.colorize('dim', `${result.iterations} iterations`)}`,
923
+ );
924
+ }
925
+ }
926
+
649
927
  /**
650
928
  * Render the progress window at the bottom
651
929
  */
@@ -664,4 +942,45 @@ export class HumanReporter extends BaseReporter {
664
942
  console.log(this.lastProgressLine);
665
943
  this.progressWindowActive = true;
666
944
  }
945
+
946
+ /**
947
+ * Wrap text to a maximum width, breaking at word boundaries when possible
948
+ */
949
+ private wrapText(text: string, maxWidth: number): string[] {
950
+ if (this.getVisibleLength(text) <= maxWidth) {
951
+ return [text];
952
+ }
953
+
954
+ const lines: string[] = [];
955
+ let currentLine = '';
956
+
957
+ const words = text.split(/(\s+)/); // Keep whitespace in split
958
+
959
+ for (const word of words) {
960
+ const testLine = currentLine + word;
961
+ if (this.getVisibleLength(testLine) <= maxWidth) {
962
+ currentLine = testLine;
963
+ } else {
964
+ // If current line has content, save it
965
+ if (currentLine.trim()) {
966
+ lines.push(currentLine.trimEnd());
967
+ currentLine = word.trim() + ' ';
968
+ } else {
969
+ // Single word is too long, force break it
970
+ if (this.getVisibleLength(word) > maxWidth) {
971
+ lines.push(word.substring(0, maxWidth));
972
+ currentLine = word.substring(maxWidth);
973
+ } else {
974
+ currentLine = word;
975
+ }
976
+ }
977
+ }
978
+ }
979
+
980
+ if (currentLine.trim()) {
981
+ lines.push(currentLine.trimEnd());
982
+ }
983
+
984
+ return lines;
985
+ }
667
986
  }