modestbench 0.3.2 → 0.5.0

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 (158) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +45 -2
  3. package/dist/adapters/ava-adapter.cjs +421 -0
  4. package/dist/adapters/ava-adapter.cjs.map +1 -0
  5. package/dist/adapters/ava-adapter.d.cts +39 -0
  6. package/dist/adapters/ava-adapter.d.cts.map +1 -0
  7. package/dist/adapters/ava-adapter.d.ts +39 -0
  8. package/dist/adapters/ava-adapter.d.ts.map +1 -0
  9. package/dist/adapters/ava-adapter.js +384 -0
  10. package/dist/adapters/ava-adapter.js.map +1 -0
  11. package/dist/adapters/ava-hooks.cjs +66 -0
  12. package/dist/adapters/ava-hooks.cjs.map +1 -0
  13. package/dist/adapters/ava-hooks.d.cts +24 -0
  14. package/dist/adapters/ava-hooks.d.cts.map +1 -0
  15. package/dist/adapters/ava-hooks.d.ts +24 -0
  16. package/dist/adapters/ava-hooks.d.ts.map +1 -0
  17. package/dist/adapters/ava-hooks.js +61 -0
  18. package/dist/adapters/ava-hooks.js.map +1 -0
  19. package/dist/adapters/ava-register.cjs +16 -0
  20. package/dist/adapters/ava-register.cjs.map +1 -0
  21. package/dist/adapters/ava-register.d.cts +11 -0
  22. package/dist/adapters/ava-register.d.cts.map +1 -0
  23. package/dist/adapters/ava-register.d.ts +11 -0
  24. package/dist/adapters/ava-register.d.ts.map +1 -0
  25. package/dist/adapters/ava-register.js +14 -0
  26. package/dist/adapters/ava-register.js.map +1 -0
  27. package/dist/adapters/mocha-adapter.cjs +254 -0
  28. package/dist/adapters/mocha-adapter.cjs.map +1 -0
  29. package/dist/adapters/mocha-adapter.d.cts +26 -0
  30. package/dist/adapters/mocha-adapter.d.cts.map +1 -0
  31. package/dist/adapters/mocha-adapter.d.ts +26 -0
  32. package/dist/adapters/mocha-adapter.d.ts.map +1 -0
  33. package/dist/adapters/mocha-adapter.js +217 -0
  34. package/dist/adapters/mocha-adapter.js.map +1 -0
  35. package/dist/adapters/node-test-adapter.cjs +335 -0
  36. package/dist/adapters/node-test-adapter.cjs.map +1 -0
  37. package/dist/adapters/node-test-adapter.d.cts +41 -0
  38. package/dist/adapters/node-test-adapter.d.cts.map +1 -0
  39. package/dist/adapters/node-test-adapter.d.ts +41 -0
  40. package/dist/adapters/node-test-adapter.d.ts.map +1 -0
  41. package/dist/adapters/node-test-adapter.js +298 -0
  42. package/dist/adapters/node-test-adapter.js.map +1 -0
  43. package/dist/adapters/node-test-hooks.cjs +72 -0
  44. package/dist/adapters/node-test-hooks.cjs.map +1 -0
  45. package/dist/adapters/node-test-hooks.d.cts +24 -0
  46. package/dist/adapters/node-test-hooks.d.cts.map +1 -0
  47. package/dist/adapters/node-test-hooks.d.ts +24 -0
  48. package/dist/adapters/node-test-hooks.d.ts.map +1 -0
  49. package/dist/adapters/node-test-hooks.js +67 -0
  50. package/dist/adapters/node-test-hooks.js.map +1 -0
  51. package/dist/adapters/node-test-register.cjs +7 -0
  52. package/dist/adapters/node-test-register.cjs.map +1 -0
  53. package/dist/adapters/node-test-register.d.cts +2 -0
  54. package/dist/adapters/node-test-register.d.cts.map +1 -0
  55. package/dist/adapters/node-test-register.d.ts +2 -0
  56. package/dist/adapters/node-test-register.d.ts.map +1 -0
  57. package/dist/adapters/node-test-register.js +5 -0
  58. package/dist/adapters/node-test-register.js.map +1 -0
  59. package/dist/adapters/types.cjs +152 -0
  60. package/dist/adapters/types.cjs.map +1 -0
  61. package/dist/adapters/types.d.cts +112 -0
  62. package/dist/adapters/types.d.cts.map +1 -0
  63. package/dist/adapters/types.d.ts +112 -0
  64. package/dist/adapters/types.d.ts.map +1 -0
  65. package/dist/adapters/types.js +148 -0
  66. package/dist/adapters/types.js.map +1 -0
  67. package/dist/cli/commands/init.cjs +21 -17
  68. package/dist/cli/commands/init.cjs.map +1 -1
  69. package/dist/cli/commands/init.d.cts.map +1 -1
  70. package/dist/cli/commands/init.d.ts.map +1 -1
  71. package/dist/cli/commands/init.js +21 -17
  72. package/dist/cli/commands/init.js.map +1 -1
  73. package/dist/cli/commands/run.cjs +6 -2
  74. package/dist/cli/commands/run.cjs.map +1 -1
  75. package/dist/cli/commands/run.js +6 -2
  76. package/dist/cli/commands/run.js.map +1 -1
  77. package/dist/cli/commands/test.cjs +392 -0
  78. package/dist/cli/commands/test.cjs.map +1 -0
  79. package/dist/cli/commands/test.d.cts +38 -0
  80. package/dist/cli/commands/test.d.cts.map +1 -0
  81. package/dist/cli/commands/test.d.ts +38 -0
  82. package/dist/cli/commands/test.d.ts.map +1 -0
  83. package/dist/cli/commands/test.js +388 -0
  84. package/dist/cli/commands/test.js.map +1 -0
  85. package/dist/cli/index.cjs +72 -1
  86. package/dist/cli/index.cjs.map +1 -1
  87. package/dist/cli/index.d.cts.map +1 -1
  88. package/dist/cli/index.d.ts.map +1 -1
  89. package/dist/cli/index.js +73 -2
  90. package/dist/cli/index.js.map +1 -1
  91. package/dist/constants.cjs +13 -1
  92. package/dist/constants.cjs.map +1 -1
  93. package/dist/constants.d.cts +12 -0
  94. package/dist/constants.d.cts.map +1 -1
  95. package/dist/constants.d.ts +12 -0
  96. package/dist/constants.d.ts.map +1 -1
  97. package/dist/constants.js +12 -0
  98. package/dist/constants.js.map +1 -1
  99. package/dist/core/engine.cjs +4 -0
  100. package/dist/core/engine.cjs.map +1 -1
  101. package/dist/core/engine.d.cts.map +1 -1
  102. package/dist/core/engine.d.ts.map +1 -1
  103. package/dist/core/engine.js +4 -0
  104. package/dist/core/engine.js.map +1 -1
  105. package/dist/core/engines/tinybench-engine.cjs +163 -131
  106. package/dist/core/engines/tinybench-engine.cjs.map +1 -1
  107. package/dist/core/engines/tinybench-engine.d.cts +6 -0
  108. package/dist/core/engines/tinybench-engine.d.cts.map +1 -1
  109. package/dist/core/engines/tinybench-engine.d.ts +6 -0
  110. package/dist/core/engines/tinybench-engine.d.ts.map +1 -1
  111. package/dist/core/engines/tinybench-engine.js +163 -131
  112. package/dist/core/engines/tinybench-engine.js.map +1 -1
  113. package/dist/errors/base.cjs +2 -1
  114. package/dist/errors/base.cjs.map +1 -1
  115. package/dist/errors/base.d.cts.map +1 -1
  116. package/dist/errors/base.d.ts.map +1 -1
  117. package/dist/errors/base.js +2 -1
  118. package/dist/errors/base.js.map +1 -1
  119. package/dist/reporters/human.cjs +83 -27
  120. package/dist/reporters/human.cjs.map +1 -1
  121. package/dist/reporters/human.d.cts +1 -0
  122. package/dist/reporters/human.d.cts.map +1 -1
  123. package/dist/reporters/human.d.ts +1 -0
  124. package/dist/reporters/human.d.ts.map +1 -1
  125. package/dist/reporters/human.js +83 -27
  126. package/dist/reporters/human.js.map +1 -1
  127. package/dist/reporters/simple.cjs +68 -21
  128. package/dist/reporters/simple.cjs.map +1 -1
  129. package/dist/reporters/simple.d.cts +1 -0
  130. package/dist/reporters/simple.d.cts.map +1 -1
  131. package/dist/reporters/simple.d.ts +1 -0
  132. package/dist/reporters/simple.d.ts.map +1 -1
  133. package/dist/reporters/simple.js +68 -21
  134. package/dist/reporters/simple.js.map +1 -1
  135. package/dist/services/config-manager.cjs +1 -1
  136. package/dist/services/config-manager.cjs.map +1 -1
  137. package/dist/services/config-manager.js +2 -2
  138. package/dist/services/config-manager.js.map +1 -1
  139. package/package.json +60 -31
  140. package/src/adapters/ava-adapter.ts +553 -0
  141. package/src/adapters/ava-hooks.ts +65 -0
  142. package/src/adapters/ava-register.ts +15 -0
  143. package/src/adapters/mocha-adapter.ts +284 -0
  144. package/src/adapters/node-test-adapter.ts +391 -0
  145. package/src/adapters/node-test-hooks.ts +71 -0
  146. package/src/adapters/node-test-register.ts +5 -0
  147. package/src/adapters/types.ts +281 -0
  148. package/src/cli/commands/init.ts +25 -17
  149. package/src/cli/commands/run.ts +9 -2
  150. package/src/cli/commands/test.ts +546 -0
  151. package/src/cli/index.ts +81 -1
  152. package/src/constants.ts +15 -0
  153. package/src/core/engine.ts +5 -0
  154. package/src/core/engines/tinybench-engine.ts +213 -141
  155. package/src/errors/base.ts +3 -2
  156. package/src/reporters/human.ts +107 -36
  157. package/src/reporters/simple.ts +81 -22
  158. package/src/services/config-manager.ts +2 -2
@@ -0,0 +1,546 @@
1
+ /**
2
+ * ModestBench Test Adapter Command
3
+ *
4
+ * Run existing test files as benchmarks by capturing test definitions and
5
+ * executing them through a lightweight benchmark runner.
6
+ */
7
+
8
+ import { hostname } from 'node:os';
9
+ import { cpus, freemem, totalmem } from 'node:os';
10
+ import { resolve } from 'node:path';
11
+ import { performance } from 'node:perf_hooks';
12
+
13
+ import type {
14
+ BenchmarkRun,
15
+ FileResult,
16
+ ModestBenchConfig,
17
+ RunSummary,
18
+ SuiteResult,
19
+ TaskResult,
20
+ } from '../../types/core.js';
21
+ import type { CliContext } from '../index.js';
22
+
23
+ import { AvaAdapter } from '../../adapters/ava-adapter.js';
24
+ import { MochaAdapter } from '../../adapters/mocha-adapter.js';
25
+ import { NodeTestAdapter } from '../../adapters/node-test-adapter.js';
26
+ import {
27
+ type CapturedSuite,
28
+ type CapturedTestFile,
29
+ capturedToBenchmark,
30
+ type ConvertedBenchmarkSuite,
31
+ type TestFramework,
32
+ } from '../../adapters/types.js';
33
+ import { ExitCodes } from '../../types/cli.js';
34
+ import { createRunId } from '../../types/core.js';
35
+ import { isError } from '../../utils/type-guards.js';
36
+
37
+ /**
38
+ * Get a minimal default config for test runs
39
+ */
40
+ const getDefaultTestConfig = (
41
+ iterations: number,
42
+ warmup: number,
43
+ verbose: boolean,
44
+ ): ModestBenchConfig => ({
45
+ bail: false,
46
+ exclude: [],
47
+ excludeTags: [],
48
+ iterations,
49
+ limitBy: 'iterations',
50
+ metadata: {},
51
+ pattern: [],
52
+ quiet: false,
53
+ reporterConfig: {},
54
+ reporters: ['human'],
55
+ tags: [],
56
+ thresholds: {},
57
+ time: 1000,
58
+ timeout: 30000,
59
+ verbose,
60
+ warmup,
61
+ });
62
+
63
+ /**
64
+ * Default iteration count for test benchmarks
65
+ */
66
+ const DEFAULT_ITERATIONS = 100;
67
+
68
+ /**
69
+ * Default warmup iterations
70
+ */
71
+ const DEFAULT_WARMUP = 5;
72
+
73
+ /**
74
+ * Test command options
75
+ */
76
+ export interface TestOptions {
77
+ /** Bail on first failure */
78
+ bail?: boolean;
79
+ /** Working directory */
80
+ cwd?: string;
81
+ /** Test framework to use */
82
+ framework: TestFramework;
83
+ /** Number of iterations per test */
84
+ iterations?: number;
85
+ /** Output JSON */
86
+ json?: boolean;
87
+ /** Disable color */
88
+ noColor?: boolean;
89
+ /** Test file paths or patterns */
90
+ pattern?: string[];
91
+ /** Quiet mode */
92
+ quiet?: boolean;
93
+ /** Verbose output */
94
+ verbose?: boolean;
95
+ /** Warmup iterations */
96
+ warmup?: number;
97
+ }
98
+
99
+ /**
100
+ * Result of benchmarking a single test
101
+ */
102
+ interface TestBenchmarkResult {
103
+ /** Error if test failed */
104
+ error?: Error;
105
+ /** Number of iterations run */
106
+ iterations: number;
107
+ /** Maximum execution time in ms */
108
+ max: number;
109
+ /** Mean execution time in ms */
110
+ mean: number;
111
+ /** Minimum execution time in ms */
112
+ min: number;
113
+ /** Test name */
114
+ name: string;
115
+ /** Operations per second */
116
+ opsPerSecond: number;
117
+ /** Standard deviation in ms */
118
+ stdDev: number;
119
+ /** Total time in ms */
120
+ totalTime: number;
121
+ }
122
+
123
+ /**
124
+ * Handle test command - run test files as benchmarks
125
+ */
126
+ export const handleTestCommand = async (
127
+ context: CliContext,
128
+ options: TestOptions,
129
+ ): Promise<number> => {
130
+ const verbose = options.verbose ?? false;
131
+ const quiet = options.quiet ?? false;
132
+ const iterations = options.iterations ?? DEFAULT_ITERATIONS;
133
+ const warmup = options.warmup ?? DEFAULT_WARMUP;
134
+
135
+ // Get the reporter (default to human unless JSON requested)
136
+ const reporterName = options.json ? 'json' : 'human';
137
+ const reporter = context.reporterRegistry.get(reporterName);
138
+
139
+ try {
140
+ // Select the appropriate adapter
141
+ const adapter = selectAdapter(options.framework);
142
+
143
+ if (verbose && !quiet) {
144
+ console.error(`Using ${options.framework} adapter`);
145
+ }
146
+
147
+ // Resolve file paths
148
+ const cwd = options.cwd ?? process.cwd();
149
+ const patterns = options.pattern ?? [];
150
+
151
+ if (patterns.length === 0) {
152
+ console.error('Error: At least one test file path is required');
153
+ return ExitCodes.CONFIG_ERROR;
154
+ }
155
+
156
+ // Build config for the run
157
+ const config = getDefaultTestConfig(iterations, warmup, verbose);
158
+
159
+ // Initialize run tracking
160
+ const runStartTime = new Date();
161
+ const fileResults: FileResult[] = [];
162
+ let hasFailures = false;
163
+
164
+ // Build initial run for onStart (will be updated at end)
165
+ const runId = createRunId(crypto.randomUUID());
166
+ const initialRun = buildBenchmarkRun(
167
+ [],
168
+ config,
169
+ runStartTime,
170
+ runStartTime,
171
+ runId,
172
+ );
173
+
174
+ // Notify reporter of start
175
+ await reporter?.onStart?.(initialRun);
176
+
177
+ for (const pattern of patterns) {
178
+ const filePath = resolve(cwd, pattern);
179
+ const fileStartTime = new Date();
180
+
181
+ if (verbose && !quiet) {
182
+ console.error(`\nCapturing tests from: ${filePath}`);
183
+ }
184
+
185
+ await reporter?.onFileStart?.(filePath);
186
+
187
+ try {
188
+ const captured = await adapter.capture(filePath);
189
+
190
+ if (verbose && !quiet) {
191
+ const testCount = countAllTests(captured);
192
+ console.error(` Found ${testCount} test(s)`);
193
+ }
194
+
195
+ // Convert to benchmark definition
196
+ const benchmarkDef = capturedToBenchmark(captured);
197
+
198
+ // Run benchmarks and collect suite results
199
+ const suiteResults: SuiteResult[] = [];
200
+
201
+ // Execute benchmarks for each suite
202
+ for (const [suiteName, suite] of Object.entries(
203
+ benchmarkDef.suites,
204
+ ) as Array<[string, ConvertedBenchmarkSuite]>) {
205
+ const suiteStartTime = new Date();
206
+ const taskResults: TaskResult[] = [];
207
+
208
+ await reporter?.onSuiteStart?.(suiteName);
209
+
210
+ // Run setup if present
211
+ if (suite.setup) {
212
+ try {
213
+ await suite.setup();
214
+ } catch (setupError) {
215
+ const err = isError(setupError)
216
+ ? setupError
217
+ : new Error(String(setupError));
218
+ await reporter?.onError?.(err);
219
+ hasFailures = true;
220
+ if (options.bail) {
221
+ return ExitCodes.BENCHMARK_FAILURES;
222
+ }
223
+ continue;
224
+ }
225
+ }
226
+
227
+ // Run each benchmark
228
+ for (const [benchName, bench] of Object.entries(suite.benchmarks)) {
229
+ await reporter?.onTaskStart?.(benchName);
230
+
231
+ const result = await runBenchmark(
232
+ benchName,
233
+ bench.fn,
234
+ iterations,
235
+ warmup,
236
+ );
237
+
238
+ // Convert to TaskResult format
239
+ const taskResult = convertToTaskResult(result);
240
+ taskResults.push(taskResult);
241
+
242
+ // Notify reporter
243
+ await reporter?.onTaskResult?.(taskResult);
244
+
245
+ if (result.error) {
246
+ hasFailures = true;
247
+ if (options.bail) {
248
+ // Run teardown before bailing
249
+ if (suite.teardown) {
250
+ try {
251
+ await suite.teardown();
252
+ } catch {
253
+ // Ignore teardown errors when bailing
254
+ }
255
+ }
256
+ return ExitCodes.BENCHMARK_FAILURES;
257
+ }
258
+ }
259
+ }
260
+
261
+ // Run teardown if present
262
+ if (suite.teardown) {
263
+ try {
264
+ await suite.teardown();
265
+ } catch (teardownError) {
266
+ const err = isError(teardownError)
267
+ ? teardownError
268
+ : new Error(String(teardownError));
269
+ await reporter?.onError?.(err);
270
+ }
271
+ }
272
+
273
+ const suiteEndTime = new Date();
274
+ const suiteResult: SuiteResult = {
275
+ duration: suiteEndTime.getTime() - suiteStartTime.getTime(),
276
+ endTime: suiteEndTime,
277
+ name: suiteName,
278
+ startTime: suiteStartTime,
279
+ tasks: taskResults,
280
+ };
281
+
282
+ suiteResults.push(suiteResult);
283
+ await reporter?.onSuiteEnd?.(suiteResult);
284
+ }
285
+
286
+ const fileEndTime = new Date();
287
+ const fileResult: FileResult = {
288
+ duration: fileEndTime.getTime() - fileStartTime.getTime(),
289
+ endTime: fileEndTime,
290
+ filePath,
291
+ startTime: fileStartTime,
292
+ suites: suiteResults,
293
+ };
294
+
295
+ fileResults.push(fileResult);
296
+ await reporter?.onFileEnd?.(fileResult);
297
+ } catch (captureError) {
298
+ const err = isError(captureError)
299
+ ? captureError
300
+ : new Error(String(captureError));
301
+ await reporter?.onError?.(err);
302
+ hasFailures = true;
303
+ if (options.bail) {
304
+ return ExitCodes.BENCHMARK_FAILURES;
305
+ }
306
+ }
307
+ }
308
+
309
+ // Build final run result
310
+ const runEndTime = new Date();
311
+ const run = buildBenchmarkRun(
312
+ fileResults,
313
+ config,
314
+ runStartTime,
315
+ runEndTime,
316
+ runId,
317
+ );
318
+
319
+ // Notify reporter of end
320
+ await reporter?.onEnd?.(run);
321
+
322
+ return hasFailures ? ExitCodes.BENCHMARK_FAILURES : ExitCodes.SUCCESS;
323
+ } catch (error) {
324
+ const err = isError(error) ? error : new Error(String(error));
325
+ await reporter?.onError?.(err);
326
+ return ExitCodes.RUNTIME_ERROR;
327
+ }
328
+ };
329
+
330
+ /**
331
+ * Run a benchmark for a single test function
332
+ */
333
+ const runBenchmark = async (
334
+ name: string,
335
+ fn: (...args: any[]) => unknown,
336
+ iterations: number,
337
+ warmup: number,
338
+ ): Promise<TestBenchmarkResult> => {
339
+ const times: number[] = [];
340
+
341
+ try {
342
+ // Warmup phase
343
+ for (let i = 0; i < warmup; i++) {
344
+ await fn();
345
+ }
346
+
347
+ // Measurement phase
348
+ for (let i = 0; i < iterations; i++) {
349
+ const start = performance.now();
350
+ await fn();
351
+ const end = performance.now();
352
+ times.push(end - start);
353
+ }
354
+
355
+ // Calculate statistics
356
+ const totalTime = times.reduce((a, b) => a + b, 0);
357
+ const mean = totalTime / times.length;
358
+ const min = Math.min(...times);
359
+ const max = Math.max(...times);
360
+
361
+ // Standard deviation
362
+ const squaredDiffs = times.map((t) => Math.pow(t - mean, 2));
363
+ const avgSquaredDiff =
364
+ squaredDiffs.reduce((a, b) => a + b, 0) / squaredDiffs.length;
365
+ const stdDev = Math.sqrt(avgSquaredDiff);
366
+
367
+ // Ops per second
368
+ const opsPerSecond = mean > 0 ? 1000 / mean : 0;
369
+
370
+ return {
371
+ iterations,
372
+ max,
373
+ mean,
374
+ min,
375
+ name,
376
+ opsPerSecond,
377
+ stdDev,
378
+ totalTime,
379
+ };
380
+ } catch (error) {
381
+ return {
382
+ error: isError(error) ? error : new Error(String(error)),
383
+ iterations: 0,
384
+ max: 0,
385
+ mean: 0,
386
+ min: 0,
387
+ name,
388
+ opsPerSecond: 0,
389
+ stdDev: 0,
390
+ totalTime: 0,
391
+ };
392
+ }
393
+ };
394
+
395
+ /**
396
+ * Convert internal TestBenchmarkResult to standard TaskResult format
397
+ *
398
+ * Note: Times are converted from milliseconds to nanoseconds
399
+ */
400
+ const convertToTaskResult = (result: TestBenchmarkResult): TaskResult => {
401
+ const MS_TO_NS = 1_000_000;
402
+
403
+ // Calculate coefficient of variation (stdDev/mean × 100)
404
+ const cv = result.mean > 0 ? (result.stdDev / result.mean) * 100 : 0;
405
+
406
+ // Estimate margin of error (using 95% confidence, ~1.96 * stdErr)
407
+ const stdErr = result.stdDev / Math.sqrt(result.iterations);
408
+ const marginOfError =
409
+ result.mean > 0 ? ((1.96 * stdErr) / result.mean) * 100 : 0;
410
+
411
+ // Variance in ns² = (stdDev in ms)² × (MS_TO_NS)²
412
+ // This is equivalent to (stdDev × MS_TO_NS)² since variance = stdDev²
413
+ const variance = result.stdDev * result.stdDev * MS_TO_NS * MS_TO_NS;
414
+
415
+ return {
416
+ cv,
417
+ error: result.error,
418
+ iterations: result.iterations,
419
+ marginOfError,
420
+ max: result.max * MS_TO_NS,
421
+ mean: result.mean * MS_TO_NS,
422
+ min: result.min * MS_TO_NS,
423
+ name: result.name,
424
+ opsPerSecond: result.opsPerSecond,
425
+ // Percentile approximations: without storing all timing samples, we estimate
426
+ // p95/p99 as fractions of max. These are NOT true percentiles from the distribution.
427
+ p95: result.max * MS_TO_NS * 0.95,
428
+ p99: result.max * MS_TO_NS * 0.99,
429
+ stdDev: result.stdDev * MS_TO_NS,
430
+ variance,
431
+ };
432
+ };
433
+
434
+ /**
435
+ * Build the final BenchmarkRun result structure
436
+ */
437
+ const buildBenchmarkRun = (
438
+ files: FileResult[],
439
+ config: ModestBenchConfig,
440
+ startTime: Date,
441
+ endTime: Date,
442
+ runId: ReturnType<typeof createRunId>,
443
+ ): BenchmarkRun => {
444
+ // Collect all task results for summary
445
+ const allTasks: TaskResult[] = [];
446
+ for (const file of files) {
447
+ for (const suite of file.suites) {
448
+ allTasks.push(...suite.tasks);
449
+ }
450
+ }
451
+
452
+ // Find fastest and slowest
453
+ const validTasks = allTasks.filter((t) => !t.error && t.opsPerSecond > 0);
454
+ const fastest =
455
+ validTasks.length > 0
456
+ ? validTasks.reduce((a, b) => (a.opsPerSecond > b.opsPerSecond ? a : b))
457
+ : null;
458
+ const slowest =
459
+ validTasks.length > 0
460
+ ? validTasks.reduce((a, b) => (a.opsPerSecond < b.opsPerSecond ? a : b))
461
+ : null;
462
+
463
+ // Calculate summary
464
+ const summary: RunSummary = {
465
+ failedTasks: allTasks.filter((t) => t.error).length,
466
+ fastest,
467
+ overallMean:
468
+ validTasks.length > 0
469
+ ? validTasks.reduce((sum, t) => sum + t.mean, 0) / validTasks.length
470
+ : 0,
471
+ passedTasks: validTasks.length,
472
+ slowest,
473
+ totalFiles: files.length,
474
+ totalOperations: allTasks.reduce((sum, t) => sum + t.iterations, 0),
475
+ totalSuites: files.reduce((sum, f) => sum + f.suites.length, 0),
476
+ totalTasks: allTasks.length,
477
+ };
478
+
479
+ return {
480
+ config,
481
+ duration: endTime.getTime() - startTime.getTime(),
482
+ endTime,
483
+ environment: {
484
+ arch: process.arch,
485
+ availableMemory: freemem(),
486
+ cpu: {
487
+ cores: cpus().length,
488
+ model: cpus()[0]?.model ?? 'unknown',
489
+ speed: cpus()[0]?.speed ?? 0,
490
+ },
491
+ env: {},
492
+ hostname: hostname(),
493
+ memory: {
494
+ free: freemem(),
495
+ total: totalmem(),
496
+ used: totalmem() - freemem(),
497
+ },
498
+ nodeVersion: process.version,
499
+ platform: process.platform,
500
+ },
501
+ files,
502
+ id: runId,
503
+ startTime,
504
+ summary,
505
+ };
506
+ };
507
+
508
+ /**
509
+ * Select the appropriate adapter for the framework
510
+ */
511
+ const selectAdapter = (framework: TestFramework) => {
512
+ switch (framework) {
513
+ case 'ava':
514
+ return new AvaAdapter();
515
+ case 'mocha':
516
+ return new MochaAdapter();
517
+ case 'node-test':
518
+ return new NodeTestAdapter();
519
+ default: {
520
+ const _exhaustive: never = framework;
521
+ throw new Error(`Unknown framework: ${_exhaustive}`);
522
+ }
523
+ }
524
+ };
525
+
526
+ /**
527
+ * Count all tests in a captured file
528
+ */
529
+ const countAllTests = (captured: CapturedTestFile): number => {
530
+ let count = captured.rootTests.length;
531
+ for (const suite of captured.rootSuites) {
532
+ count += countTestsInSuite(suite);
533
+ }
534
+ return count;
535
+ };
536
+
537
+ /**
538
+ * Count tests in a suite recursively
539
+ */
540
+ const countTestsInSuite = (suite: CapturedSuite): number => {
541
+ let count = suite.tests.length;
542
+ for (const child of suite.children) {
543
+ count += countTestsInSuite(child);
544
+ }
545
+ return count;
546
+ };
package/src/cli/index.ts CHANGED
@@ -24,6 +24,7 @@ import type {
24
24
  import { bootstrap } from '../bootstrap.js';
25
25
  import {
26
26
  ABORT_TIMEOUT,
27
+ DEFAULT_BENCHMARK_DIR,
27
28
  DEFAULT_ENGINE,
28
29
  DEFAULT_REPORTER,
29
30
  Engines,
@@ -65,6 +66,10 @@ import {
65
66
  RUN_COMMAND_DEFAULTS,
66
67
  handleRunCommand as runCommand,
67
68
  } from './commands/run.js';
69
+ import {
70
+ handleTestCommand as testCommand,
71
+ type TestOptions,
72
+ } from './commands/test.js';
68
73
 
69
74
  /**
70
75
  * CLI context with initialized services
@@ -177,7 +182,7 @@ export const main = async (
177
182
  yargs
178
183
  .positional('pattern', {
179
184
  array: true,
180
- defaultDescription: '(auto-discovered from bench/ directory)',
185
+ defaultDescription: `(auto-discovered from ${DEFAULT_BENCHMARK_DIR} directory)`,
181
186
  describe:
182
187
  'File paths, directory paths, or glob patterns for benchmark files',
183
188
  type: 'string',
@@ -986,6 +991,81 @@ export const main = async (
986
991
  process.exitCode = await analyzeCommand(context, options);
987
992
  },
988
993
  )
994
+ .command(
995
+ 'test <framework> [files..]',
996
+ 'Run test files as benchmarks (captures tests from Mocha, node:test, or AVA)',
997
+ (yargs) => {
998
+ return yargs
999
+ .positional('framework', {
1000
+ choices: ['mocha', 'node-test', 'ava'] as const,
1001
+ demandOption: true,
1002
+ describe: 'Test framework to use',
1003
+ nargs: 1,
1004
+ type: 'string',
1005
+ })
1006
+ .positional('files', {
1007
+ array: true,
1008
+ describe: 'Test file paths or glob patterns',
1009
+ type: 'string',
1010
+ })
1011
+ .option('iterations', {
1012
+ alias: 'i',
1013
+ default: 100,
1014
+ description: 'Number of iterations per test',
1015
+ type: 'number',
1016
+ })
1017
+ .option('warmup', {
1018
+ alias: 'w',
1019
+ default: 5,
1020
+ description: 'Number of warmup iterations',
1021
+ type: 'number',
1022
+ })
1023
+ .option('bail', {
1024
+ alias: 'b',
1025
+ default: false,
1026
+ description: 'Stop on first failure',
1027
+ type: 'boolean',
1028
+ })
1029
+ .option('quiet', {
1030
+ alias: 'q',
1031
+ default: false,
1032
+ description: 'Minimal output',
1033
+ type: 'boolean',
1034
+ })
1035
+ .example([
1036
+ ['$0 test mocha test/*.spec.js', 'Run Mocha tests as benchmarks'],
1037
+ [
1038
+ '$0 test node-test test/*.test.js',
1039
+ 'Run node:test tests as benchmarks',
1040
+ ],
1041
+ [
1042
+ '$0 test ava test/*.js --iterations 500',
1043
+ 'Run AVA tests with 500 iterations',
1044
+ ],
1045
+ [
1046
+ '$0 test mocha test/unit.spec.js --json',
1047
+ 'Output results as JSON',
1048
+ ],
1049
+ ]);
1050
+ },
1051
+ async (argv) => {
1052
+ const context = await createCliContext(argv, abortController!);
1053
+ const options: TestOptions = {
1054
+ bail: argv.bail,
1055
+ cwd: argv.cwd,
1056
+ framework: argv.framework as TestOptions['framework'],
1057
+ iterations: argv.iterations,
1058
+ json: argv.json,
1059
+ noColor: argv.noColor,
1060
+ pattern: argv.files,
1061
+ quiet: argv.quiet,
1062
+ verbose: argv.verbose,
1063
+ warmup: argv.warmup,
1064
+ };
1065
+ const exitCode = await testCommand(context, options);
1066
+ process.exit(exitCode);
1067
+ },
1068
+ )
989
1069
  .fail((msg, err, yargs) => {
990
1070
  if (err) {
991
1071
  console.error('Error:', err.message);
package/src/constants.ts CHANGED
@@ -131,3 +131,18 @@ export const ErrorCodes = {
131
131
  VALIDATION_TYPE_FAILED: 'ERR_MB_VALIDATION_TYPE_FAILED',
132
132
  //#endregion
133
133
  } as const;
134
+
135
+ /**
136
+ * Default output directory for reporters, history, cache, etc.
137
+ */
138
+ export const DEFAULT_OUTPUT_DIR = './.modestbench';
139
+
140
+ /**
141
+ * Default directory containing `*.bench.*` files
142
+ */
143
+ export const DEFAULT_BENCHMARK_DIR = 'bench';
144
+
145
+ /**
146
+ * URL of the site
147
+ */
148
+ export const SITE_URL = 'https://modestbench.dev';
@@ -323,6 +323,11 @@ export abstract class ModestBenchEngine implements BenchmarkEngine {
323
323
  file.suites.flatMap((suite: SuiteResult) => suite.tasks),
324
324
  );
325
325
  const finalTotalTasks = allTasks.length;
326
+
327
+ // Count task failures and passed tasks
328
+ // Note: Suite-level failures (setup errors) are NOT counted here to keep
329
+ // passedTasks + failedTasks == finalTotalTasks. Suite failures are detected
330
+ // separately in the CLI for exit code purposes.
326
331
  const failedTasks = allTasks.filter((task) => task.error).length;
327
332
  const passedTasks = finalTotalTasks - failedTasks;
328
333