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.
- package/CHANGELOG.md +37 -0
- package/README.md +45 -2
- package/dist/adapters/ava-adapter.cjs +421 -0
- package/dist/adapters/ava-adapter.cjs.map +1 -0
- package/dist/adapters/ava-adapter.d.cts +39 -0
- package/dist/adapters/ava-adapter.d.cts.map +1 -0
- package/dist/adapters/ava-adapter.d.ts +39 -0
- package/dist/adapters/ava-adapter.d.ts.map +1 -0
- package/dist/adapters/ava-adapter.js +384 -0
- package/dist/adapters/ava-adapter.js.map +1 -0
- package/dist/adapters/ava-hooks.cjs +66 -0
- package/dist/adapters/ava-hooks.cjs.map +1 -0
- package/dist/adapters/ava-hooks.d.cts +24 -0
- package/dist/adapters/ava-hooks.d.cts.map +1 -0
- package/dist/adapters/ava-hooks.d.ts +24 -0
- package/dist/adapters/ava-hooks.d.ts.map +1 -0
- package/dist/adapters/ava-hooks.js +61 -0
- package/dist/adapters/ava-hooks.js.map +1 -0
- package/dist/adapters/ava-register.cjs +16 -0
- package/dist/adapters/ava-register.cjs.map +1 -0
- package/dist/adapters/ava-register.d.cts +11 -0
- package/dist/adapters/ava-register.d.cts.map +1 -0
- package/dist/adapters/ava-register.d.ts +11 -0
- package/dist/adapters/ava-register.d.ts.map +1 -0
- package/dist/adapters/ava-register.js +14 -0
- package/dist/adapters/ava-register.js.map +1 -0
- package/dist/adapters/mocha-adapter.cjs +254 -0
- package/dist/adapters/mocha-adapter.cjs.map +1 -0
- package/dist/adapters/mocha-adapter.d.cts +26 -0
- package/dist/adapters/mocha-adapter.d.cts.map +1 -0
- package/dist/adapters/mocha-adapter.d.ts +26 -0
- package/dist/adapters/mocha-adapter.d.ts.map +1 -0
- package/dist/adapters/mocha-adapter.js +217 -0
- package/dist/adapters/mocha-adapter.js.map +1 -0
- package/dist/adapters/node-test-adapter.cjs +335 -0
- package/dist/adapters/node-test-adapter.cjs.map +1 -0
- package/dist/adapters/node-test-adapter.d.cts +41 -0
- package/dist/adapters/node-test-adapter.d.cts.map +1 -0
- package/dist/adapters/node-test-adapter.d.ts +41 -0
- package/dist/adapters/node-test-adapter.d.ts.map +1 -0
- package/dist/adapters/node-test-adapter.js +298 -0
- package/dist/adapters/node-test-adapter.js.map +1 -0
- package/dist/adapters/node-test-hooks.cjs +72 -0
- package/dist/adapters/node-test-hooks.cjs.map +1 -0
- package/dist/adapters/node-test-hooks.d.cts +24 -0
- package/dist/adapters/node-test-hooks.d.cts.map +1 -0
- package/dist/adapters/node-test-hooks.d.ts +24 -0
- package/dist/adapters/node-test-hooks.d.ts.map +1 -0
- package/dist/adapters/node-test-hooks.js +67 -0
- package/dist/adapters/node-test-hooks.js.map +1 -0
- package/dist/adapters/node-test-register.cjs +7 -0
- package/dist/adapters/node-test-register.cjs.map +1 -0
- package/dist/adapters/node-test-register.d.cts +2 -0
- package/dist/adapters/node-test-register.d.cts.map +1 -0
- package/dist/adapters/node-test-register.d.ts +2 -0
- package/dist/adapters/node-test-register.d.ts.map +1 -0
- package/dist/adapters/node-test-register.js +5 -0
- package/dist/adapters/node-test-register.js.map +1 -0
- package/dist/adapters/types.cjs +152 -0
- package/dist/adapters/types.cjs.map +1 -0
- package/dist/adapters/types.d.cts +112 -0
- package/dist/adapters/types.d.cts.map +1 -0
- package/dist/adapters/types.d.ts +112 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +148 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/cli/commands/init.cjs +21 -17
- package/dist/cli/commands/init.cjs.map +1 -1
- package/dist/cli/commands/init.d.cts.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +21 -17
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/run.cjs +6 -2
- package/dist/cli/commands/run.cjs.map +1 -1
- package/dist/cli/commands/run.js +6 -2
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/commands/test.cjs +392 -0
- package/dist/cli/commands/test.cjs.map +1 -0
- package/dist/cli/commands/test.d.cts +38 -0
- package/dist/cli/commands/test.d.cts.map +1 -0
- package/dist/cli/commands/test.d.ts +38 -0
- package/dist/cli/commands/test.d.ts.map +1 -0
- package/dist/cli/commands/test.js +388 -0
- package/dist/cli/commands/test.js.map +1 -0
- package/dist/cli/index.cjs +72 -1
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.d.cts.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +73 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/constants.cjs +13 -1
- package/dist/constants.cjs.map +1 -1
- package/dist/constants.d.cts +12 -0
- package/dist/constants.d.cts.map +1 -1
- package/dist/constants.d.ts +12 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +12 -0
- package/dist/constants.js.map +1 -1
- package/dist/core/engine.cjs +4 -0
- package/dist/core/engine.cjs.map +1 -1
- package/dist/core/engine.d.cts.map +1 -1
- package/dist/core/engine.d.ts.map +1 -1
- package/dist/core/engine.js +4 -0
- package/dist/core/engine.js.map +1 -1
- package/dist/core/engines/tinybench-engine.cjs +163 -131
- package/dist/core/engines/tinybench-engine.cjs.map +1 -1
- package/dist/core/engines/tinybench-engine.d.cts +6 -0
- package/dist/core/engines/tinybench-engine.d.cts.map +1 -1
- package/dist/core/engines/tinybench-engine.d.ts +6 -0
- package/dist/core/engines/tinybench-engine.d.ts.map +1 -1
- package/dist/core/engines/tinybench-engine.js +163 -131
- package/dist/core/engines/tinybench-engine.js.map +1 -1
- package/dist/errors/base.cjs +2 -1
- package/dist/errors/base.cjs.map +1 -1
- package/dist/errors/base.d.cts.map +1 -1
- package/dist/errors/base.d.ts.map +1 -1
- package/dist/errors/base.js +2 -1
- package/dist/errors/base.js.map +1 -1
- package/dist/reporters/human.cjs +83 -27
- package/dist/reporters/human.cjs.map +1 -1
- package/dist/reporters/human.d.cts +1 -0
- package/dist/reporters/human.d.cts.map +1 -1
- package/dist/reporters/human.d.ts +1 -0
- package/dist/reporters/human.d.ts.map +1 -1
- package/dist/reporters/human.js +83 -27
- package/dist/reporters/human.js.map +1 -1
- package/dist/reporters/simple.cjs +68 -21
- package/dist/reporters/simple.cjs.map +1 -1
- package/dist/reporters/simple.d.cts +1 -0
- package/dist/reporters/simple.d.cts.map +1 -1
- package/dist/reporters/simple.d.ts +1 -0
- package/dist/reporters/simple.d.ts.map +1 -1
- package/dist/reporters/simple.js +68 -21
- package/dist/reporters/simple.js.map +1 -1
- package/dist/services/config-manager.cjs +1 -1
- package/dist/services/config-manager.cjs.map +1 -1
- package/dist/services/config-manager.js +2 -2
- package/dist/services/config-manager.js.map +1 -1
- package/package.json +60 -31
- package/src/adapters/ava-adapter.ts +553 -0
- package/src/adapters/ava-hooks.ts +65 -0
- package/src/adapters/ava-register.ts +15 -0
- package/src/adapters/mocha-adapter.ts +284 -0
- package/src/adapters/node-test-adapter.ts +391 -0
- package/src/adapters/node-test-hooks.ts +71 -0
- package/src/adapters/node-test-register.ts +5 -0
- package/src/adapters/types.ts +281 -0
- package/src/cli/commands/init.ts +25 -17
- package/src/cli/commands/run.ts +9 -2
- package/src/cli/commands/test.ts +546 -0
- package/src/cli/index.ts +81 -1
- package/src/constants.ts +15 -0
- package/src/core/engine.ts +5 -0
- package/src/core/engines/tinybench-engine.ts +213 -141
- package/src/errors/base.ts +3 -2
- package/src/reporters/human.ts +107 -36
- package/src/reporters/simple.ts +81 -22
- 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:
|
|
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';
|
package/src/core/engine.ts
CHANGED
|
@@ -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
|
|