modestbench 0.3.2 → 0.5.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.
- package/CHANGELOG.md +44 -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/core/stats-utils.cjs +4 -2
- package/dist/core/stats-utils.cjs.map +1 -1
- package/dist/core/stats-utils.d.cts +1 -1
- package/dist/core/stats-utils.d.cts.map +1 -1
- package/dist/core/stats-utils.d.ts +1 -1
- package/dist/core/stats-utils.d.ts.map +1 -1
- package/dist/core/stats-utils.js +4 -2
- package/dist/core/stats-utils.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/formatters/history/compare.cjs.map +1 -1
- package/dist/formatters/history/compare.d.cts.map +1 -1
- package/dist/formatters/history/compare.d.ts.map +1 -1
- package/dist/formatters/history/compare.js.map +1 -1
- package/dist/formatters/history/list.cjs.map +1 -1
- package/dist/formatters/history/list.d.cts.map +1 -1
- package/dist/formatters/history/list.d.ts.map +1 -1
- package/dist/formatters/history/list.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.d.cts.map +1 -1
- package/dist/services/config-manager.d.ts.map +1 -1
- package/dist/services/config-manager.js +2 -2
- package/dist/services/config-manager.js.map +1 -1
- package/dist/services/file-loader.cjs.map +1 -1
- package/dist/services/file-loader.d.cts.map +1 -1
- package/dist/services/file-loader.d.ts.map +1 -1
- package/dist/services/file-loader.js.map +1 -1
- package/package.json +63 -35
- 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/core/stats-utils.ts +5 -3
- package/src/errors/base.ts +3 -2
- package/src/formatters/history/compare.ts +1 -3
- package/src/formatters/history/list.ts +1 -3
- package/src/reporters/human.ts +107 -36
- package/src/reporters/simple.ts +81 -22
- package/src/services/config-manager.ts +4 -5
- package/src/services/file-loader.ts +2 -3
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModestBench Mocha Adapter
|
|
3
|
+
*
|
|
4
|
+
* Captures test definitions from Mocha test files by replacing global
|
|
5
|
+
* `describe`, `it`, and hook functions before the test file loads.
|
|
6
|
+
*
|
|
7
|
+
* Mocha exposes these as globals, making interception straightforward - no ES
|
|
8
|
+
* module loader hooks required.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { pathToFileURL } from 'node:url';
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
CapturedSuite,
|
|
15
|
+
CapturedTest,
|
|
16
|
+
CapturedTestFile,
|
|
17
|
+
SuiteHooks,
|
|
18
|
+
TestFrameworkAdapter,
|
|
19
|
+
} from './types.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Internal state for capturing test definitions
|
|
23
|
+
*/
|
|
24
|
+
interface CaptureState {
|
|
25
|
+
/** Stack of suite contexts for nested describes */
|
|
26
|
+
currentSuite: CapturedSuite | null;
|
|
27
|
+
/** Root-level suites */
|
|
28
|
+
rootSuites: CapturedSuite[];
|
|
29
|
+
/** Root-level tests (if any, though Mocha usually uses describe) */
|
|
30
|
+
rootTests: CapturedTest[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Mocha-style describe function signature
|
|
35
|
+
*/
|
|
36
|
+
type DescribeFn = {
|
|
37
|
+
(name: string, fn: () => void): void;
|
|
38
|
+
only: (name: string, fn: () => void) => void;
|
|
39
|
+
skip: (name: string, fn: () => void) => void;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Mocha-style hook function signature
|
|
44
|
+
*/
|
|
45
|
+
type HookFn = (fn: () => Promise<void> | void) => void;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Mocha-style it function signature
|
|
49
|
+
*/
|
|
50
|
+
type ItFn = {
|
|
51
|
+
(name: string, fn: () => Promise<void> | void): void;
|
|
52
|
+
only: (name: string, fn: () => Promise<void> | void) => void;
|
|
53
|
+
skip: (name: string, fn: () => Promise<void> | void) => void;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Mocha test framework adapter
|
|
58
|
+
*
|
|
59
|
+
* Captures tests by installing global mocks before importing the test file.
|
|
60
|
+
*/
|
|
61
|
+
export class MochaAdapter implements TestFrameworkAdapter {
|
|
62
|
+
readonly framework = 'mocha' as const;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Capture test definitions from a Mocha test file
|
|
66
|
+
*
|
|
67
|
+
* @param filePath - Absolute path to the test file
|
|
68
|
+
* @returns Captured test structure
|
|
69
|
+
*/
|
|
70
|
+
async capture(filePath: string): Promise<CapturedTestFile> {
|
|
71
|
+
// Install our mock globals
|
|
72
|
+
const state = installMochaGlobals();
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// Import the test file - this triggers describe/it calls
|
|
76
|
+
// which populate our state
|
|
77
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
78
|
+
|
|
79
|
+
// Use a cache-busting query param to ensure fresh import
|
|
80
|
+
const bustCache = `?t=${Date.now()}`;
|
|
81
|
+
await import(fileUrl + bustCache);
|
|
82
|
+
|
|
83
|
+
// Return captured structure
|
|
84
|
+
return {
|
|
85
|
+
filePath,
|
|
86
|
+
framework: 'mocha',
|
|
87
|
+
rootSuites: state.rootSuites,
|
|
88
|
+
rootTests: state.rootTests,
|
|
89
|
+
};
|
|
90
|
+
} finally {
|
|
91
|
+
// Clean up globals
|
|
92
|
+
uninstallMochaGlobals();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Create empty hooks structure
|
|
99
|
+
*/
|
|
100
|
+
const createEmptyHooks = (): SuiteHooks => {
|
|
101
|
+
return {
|
|
102
|
+
after: [],
|
|
103
|
+
afterEach: [],
|
|
104
|
+
before: [],
|
|
105
|
+
beforeEach: [],
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create a new suite structure
|
|
111
|
+
*/
|
|
112
|
+
const createSuite = (name: string): CapturedSuite => {
|
|
113
|
+
return {
|
|
114
|
+
children: [],
|
|
115
|
+
hooks: createEmptyHooks(),
|
|
116
|
+
name,
|
|
117
|
+
tests: [],
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Install Mocha global mocks and return the capture state
|
|
123
|
+
*
|
|
124
|
+
* This replaces globalThis.describe, globalThis.it, etc. with our capturing
|
|
125
|
+
* implementations.
|
|
126
|
+
*/
|
|
127
|
+
const installMochaGlobals = (): CaptureState => {
|
|
128
|
+
const state: CaptureState = {
|
|
129
|
+
currentSuite: null,
|
|
130
|
+
rootSuites: [],
|
|
131
|
+
rootTests: [],
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// describe() - creates a suite
|
|
135
|
+
const describe: DescribeFn = (name: string, fn: () => void) => {
|
|
136
|
+
const suite = createSuite(name);
|
|
137
|
+
const parent = state.currentSuite;
|
|
138
|
+
|
|
139
|
+
if (parent) {
|
|
140
|
+
// Nested describe: inherit beforeEach/afterEach from parent
|
|
141
|
+
// and add to parent's children
|
|
142
|
+
parent.children.push(suite);
|
|
143
|
+
} else {
|
|
144
|
+
// Root-level describe
|
|
145
|
+
state.rootSuites.push(suite);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Enter this suite context
|
|
149
|
+
state.currentSuite = suite;
|
|
150
|
+
|
|
151
|
+
// Execute the describe body to capture tests and nested describes
|
|
152
|
+
fn();
|
|
153
|
+
|
|
154
|
+
// Exit back to parent context
|
|
155
|
+
state.currentSuite = parent;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// describe.only() - marks suite as exclusive
|
|
159
|
+
describe.only = (name: string, fn: () => void) => {
|
|
160
|
+
// For benchmarking, we treat .only the same as regular
|
|
161
|
+
// (filtering happens at a higher level)
|
|
162
|
+
describe(name, fn);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// describe.skip() - marks suite as skipped
|
|
166
|
+
describe.skip = (_name: string, _fn: () => void) => {
|
|
167
|
+
// Skip entirely - don't even register
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// it() - creates a test
|
|
171
|
+
const it: ItFn = (name: string, fn: () => Promise<void> | void) => {
|
|
172
|
+
const test: CapturedTest = { fn, name };
|
|
173
|
+
|
|
174
|
+
if (state.currentSuite) {
|
|
175
|
+
state.currentSuite.tests.push(test);
|
|
176
|
+
} else {
|
|
177
|
+
// Root-level test (unusual for Mocha but supported)
|
|
178
|
+
state.rootTests.push(test);
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// it.only() - marks test as exclusive
|
|
183
|
+
it.only = (name: string, fn: () => Promise<void> | void) => {
|
|
184
|
+
const test: CapturedTest = { fn, name, only: true };
|
|
185
|
+
|
|
186
|
+
if (state.currentSuite) {
|
|
187
|
+
state.currentSuite.tests.push(test);
|
|
188
|
+
} else {
|
|
189
|
+
state.rootTests.push(test);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// it.skip() - marks test as skipped
|
|
194
|
+
it.skip = (name: string, fn: () => Promise<void> | void) => {
|
|
195
|
+
const test: CapturedTest = { fn, name, skip: true };
|
|
196
|
+
|
|
197
|
+
if (state.currentSuite) {
|
|
198
|
+
state.currentSuite.tests.push(test);
|
|
199
|
+
} else {
|
|
200
|
+
state.rootTests.push(test);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// before() - runs once before all tests in suite
|
|
205
|
+
const before: HookFn = (fn: () => Promise<void> | void) => {
|
|
206
|
+
if (state.currentSuite) {
|
|
207
|
+
(
|
|
208
|
+
state.currentSuite.hooks.before as Array<() => Promise<void> | void>
|
|
209
|
+
).push(fn);
|
|
210
|
+
}
|
|
211
|
+
// Root-level before is ignored (no suite to attach to)
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
// after() - runs once after all tests in suite
|
|
215
|
+
const after: HookFn = (fn: () => Promise<void> | void) => {
|
|
216
|
+
if (state.currentSuite) {
|
|
217
|
+
(
|
|
218
|
+
state.currentSuite.hooks.after as Array<() => Promise<void> | void>
|
|
219
|
+
).push(fn);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// beforeEach() - runs before each test
|
|
224
|
+
const beforeEach: HookFn = (fn: () => Promise<void> | void) => {
|
|
225
|
+
if (state.currentSuite) {
|
|
226
|
+
(
|
|
227
|
+
state.currentSuite.hooks.beforeEach as Array<() => Promise<void> | void>
|
|
228
|
+
).push(fn);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// afterEach() - runs after each test
|
|
233
|
+
const afterEach: HookFn = (fn: () => Promise<void> | void) => {
|
|
234
|
+
if (state.currentSuite) {
|
|
235
|
+
(
|
|
236
|
+
state.currentSuite.hooks.afterEach as Array<() => Promise<void> | void>
|
|
237
|
+
).push(fn);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Install globals
|
|
242
|
+
// @ts-expect-error - intentionally modifying globalThis
|
|
243
|
+
globalThis.describe = describe;
|
|
244
|
+
// @ts-expect-error - intentionally modifying globalThis
|
|
245
|
+
globalThis.it = it;
|
|
246
|
+
// @ts-expect-error - intentionally modifying globalThis
|
|
247
|
+
globalThis.before = before;
|
|
248
|
+
// @ts-expect-error - intentionally modifying globalThis
|
|
249
|
+
globalThis.after = after;
|
|
250
|
+
// @ts-expect-error - intentionally modifying globalThis
|
|
251
|
+
globalThis.beforeEach = beforeEach;
|
|
252
|
+
// @ts-expect-error - intentionally modifying globalThis
|
|
253
|
+
globalThis.afterEach = afterEach;
|
|
254
|
+
|
|
255
|
+
// Mocha aliases
|
|
256
|
+
// @ts-expect-error - intentionally modifying globalThis
|
|
257
|
+
globalThis.context = describe;
|
|
258
|
+
// @ts-expect-error - intentionally modifying globalThis
|
|
259
|
+
globalThis.specify = it;
|
|
260
|
+
|
|
261
|
+
return state;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Remove Mocha globals installed by installMochaGlobals
|
|
266
|
+
*/
|
|
267
|
+
const uninstallMochaGlobals = (): void => {
|
|
268
|
+
// @ts-expect-error - cleaning up globalThis
|
|
269
|
+
delete globalThis.describe;
|
|
270
|
+
// @ts-expect-error - cleaning up globalThis
|
|
271
|
+
delete globalThis.it;
|
|
272
|
+
// @ts-expect-error - cleaning up globalThis
|
|
273
|
+
delete globalThis.before;
|
|
274
|
+
// @ts-expect-error - cleaning up globalThis
|
|
275
|
+
delete globalThis.after;
|
|
276
|
+
// @ts-expect-error - cleaning up globalThis
|
|
277
|
+
delete globalThis.beforeEach;
|
|
278
|
+
// @ts-expect-error - cleaning up globalThis
|
|
279
|
+
delete globalThis.afterEach;
|
|
280
|
+
// @ts-expect-error - cleaning up globalThis
|
|
281
|
+
delete globalThis.context;
|
|
282
|
+
// @ts-expect-error - cleaning up globalThis
|
|
283
|
+
delete globalThis.specify;
|
|
284
|
+
};
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModestBench Node.js Test Runner Adapter
|
|
3
|
+
*
|
|
4
|
+
* Captures test definitions from node:test files using ES module loader hooks.
|
|
5
|
+
*
|
|
6
|
+
* Unlike Mocha (which uses globals), node:test requires ES module import
|
|
7
|
+
* interception. We use Node.js module.register() API to install a custom loader
|
|
8
|
+
* that returns our capturing mock instead of the real node:test.
|
|
9
|
+
*
|
|
10
|
+
* Architecture:
|
|
11
|
+
*
|
|
12
|
+
* 1. Register a custom loader that intercepts node:test imports
|
|
13
|
+
* 2. Import the test file - this triggers test/describe calls
|
|
14
|
+
* 3. Retrieve captured state from global storage
|
|
15
|
+
* 4. Unregister the loader
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { pathToFileURL } from 'node:url';
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
CapturedSuite,
|
|
22
|
+
CapturedTest,
|
|
23
|
+
CapturedTestFile,
|
|
24
|
+
SuiteHooks,
|
|
25
|
+
TestFrameworkAdapter,
|
|
26
|
+
} from './types.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Global capture state key
|
|
30
|
+
*/
|
|
31
|
+
const CAPTURE_STATE_KEY = '__MODESTBENCH_NODE_TEST_CAPTURE__';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Internal capture state structure
|
|
35
|
+
*/
|
|
36
|
+
interface CaptureState {
|
|
37
|
+
currentSuite: MutableCapturedSuite | null;
|
|
38
|
+
rootSuites: MutableCapturedSuite[];
|
|
39
|
+
rootTests: MutableCapturedTest[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Mutable version of CapturedSuite for building state
|
|
44
|
+
*/
|
|
45
|
+
interface MutableCapturedSuite {
|
|
46
|
+
children: MutableCapturedSuite[];
|
|
47
|
+
hooks: MutableSuiteHooks;
|
|
48
|
+
name: string;
|
|
49
|
+
tests: MutableCapturedTest[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Mutable version of CapturedTest
|
|
54
|
+
*/
|
|
55
|
+
interface MutableCapturedTest {
|
|
56
|
+
fn: () => Promise<void> | void;
|
|
57
|
+
name: string;
|
|
58
|
+
only?: boolean;
|
|
59
|
+
skip?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Mutable version of SuiteHooks
|
|
64
|
+
*/
|
|
65
|
+
interface MutableSuiteHooks {
|
|
66
|
+
after: Array<() => Promise<void> | void>;
|
|
67
|
+
afterEach: Array<() => Promise<void> | void>;
|
|
68
|
+
before: Array<() => Promise<void> | void>;
|
|
69
|
+
beforeEach: Array<() => Promise<void> | void>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Node.js test runner adapter
|
|
74
|
+
*
|
|
75
|
+
* Captures test definitions by installing mock implementations before importing
|
|
76
|
+
* the test file.
|
|
77
|
+
*
|
|
78
|
+
* IMPORTANT: For this to work with actual node:test imports, you must run
|
|
79
|
+
* Node.js with our loader: node --import modestbench/node-test your-test.js
|
|
80
|
+
*
|
|
81
|
+
* Without the loader, this adapter only works with test files that use
|
|
82
|
+
* globalThis.**MODESTBENCH_NODE_TEST_MOCK** directly (not useful for real test
|
|
83
|
+
* files).
|
|
84
|
+
*/
|
|
85
|
+
export class NodeTestAdapter implements TestFrameworkAdapter {
|
|
86
|
+
readonly framework = 'node-test' as const;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Capture test definitions from a node:test file
|
|
90
|
+
*
|
|
91
|
+
* @param filePath - Absolute path to the test file
|
|
92
|
+
* @returns Captured test structure
|
|
93
|
+
*/
|
|
94
|
+
async capture(filePath: string): Promise<CapturedTestFile> {
|
|
95
|
+
// Initialize capture state
|
|
96
|
+
const state = initCaptureState();
|
|
97
|
+
|
|
98
|
+
// Install mocks
|
|
99
|
+
installNodeTestMocks(state);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
// Import the test file
|
|
103
|
+
// The loader hook will intercept 'node:test' and use our mocks
|
|
104
|
+
const fileUrl = pathToFileURL(filePath).href;
|
|
105
|
+
const bustCache = `?t=${Date.now()}`;
|
|
106
|
+
await import(fileUrl + bustCache);
|
|
107
|
+
|
|
108
|
+
// Return captured structure
|
|
109
|
+
return toCapturedTestFile(state, filePath);
|
|
110
|
+
} finally {
|
|
111
|
+
// Clean up
|
|
112
|
+
uninstallNodeTestMocks();
|
|
113
|
+
clearCaptureState();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Clear capture state from globalThis
|
|
120
|
+
*/
|
|
121
|
+
const clearCaptureState = (): void => {
|
|
122
|
+
// @ts-expect-error - intentionally using globalThis
|
|
123
|
+
delete globalThis[CAPTURE_STATE_KEY];
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Create empty hooks structure
|
|
128
|
+
*/
|
|
129
|
+
const createEmptyHooks = (): MutableSuiteHooks => {
|
|
130
|
+
return {
|
|
131
|
+
after: [],
|
|
132
|
+
afterEach: [],
|
|
133
|
+
before: [],
|
|
134
|
+
beforeEach: [],
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Create a new suite structure
|
|
140
|
+
*/
|
|
141
|
+
const createSuite = (name: string): MutableCapturedSuite => {
|
|
142
|
+
return {
|
|
143
|
+
children: [],
|
|
144
|
+
hooks: createEmptyHooks(),
|
|
145
|
+
name,
|
|
146
|
+
tests: [],
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Initialize capture state on globalThis
|
|
152
|
+
*/
|
|
153
|
+
const initCaptureState = (): CaptureState => {
|
|
154
|
+
const state: CaptureState = {
|
|
155
|
+
currentSuite: null,
|
|
156
|
+
rootSuites: [],
|
|
157
|
+
rootTests: [],
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// @ts-expect-error - intentionally using globalThis for cross-module state
|
|
161
|
+
globalThis[CAPTURE_STATE_KEY] = state;
|
|
162
|
+
|
|
163
|
+
return state;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Install node:test mocks on globalThis for module interception
|
|
168
|
+
*
|
|
169
|
+
* Since we can't truly intercept ES imports without a loader, we use a
|
|
170
|
+
* workaround: install mocks that test files can import via a special path.
|
|
171
|
+
*
|
|
172
|
+
* For true node:test interception, users must run with: node --import
|
|
173
|
+
* modestbench/register test-file.js
|
|
174
|
+
*/
|
|
175
|
+
const installNodeTestMocks = (state: CaptureState): void => {
|
|
176
|
+
// Create mock test function
|
|
177
|
+
const mockTest = Object.assign(
|
|
178
|
+
(
|
|
179
|
+
nameOrOptions: string | { name?: string; only?: boolean; skip?: boolean },
|
|
180
|
+
fnOrOptions?:
|
|
181
|
+
| (() => Promise<void> | void)
|
|
182
|
+
| { only?: boolean; skip?: boolean },
|
|
183
|
+
maybeFn?: () => Promise<void> | void,
|
|
184
|
+
): Promise<void> => {
|
|
185
|
+
let name: string;
|
|
186
|
+
let fn: () => Promise<void> | void;
|
|
187
|
+
let skip = false;
|
|
188
|
+
let only = false;
|
|
189
|
+
|
|
190
|
+
// Parse arguments - node:test has flexible signatures
|
|
191
|
+
if (typeof nameOrOptions === 'string') {
|
|
192
|
+
name = nameOrOptions;
|
|
193
|
+
if (typeof fnOrOptions === 'function') {
|
|
194
|
+
fn = fnOrOptions;
|
|
195
|
+
} else if (fnOrOptions && typeof fnOrOptions === 'object') {
|
|
196
|
+
skip = fnOrOptions.skip ?? false;
|
|
197
|
+
only = fnOrOptions.only ?? false;
|
|
198
|
+
fn = maybeFn ?? (() => {});
|
|
199
|
+
} else {
|
|
200
|
+
fn = () => {};
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
name = nameOrOptions.name ?? 'unnamed test';
|
|
204
|
+
skip = nameOrOptions.skip ?? false;
|
|
205
|
+
only = nameOrOptions.only ?? false;
|
|
206
|
+
fn = (fnOrOptions as () => Promise<void> | void) ?? (() => {});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const test: MutableCapturedTest = { fn, name, only, skip };
|
|
210
|
+
|
|
211
|
+
if (state.currentSuite) {
|
|
212
|
+
state.currentSuite.tests.push(test);
|
|
213
|
+
} else {
|
|
214
|
+
state.rootTests.push(test);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return Promise.resolve();
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
only: function only(
|
|
221
|
+
name: string,
|
|
222
|
+
fn?: () => Promise<void> | void,
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
return mockTest(name, { only: true }, fn);
|
|
225
|
+
},
|
|
226
|
+
skip: function skip(
|
|
227
|
+
name: string,
|
|
228
|
+
fn?: () => Promise<void> | void,
|
|
229
|
+
): Promise<void> {
|
|
230
|
+
return mockTest(name, { skip: true }, fn);
|
|
231
|
+
},
|
|
232
|
+
todo: function todo(
|
|
233
|
+
name: string,
|
|
234
|
+
fn?: () => Promise<void> | void,
|
|
235
|
+
): Promise<void> {
|
|
236
|
+
return mockTest(name, { skip: true }, fn);
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
// Create mock describe function
|
|
242
|
+
const mockDescribe = Object.assign(
|
|
243
|
+
(
|
|
244
|
+
nameOrOptions: string | { name?: string; only?: boolean; skip?: boolean },
|
|
245
|
+
fnOrOptions?: (() => void) | { only?: boolean; skip?: boolean },
|
|
246
|
+
maybeFn?: () => void,
|
|
247
|
+
): Promise<void> => {
|
|
248
|
+
let name: string;
|
|
249
|
+
let fn: (() => void) | undefined;
|
|
250
|
+
|
|
251
|
+
if (typeof nameOrOptions === 'string') {
|
|
252
|
+
name = nameOrOptions;
|
|
253
|
+
if (typeof fnOrOptions === 'function') {
|
|
254
|
+
fn = fnOrOptions;
|
|
255
|
+
} else {
|
|
256
|
+
fn = maybeFn;
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
name = nameOrOptions.name ?? 'unnamed suite';
|
|
260
|
+
fn = fnOrOptions as (() => void) | undefined;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const suite = createSuite(name);
|
|
264
|
+
const parent = state.currentSuite;
|
|
265
|
+
|
|
266
|
+
if (parent) {
|
|
267
|
+
parent.children.push(suite);
|
|
268
|
+
} else {
|
|
269
|
+
state.rootSuites.push(suite);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Enter suite context
|
|
273
|
+
state.currentSuite = suite;
|
|
274
|
+
|
|
275
|
+
// Execute describe body
|
|
276
|
+
if (fn) {
|
|
277
|
+
fn();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Exit back to parent
|
|
281
|
+
state.currentSuite = parent;
|
|
282
|
+
|
|
283
|
+
return Promise.resolve();
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
only: function only(name: string, fn?: () => void): Promise<void> {
|
|
287
|
+
return mockDescribe(name, fn);
|
|
288
|
+
},
|
|
289
|
+
skip: function skip(_name: string, _fn?: () => void): Promise<void> {
|
|
290
|
+
return Promise.resolve();
|
|
291
|
+
},
|
|
292
|
+
todo: function todo(_name: string, _fn?: () => void): Promise<void> {
|
|
293
|
+
return Promise.resolve();
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// Hook functions
|
|
299
|
+
const mockBefore = (fn: () => Promise<void> | void): void => {
|
|
300
|
+
if (state.currentSuite) {
|
|
301
|
+
state.currentSuite.hooks.before.push(fn);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const mockAfter = (fn: () => Promise<void> | void): void => {
|
|
306
|
+
if (state.currentSuite) {
|
|
307
|
+
state.currentSuite.hooks.after.push(fn);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const mockBeforeEach = (fn: () => Promise<void> | void): void => {
|
|
312
|
+
if (state.currentSuite) {
|
|
313
|
+
state.currentSuite.hooks.beforeEach.push(fn);
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const mockAfterEach = (fn: () => Promise<void> | void): void => {
|
|
318
|
+
if (state.currentSuite) {
|
|
319
|
+
state.currentSuite.hooks.afterEach.push(fn);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Install on globalThis for the loader to access
|
|
324
|
+
// @ts-expect-error - intentionally using globalThis
|
|
325
|
+
globalThis.__MODESTBENCH_NODE_TEST_MOCK__ = {
|
|
326
|
+
after: mockAfter,
|
|
327
|
+
afterEach: mockAfterEach,
|
|
328
|
+
before: mockBefore,
|
|
329
|
+
beforeEach: mockBeforeEach,
|
|
330
|
+
default: mockTest,
|
|
331
|
+
describe: mockDescribe,
|
|
332
|
+
it: mockTest,
|
|
333
|
+
mock: {
|
|
334
|
+
fn: () => () => {},
|
|
335
|
+
getter: () => {},
|
|
336
|
+
method: () => {},
|
|
337
|
+
reset: () => {},
|
|
338
|
+
restoreAll: () => {},
|
|
339
|
+
setter: () => {},
|
|
340
|
+
timers: {
|
|
341
|
+
enable: () => {},
|
|
342
|
+
reset: () => {},
|
|
343
|
+
runAll: () => {},
|
|
344
|
+
tick: () => {},
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
suite: mockDescribe,
|
|
348
|
+
test: mockTest,
|
|
349
|
+
};
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Convert mutable capture state to immutable CapturedTestFile
|
|
354
|
+
*/
|
|
355
|
+
const toCapturedTestFile = (
|
|
356
|
+
state: CaptureState,
|
|
357
|
+
filePath: string,
|
|
358
|
+
): CapturedTestFile => {
|
|
359
|
+
return {
|
|
360
|
+
filePath,
|
|
361
|
+
framework: 'node-test',
|
|
362
|
+
rootSuites: state.rootSuites.map(toImmutableSuite),
|
|
363
|
+
rootTests: state.rootTests.map(toImmutableTest),
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const toImmutableSuite = (suite: MutableCapturedSuite): CapturedSuite => {
|
|
368
|
+
return {
|
|
369
|
+
children: suite.children.map(toImmutableSuite),
|
|
370
|
+
hooks: suite.hooks as SuiteHooks,
|
|
371
|
+
name: suite.name,
|
|
372
|
+
tests: suite.tests.map(toImmutableTest),
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const toImmutableTest = (test: MutableCapturedTest): CapturedTest => {
|
|
377
|
+
return {
|
|
378
|
+
fn: test.fn,
|
|
379
|
+
name: test.name,
|
|
380
|
+
only: test.only,
|
|
381
|
+
skip: test.skip,
|
|
382
|
+
};
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Uninstall node:test mocks
|
|
387
|
+
*/
|
|
388
|
+
const uninstallNodeTestMocks = (): void => {
|
|
389
|
+
// @ts-expect-error - cleaning up globalThis
|
|
390
|
+
delete globalThis.__MODESTBENCH_NODE_TEST_MOCK__;
|
|
391
|
+
};
|