modestbench 0.3.1 → 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 (175) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +46 -3
  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 -16
  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 -16
  72. package/dist/cli/commands/init.js.map +1 -1
  73. package/dist/cli/commands/run.cjs +9 -2
  74. package/dist/cli/commands/run.cjs.map +1 -1
  75. package/dist/cli/commands/run.d.cts.map +1 -1
  76. package/dist/cli/commands/run.d.ts.map +1 -1
  77. package/dist/cli/commands/run.js +9 -2
  78. package/dist/cli/commands/run.js.map +1 -1
  79. package/dist/cli/commands/test.cjs +392 -0
  80. package/dist/cli/commands/test.cjs.map +1 -0
  81. package/dist/cli/commands/test.d.cts +38 -0
  82. package/dist/cli/commands/test.d.cts.map +1 -0
  83. package/dist/cli/commands/test.d.ts +38 -0
  84. package/dist/cli/commands/test.d.ts.map +1 -0
  85. package/dist/cli/commands/test.js +388 -0
  86. package/dist/cli/commands/test.js.map +1 -0
  87. package/dist/cli/index.cjs +72 -1
  88. package/dist/cli/index.cjs.map +1 -1
  89. package/dist/cli/index.d.cts.map +1 -1
  90. package/dist/cli/index.d.ts.map +1 -1
  91. package/dist/cli/index.js +73 -2
  92. package/dist/cli/index.js.map +1 -1
  93. package/dist/config/schema.cjs +2 -1
  94. package/dist/config/schema.cjs.map +1 -1
  95. package/dist/config/schema.d.cts +3 -3
  96. package/dist/config/schema.d.cts.map +1 -1
  97. package/dist/config/schema.d.ts +3 -3
  98. package/dist/config/schema.d.ts.map +1 -1
  99. package/dist/config/schema.js +2 -1
  100. package/dist/config/schema.js.map +1 -1
  101. package/dist/constants.cjs +13 -1
  102. package/dist/constants.cjs.map +1 -1
  103. package/dist/constants.d.cts +12 -0
  104. package/dist/constants.d.cts.map +1 -1
  105. package/dist/constants.d.ts +12 -0
  106. package/dist/constants.d.ts.map +1 -1
  107. package/dist/constants.js +12 -0
  108. package/dist/constants.js.map +1 -1
  109. package/dist/core/engine.cjs +4 -0
  110. package/dist/core/engine.cjs.map +1 -1
  111. package/dist/core/engine.d.cts.map +1 -1
  112. package/dist/core/engine.d.ts.map +1 -1
  113. package/dist/core/engine.js +4 -0
  114. package/dist/core/engine.js.map +1 -1
  115. package/dist/core/engines/tinybench-engine.cjs +163 -131
  116. package/dist/core/engines/tinybench-engine.cjs.map +1 -1
  117. package/dist/core/engines/tinybench-engine.d.cts +6 -0
  118. package/dist/core/engines/tinybench-engine.d.cts.map +1 -1
  119. package/dist/core/engines/tinybench-engine.d.ts +6 -0
  120. package/dist/core/engines/tinybench-engine.d.ts.map +1 -1
  121. package/dist/core/engines/tinybench-engine.js +163 -131
  122. package/dist/core/engines/tinybench-engine.js.map +1 -1
  123. package/dist/errors/base.cjs +2 -1
  124. package/dist/errors/base.cjs.map +1 -1
  125. package/dist/errors/base.d.cts.map +1 -1
  126. package/dist/errors/base.d.ts.map +1 -1
  127. package/dist/errors/base.js +2 -1
  128. package/dist/errors/base.js.map +1 -1
  129. package/dist/reporters/human.cjs +83 -27
  130. package/dist/reporters/human.cjs.map +1 -1
  131. package/dist/reporters/human.d.cts +1 -0
  132. package/dist/reporters/human.d.cts.map +1 -1
  133. package/dist/reporters/human.d.ts +1 -0
  134. package/dist/reporters/human.d.ts.map +1 -1
  135. package/dist/reporters/human.js +83 -27
  136. package/dist/reporters/human.js.map +1 -1
  137. package/dist/reporters/simple.cjs +68 -21
  138. package/dist/reporters/simple.cjs.map +1 -1
  139. package/dist/reporters/simple.d.cts +1 -0
  140. package/dist/reporters/simple.d.cts.map +1 -1
  141. package/dist/reporters/simple.d.ts +1 -0
  142. package/dist/reporters/simple.d.ts.map +1 -1
  143. package/dist/reporters/simple.js +68 -21
  144. package/dist/reporters/simple.js.map +1 -1
  145. package/dist/schema/modestbench-config.schema.json +1 -1
  146. package/dist/services/config-manager.cjs +2 -2
  147. package/dist/services/config-manager.cjs.map +1 -1
  148. package/dist/services/config-manager.js +3 -3
  149. package/dist/services/config-manager.js.map +1 -1
  150. package/dist/types/core.d.cts +2 -2
  151. package/dist/types/core.d.cts.map +1 -1
  152. package/dist/types/core.d.ts +2 -2
  153. package/dist/types/core.d.ts.map +1 -1
  154. package/package.json +60 -31
  155. package/src/adapters/ava-adapter.ts +553 -0
  156. package/src/adapters/ava-hooks.ts +65 -0
  157. package/src/adapters/ava-register.ts +15 -0
  158. package/src/adapters/mocha-adapter.ts +284 -0
  159. package/src/adapters/node-test-adapter.ts +391 -0
  160. package/src/adapters/node-test-hooks.ts +71 -0
  161. package/src/adapters/node-test-register.ts +5 -0
  162. package/src/adapters/types.ts +281 -0
  163. package/src/cli/commands/init.ts +25 -16
  164. package/src/cli/commands/run.ts +12 -2
  165. package/src/cli/commands/test.ts +546 -0
  166. package/src/cli/index.ts +81 -1
  167. package/src/config/schema.ts +2 -1
  168. package/src/constants.ts +15 -0
  169. package/src/core/engine.ts +5 -0
  170. package/src/core/engines/tinybench-engine.ts +213 -141
  171. package/src/errors/base.ts +3 -2
  172. package/src/reporters/human.ts +107 -36
  173. package/src/reporters/simple.ts +81 -22
  174. package/src/services/config-manager.ts +3 -3
  175. package/src/types/core.ts +2 -2
@@ -0,0 +1,553 @@
1
+ /**
2
+ * ModestBench AVA Adapter
3
+ *
4
+ * Captures test definitions from AVA test files using ES module loader hooks.
5
+ *
6
+ * AVA differs from Mocha and node:test:
7
+ *
8
+ * - No describe blocks - tests are flat
9
+ * - Tests receive an execution context `t` with assertions
10
+ * - Supports test.before/after/beforeEach/afterEach at file level
11
+ * - Each test file is isolated (we don't need to handle that)
12
+ *
13
+ * Architecture:
14
+ *
15
+ * 1. Install mock on globalThis
16
+ * 2. Loader intercepts 'ava' imports and returns the mock
17
+ * 3. Import test file to capture test definitions
18
+ */
19
+
20
+ import { pathToFileURL } from 'node:url';
21
+
22
+ import type {
23
+ CapturedSuite,
24
+ CapturedTest,
25
+ CapturedTestFile,
26
+ SuiteHooks,
27
+ TestFrameworkAdapter,
28
+ } from './types.js';
29
+
30
+ /**
31
+ * Global capture state key
32
+ */
33
+ const CAPTURE_STATE_KEY = '__MODESTBENCH_AVA_CAPTURE__';
34
+
35
+ /**
36
+ * AVA's test execution context (simplified mock)
37
+ *
38
+ * Real AVA passes a context object with assertions. We provide a minimal mock
39
+ * that allows tests to run without crashing.
40
+ */
41
+ interface AvaExecutionContext {
42
+ assert: (value: unknown, message?: string) => void;
43
+ context: Record<string, unknown>;
44
+ deepEqual: (actual: unknown, expected: unknown, message?: string) => void;
45
+ fail: (message?: string) => void;
46
+ false: (value: unknown, message?: string) => void;
47
+ falsy: (value: unknown, message?: string) => void;
48
+ is: (actual: unknown, expected: unknown, message?: string) => void;
49
+ like: (actual: unknown, selector: unknown, message?: string) => void;
50
+ log: (...values: unknown[]) => void;
51
+ not: (actual: unknown, expected: unknown, message?: string) => void;
52
+ notDeepEqual: (actual: unknown, expected: unknown, message?: string) => void;
53
+ notRegex: (contents: string, regex: RegExp, message?: string) => void;
54
+ notThrows: (fn: () => unknown, message?: string) => void;
55
+ notThrowsAsync: (
56
+ fn: () => Promise<unknown>,
57
+ message?: string,
58
+ ) => Promise<void>;
59
+ pass: (message?: string) => void;
60
+ plan: (count: number) => void;
61
+ regex: (contents: string, regex: RegExp, message?: string) => void;
62
+ snapshot: (expected: unknown, message?: string) => void;
63
+ teardown: (fn: () => Promise<void> | void) => void;
64
+ throws: (
65
+ fn: () => unknown,
66
+ expectations?: unknown,
67
+ message?: string,
68
+ ) => unknown;
69
+ throwsAsync: (
70
+ fn: () => Promise<unknown>,
71
+ expectations?: unknown,
72
+ message?: string,
73
+ ) => Promise<unknown>;
74
+ timeout: (ms: number, message?: string) => void;
75
+ true: (value: unknown, message?: string) => void;
76
+ truthy: (value: unknown, message?: string) => void;
77
+ try: (
78
+ ...args: unknown[]
79
+ ) => Promise<{ commit: () => void; discard: () => void; passed: boolean }>;
80
+ }
81
+
82
+ /**
83
+ * AVA macro type - can be a function or object with exec/title
84
+ */
85
+ interface AvaMacro {
86
+ exec: AvaTestFn;
87
+ title?: (providedTitle: string | undefined, ...args: unknown[]) => string;
88
+ }
89
+
90
+ /**
91
+ * AVA test function type
92
+ */
93
+ type AvaTestFn = (
94
+ t: AvaExecutionContext,
95
+ ...args: unknown[]
96
+ ) => Promise<void> | void;
97
+
98
+ /**
99
+ * Check if a value is an AVA macro object (has exec property)
100
+ */
101
+ const isMacroObject = (value: unknown): value is AvaMacro => {
102
+ return (
103
+ typeof value === 'object' &&
104
+ value !== null &&
105
+ 'exec' in value &&
106
+ typeof (value as AvaMacro).exec === 'function'
107
+ );
108
+ };
109
+
110
+ /**
111
+ * Check if a value is a function (macro or test function)
112
+ */
113
+ const isFunction = (value: unknown): value is AvaTestFn => {
114
+ return typeof value === 'function';
115
+ };
116
+
117
+ /**
118
+ * Internal capture state structure
119
+ *
120
+ * AVA doesn't have suites, so we capture everything as root-level tests with
121
+ * file-level hooks stored separately
122
+ */
123
+ interface CaptureState {
124
+ hooks: {
125
+ after: Array<() => Promise<void> | void>;
126
+ afterAlways: Array<() => Promise<void> | void>;
127
+ afterEach: Array<() => Promise<void> | void>;
128
+ afterEachAlways: Array<() => Promise<void> | void>;
129
+ before: Array<() => Promise<void> | void>;
130
+ beforeEach: Array<() => Promise<void> | void>;
131
+ };
132
+ tests: MutableCapturedTest[];
133
+ }
134
+
135
+ /**
136
+ * Mutable version of CapturedTest
137
+ */
138
+ interface MutableCapturedTest {
139
+ fn: (t: AvaExecutionContext) => Promise<void> | void;
140
+ name: string;
141
+ only?: boolean;
142
+ serial?: boolean;
143
+ skip?: boolean;
144
+ }
145
+
146
+ /**
147
+ * AVA test framework adapter
148
+ *
149
+ * Captures test definitions by installing mock implementations before importing
150
+ * the test file.
151
+ *
152
+ * IMPORTANT: For this to work with actual AVA imports, you must run Node.js
153
+ * with our loader: node --import modestbench/ava your-test.js
154
+ */
155
+ export class AvaAdapter implements TestFrameworkAdapter {
156
+ readonly framework = 'ava' as const;
157
+
158
+ /**
159
+ * Capture test definitions from an AVA test file
160
+ *
161
+ * @param filePath - Absolute path to the test file
162
+ * @returns Captured test structure
163
+ */
164
+ async capture(filePath: string): Promise<CapturedTestFile> {
165
+ // Initialize capture state
166
+ const state = initCaptureState();
167
+
168
+ // Install mocks
169
+ installAvaMocks(state);
170
+
171
+ try {
172
+ // Import the test file
173
+ // The loader hook will intercept 'ava' and use our mocks
174
+ const fileUrl = pathToFileURL(filePath).href;
175
+ const bustCache = `?t=${Date.now()}`;
176
+ await import(fileUrl + bustCache);
177
+
178
+ // Return captured structure
179
+ return toCapturedTestFile(state, filePath);
180
+ } finally {
181
+ // Clean up
182
+ uninstallAvaMocks();
183
+ clearCaptureState();
184
+ }
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Clear capture state from globalThis
190
+ */
191
+ const clearCaptureState = (): void => {
192
+ // @ts-expect-error - intentionally using globalThis
193
+ delete globalThis[CAPTURE_STATE_KEY];
194
+ };
195
+
196
+ /**
197
+ * Create a mock AVA execution context
198
+ *
199
+ * This allows test functions to call t.is(), t.pass(), etc. without errors
200
+ * during benchmarking. We're measuring performance, not correctness.
201
+ */
202
+ const createMockContext = (): AvaExecutionContext => {
203
+ const noop = () => {};
204
+ const noopAsync = () => Promise.resolve();
205
+
206
+ return {
207
+ assert: noop,
208
+ context: {},
209
+ deepEqual: noop,
210
+ fail: noop,
211
+ false: noop,
212
+ falsy: noop,
213
+ is: noop,
214
+ like: noop,
215
+ log: noop,
216
+ not: noop,
217
+ notDeepEqual: noop,
218
+ notRegex: noop,
219
+ notThrows: noop,
220
+ notThrowsAsync: noopAsync,
221
+ pass: noop,
222
+ plan: noop,
223
+ regex: noop,
224
+ snapshot: noop,
225
+ teardown: noop,
226
+ throws: () => undefined,
227
+ throwsAsync: noopAsync as () => Promise<unknown>,
228
+ timeout: noop,
229
+ true: noop,
230
+ truthy: noop,
231
+ try: async () => ({ commit: noop, discard: noop, passed: true }),
232
+ };
233
+ };
234
+
235
+ /**
236
+ * Initialize capture state on globalThis
237
+ */
238
+ const initCaptureState = (): CaptureState => {
239
+ const state: CaptureState = {
240
+ hooks: {
241
+ after: [],
242
+ afterAlways: [],
243
+ afterEach: [],
244
+ afterEachAlways: [],
245
+ before: [],
246
+ beforeEach: [],
247
+ },
248
+ tests: [],
249
+ };
250
+
251
+ // @ts-expect-error - intentionally using globalThis for cross-module state
252
+ globalThis[CAPTURE_STATE_KEY] = state;
253
+
254
+ return state;
255
+ };
256
+
257
+ /**
258
+ * Install AVA mocks on globalThis for module interception
259
+ */
260
+ const installAvaMocks = (state: CaptureState): void => {
261
+ /**
262
+ * Parse test arguments and return name + wrapped function Used by
263
+ * registerTest and test variants (only, serial, skip)
264
+ */
265
+ const parseTestArgs = (
266
+ titleOrMacroOrFn: AvaMacro | AvaTestFn | string,
267
+ rest: unknown[],
268
+ ): { name: string; wrappedFn: () => Promise<void> } => {
269
+ let name: string;
270
+ let fn: AvaTestFn;
271
+ let args: unknown[] = [];
272
+
273
+ if (typeof titleOrMacroOrFn === 'string') {
274
+ name = titleOrMacroOrFn;
275
+ const [macroOrFn, ...restArgs] = rest;
276
+
277
+ if (isMacroObject(macroOrFn)) {
278
+ fn = macroOrFn.exec;
279
+ args = restArgs;
280
+ if (macroOrFn.title) {
281
+ name = macroOrFn.title(titleOrMacroOrFn, ...restArgs);
282
+ }
283
+ } else if (isFunction(macroOrFn)) {
284
+ fn = macroOrFn;
285
+ args = restArgs;
286
+ } else {
287
+ fn = () => {};
288
+ }
289
+ } else if (isMacroObject(titleOrMacroOrFn)) {
290
+ fn = titleOrMacroOrFn.exec;
291
+ args = rest;
292
+ name = titleOrMacroOrFn.title
293
+ ? titleOrMacroOrFn.title(undefined, ...rest)
294
+ : fn.name || 'unnamed test';
295
+ } else if (isFunction(titleOrMacroOrFn)) {
296
+ fn = titleOrMacroOrFn;
297
+ args = rest;
298
+ name = fn.name || 'unnamed test';
299
+ } else {
300
+ name = 'unnamed test';
301
+ fn = () => {};
302
+ }
303
+
304
+ const wrappedFn = async () => {
305
+ const ctx = createMockContext();
306
+ await fn(ctx, ...args);
307
+ };
308
+
309
+ return { name, wrappedFn };
310
+ };
311
+
312
+ /**
313
+ * Process a test registration, handling all AVA calling conventions:
314
+ *
315
+ * - Test('title', fn)
316
+ * - Test('title', macro, ...args)
317
+ * - Test(macro, ...args)
318
+ * - Test(fn)
319
+ */
320
+ const registerTest = (
321
+ titleOrMacroOrFn: AvaMacro | AvaTestFn | string,
322
+ ...rest: unknown[]
323
+ ): void => {
324
+ const { name, wrappedFn } = parseTestArgs(titleOrMacroOrFn, rest);
325
+ state.tests.push({
326
+ fn: wrappedFn as unknown as (
327
+ t: AvaExecutionContext,
328
+ ) => Promise<void> | void,
329
+ name,
330
+ });
331
+ };
332
+
333
+ // Create mock test function
334
+ const mockTest = Object.assign(registerTest, {
335
+ // test.after() - runs once after all tests
336
+ after: Object.assign(
337
+ (fn: AvaTestFn): void => {
338
+ const wrappedFn = async () => {
339
+ const ctx = createMockContext();
340
+ await fn(ctx);
341
+ };
342
+ state.hooks.after.push(wrappedFn);
343
+ },
344
+ {
345
+ // test.after.always() - runs even if tests fail
346
+ always: function always(fn: AvaTestFn): void {
347
+ const wrappedFn = async () => {
348
+ const ctx = createMockContext();
349
+ await fn(ctx);
350
+ };
351
+ state.hooks.afterAlways.push(wrappedFn);
352
+ },
353
+ },
354
+ ),
355
+
356
+ // test.afterEach() - runs after each test
357
+ afterEach: Object.assign(
358
+ (fn: AvaTestFn): void => {
359
+ const wrappedFn = async () => {
360
+ const ctx = createMockContext();
361
+ await fn(ctx);
362
+ };
363
+ state.hooks.afterEach.push(wrappedFn);
364
+ },
365
+ {
366
+ // test.afterEach.always() - runs even if test fails
367
+ always: function always(fn: AvaTestFn): void {
368
+ const wrappedFn = async () => {
369
+ const ctx = createMockContext();
370
+ await fn(ctx);
371
+ };
372
+ state.hooks.afterEachAlways.push(wrappedFn);
373
+ },
374
+ },
375
+ ),
376
+
377
+ // test.before() - runs once before all tests
378
+ before: function before(fn: AvaTestFn): void {
379
+ const wrappedFn = async () => {
380
+ const ctx = createMockContext();
381
+ await fn(ctx);
382
+ };
383
+ state.hooks.before.push(wrappedFn);
384
+ },
385
+
386
+ // test.beforeEach() - runs before each test
387
+ beforeEach: function beforeEach(fn: AvaTestFn): void {
388
+ const wrappedFn = async () => {
389
+ const ctx = createMockContext();
390
+ await fn(ctx);
391
+ };
392
+ state.hooks.beforeEach.push(wrappedFn);
393
+ },
394
+
395
+ // test.failing() - expected to fail (treat as regular for benchmarks)
396
+ failing: function failing(
397
+ titleOrMacro: AvaTestFn | string,
398
+ implementation?: AvaTestFn,
399
+ ): void {
400
+ mockTest(titleOrMacro, implementation);
401
+ },
402
+
403
+ // test.macro() - create reusable test macro
404
+ // Accepts either a function or an object with exec/title
405
+ macro: function macro<Args extends unknown[]>(
406
+ fnOrObject:
407
+ | ((t: AvaExecutionContext, ...args: Args) => Promise<void> | void)
408
+ | {
409
+ exec: (
410
+ t: AvaExecutionContext,
411
+ ...args: Args
412
+ ) => Promise<void> | void;
413
+ title?: (
414
+ providedTitle: string | undefined,
415
+ ...args: Args
416
+ ) => string;
417
+ },
418
+ ): AvaMacro | AvaTestFn {
419
+ if (typeof fnOrObject === 'function') {
420
+ // Simple function macro - return as-is
421
+ return fnOrObject as AvaTestFn;
422
+ }
423
+ // Object macro with exec/title - return as AvaMacro
424
+ return {
425
+ exec: fnOrObject.exec as AvaTestFn,
426
+ title: fnOrObject.title as AvaMacro['title'],
427
+ };
428
+ },
429
+
430
+ // test.only() - marks test as exclusive
431
+ only: function only(
432
+ titleOrMacroOrFn: AvaMacro | AvaTestFn | string,
433
+ ...rest: unknown[]
434
+ ): void {
435
+ // Reuse registerTest logic but add only flag
436
+ const { name, wrappedFn } = parseTestArgs(titleOrMacroOrFn, rest);
437
+ state.tests.push({
438
+ fn: wrappedFn,
439
+ name,
440
+ only: true,
441
+ });
442
+ },
443
+
444
+ // test.serial() - run serially (we capture but ignore serial flag for benchmarks)
445
+ serial: function serial(
446
+ titleOrMacroOrFn: AvaMacro | AvaTestFn | string,
447
+ ...rest: unknown[]
448
+ ): void {
449
+ const { name, wrappedFn } = parseTestArgs(titleOrMacroOrFn, rest);
450
+ state.tests.push({
451
+ fn: wrappedFn,
452
+ name,
453
+ serial: true,
454
+ });
455
+ },
456
+
457
+ // test.skip() - marks test as skipped
458
+ skip: function skip(
459
+ titleOrMacroOrFn: AvaMacro | AvaTestFn | string,
460
+ ...rest: unknown[]
461
+ ): void {
462
+ const { name, wrappedFn } = parseTestArgs(titleOrMacroOrFn, rest);
463
+ state.tests.push({
464
+ fn: wrappedFn,
465
+ name,
466
+ skip: true,
467
+ });
468
+ },
469
+
470
+ // test.todo() - placeholder test
471
+ todo: function todo(title: string): void {
472
+ state.tests.push({
473
+ fn: () => {},
474
+ name: title,
475
+ skip: true,
476
+ });
477
+ },
478
+ });
479
+
480
+ // Install on globalThis for the loader to access
481
+ // @ts-expect-error - intentionally using globalThis
482
+ globalThis.__MODESTBENCH_AVA_MOCK__ = {
483
+ default: mockTest,
484
+ test: mockTest,
485
+ };
486
+ };
487
+
488
+ /**
489
+ * Convert capture state to CapturedTestFile
490
+ *
491
+ * Since AVA doesn't have suites, we create a synthetic "default" suite to hold
492
+ * the file-level hooks and tests.
493
+ */
494
+ const toCapturedTestFile = (
495
+ state: CaptureState,
496
+ filePath: string,
497
+ ): CapturedTestFile => {
498
+ // Combine after and afterAlways, afterEach and afterEachAlways
499
+ const combinedHooks: SuiteHooks = {
500
+ after: [...state.hooks.after, ...state.hooks.afterAlways],
501
+ afterEach: [...state.hooks.afterEach, ...state.hooks.afterEachAlways],
502
+ before: state.hooks.before,
503
+ beforeEach: state.hooks.beforeEach,
504
+ };
505
+
506
+ // Convert tests
507
+ const tests: CapturedTest[] = state.tests.map((t) => ({
508
+ fn: t.fn as unknown as () => Promise<void> | void,
509
+ name: t.name,
510
+ only: t.only,
511
+ skip: t.skip,
512
+ }));
513
+
514
+ // If there are hooks, wrap them in a synthetic suite
515
+ const hasHooks =
516
+ combinedHooks.before.length > 0 ||
517
+ combinedHooks.after.length > 0 ||
518
+ combinedHooks.beforeEach.length > 0 ||
519
+ combinedHooks.afterEach.length > 0;
520
+
521
+ if (hasHooks) {
522
+ // Create a synthetic suite to hold hooks
523
+ const suite: CapturedSuite = {
524
+ children: [],
525
+ hooks: combinedHooks,
526
+ name: 'default',
527
+ tests,
528
+ };
529
+
530
+ return {
531
+ filePath,
532
+ framework: 'ava',
533
+ rootSuites: [suite],
534
+ rootTests: [],
535
+ };
536
+ }
537
+
538
+ // No hooks - just return flat tests
539
+ return {
540
+ filePath,
541
+ framework: 'ava',
542
+ rootSuites: [],
543
+ rootTests: tests,
544
+ };
545
+ };
546
+
547
+ /**
548
+ * Uninstall AVA mocks
549
+ */
550
+ const uninstallAvaMocks = (): void => {
551
+ // @ts-expect-error - cleaning up globalThis
552
+ delete globalThis.__MODESTBENCH_AVA_MOCK__;
553
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * ModestBench AVA Loader Hooks
3
+ *
4
+ * ES module loader hooks that intercept `ava` imports and return our capturing
5
+ * mock from globalThis.
6
+ *
7
+ * Usage: node --import modestbench/ava test-file.js
8
+ *
9
+ * This loader exports async `resolve` and `load` hooks that get registered via
10
+ * module.register() when imported through ava-register.ts.
11
+ */
12
+
13
+ import type { LoadHook, ResolveHook } from 'node:module';
14
+
15
+ /**
16
+ * Generate the mock module source code
17
+ *
18
+ * Uses top-level await to conditionally get mock or real module. Note: Uses
19
+ * 'ava?passthrough' to bypass our hook when falling back.
20
+ */
21
+ const generateMockSource = (): string => `
22
+ const mock = globalThis.__MODESTBENCH_AVA_MOCK__;
23
+
24
+ // If no mock installed, fall through to real ava
25
+ // The '?passthrough' query tells our hook to not intercept this import
26
+ const source = mock ?? await import('ava?passthrough');
27
+
28
+ export const test = source.test ?? source.default;
29
+ export default source.default ?? source.test;
30
+ `;
31
+
32
+ /**
33
+ * Resolve hook - intercepts ava specifier
34
+ *
35
+ * Uses query param '?passthrough' to prevent infinite recursion when falling
36
+ * back to real ava (when no mock is installed).
37
+ */
38
+ export const resolve: ResolveHook = async (specifier, context, nextResolve) => {
39
+ // Only intercept bare 'ava', not 'ava?passthrough'
40
+ if (specifier === 'ava') {
41
+ return {
42
+ shortCircuit: true,
43
+ url: 'modestbench://capture/ava',
44
+ };
45
+ }
46
+ // Strip passthrough query to resolve real ava
47
+ if (specifier === 'ava?passthrough') {
48
+ return nextResolve('ava', context);
49
+ }
50
+ return nextResolve(specifier, context);
51
+ };
52
+
53
+ /**
54
+ * Load hook - returns mock module for our custom URL
55
+ */
56
+ export const load: LoadHook = async (url, context, nextLoad) => {
57
+ if (url === 'modestbench://capture/ava') {
58
+ return {
59
+ format: 'module',
60
+ shortCircuit: true,
61
+ source: generateMockSource(),
62
+ };
63
+ }
64
+ return nextLoad(url, context);
65
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * ModestBench AVA Loader Registration
3
+ *
4
+ * Registers the AVA ESM loader hooks via module.register().
5
+ *
6
+ * Usage: node --import modestbench/ava your-test.js
7
+ *
8
+ * This file registers the hooks module which intercepts 'ava' imports.
9
+ */
10
+
11
+ import { register } from 'node:module';
12
+
13
+ register('./ava-hooks.js', {
14
+ parentURL: import.meta.url,
15
+ });