@grest-ts/testkit 0.0.5 → 0.0.7
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/LICENSE +21 -21
- package/README.md +418 -413
- package/dist/src/runner/isolated-loader.mjs +91 -91
- package/dist/src/runner/worker-loader.mjs +49 -49
- package/dist/tsconfig.publish.tsbuildinfo +1 -1
- package/package.json +12 -12
- package/src/GGBundleTest.ts +89 -89
- package/src/GGTest.ts +318 -318
- package/src/GGTestContext.ts +74 -74
- package/src/GGTestRunner.ts +308 -308
- package/src/GGTestRuntime.ts +265 -265
- package/src/GGTestRuntimeWorker.ts +159 -159
- package/src/GGTestSharedRef.ts +116 -116
- package/src/GGTestkitExtensionsDiscovery.ts +26 -26
- package/src/IGGLocalDiscoveryServer.ts +16 -16
- package/src/callOn/GGCallOnSelector.ts +61 -61
- package/src/callOn/GGContractClass.implement.ts +43 -43
- package/src/callOn/GGTestActionForLocatorOnCall.ts +134 -134
- package/src/callOn/TestableIPC.ts +81 -81
- package/src/callOn/callOn.ts +224 -224
- package/src/callOn/registerOnCallHandler.ts +123 -123
- package/src/index-node.ts +64 -64
- package/src/mockable/GGMockable.ts +22 -22
- package/src/mockable/GGMockableCall.ts +45 -45
- package/src/mockable/GGMockableIPC.ts +20 -20
- package/src/mockable/GGMockableInterceptor.ts +44 -44
- package/src/mockable/GGMockableInterceptorsServer.ts +69 -69
- package/src/mockable/mockable.ts +71 -71
- package/src/runner/InlineRunner.ts +47 -47
- package/src/runner/IsolatedRunner.ts +179 -179
- package/src/runner/RuntimeRunner.ts +15 -15
- package/src/runner/WorkerRunner.ts +179 -179
- package/src/runner/isolated-loader.mjs +91 -91
- package/src/runner/worker-loader.mjs +49 -49
- package/src/testers/GGCallInterceptor.ts +224 -224
- package/src/testers/GGMockWith.ts +92 -92
- package/src/testers/GGSpyWith.ts +115 -115
- package/src/testers/GGTestAction.ts +332 -332
- package/src/testers/GGTestComponent.ts +16 -16
- package/src/testers/GGTestSelector.ts +223 -223
- package/src/testers/IGGTestInterceptor.ts +10 -10
- package/src/testers/IGGTestWith.ts +15 -15
- package/src/testers/RuntimeSelector.ts +151 -151
- package/src/utils/GGExpectations.ts +78 -78
- package/src/utils/GGTestError.ts +36 -36
- package/src/utils/captureStack.ts +53 -53
package/src/GGTestRunner.ts
CHANGED
|
@@ -1,308 +1,308 @@
|
|
|
1
|
-
import {GGTestRuntime} from "./GGTestRuntime"
|
|
2
|
-
import {GGLog} from "@grest-ts/logger"
|
|
3
|
-
import {IPCClientRequest, IPCServer} from "@grest-ts/ipc";
|
|
4
|
-
import {GGContext} from "@grest-ts/context";
|
|
5
|
-
import {GG_TRACE} from "@grest-ts/trace";
|
|
6
|
-
import {GGLocatorKey} from "@grest-ts/locator";
|
|
7
|
-
import {GGTestComponent, GGTestComponentType} from "./testers/GGTestComponent";
|
|
8
|
-
import {IGGLocalDiscoveryServer} from "./IGGLocalDiscoveryServer";
|
|
9
|
-
import {TestableIPC} from "./callOn/TestableIPC";
|
|
10
|
-
|
|
11
|
-
export const GG_TEST_RUNNER = new GGLocatorKey<GGTestRunner>("GGTestRunner");
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Interface for test lifecycle hooks.
|
|
15
|
-
* Registered via GGTest.registerHook() to run setup/teardown logic.
|
|
16
|
-
*/
|
|
17
|
-
export interface GGTestRunnerHook {
|
|
18
|
-
/** Key name for deduplication (e.g., config key name) */
|
|
19
|
-
keyName: string;
|
|
20
|
-
/** Runs in beforeAll - setup logic */
|
|
21
|
-
beforeAll: () => Promise<void>;
|
|
22
|
-
/** Runs in afterAll - cleanup logic */
|
|
23
|
-
afterAll: () => Promise<void>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Symbol for resources that support test operations.
|
|
28
|
-
* Resources implement this to expose operations like clone().
|
|
29
|
-
*/
|
|
30
|
-
export const GG_TEST_RESOURCE = Symbol('GG_TEST_RESOURCE');
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Type for objects that expose test resource operations.
|
|
34
|
-
* Used by GGTest.with() to get available operations.
|
|
35
|
-
*/
|
|
36
|
-
export interface TestResource<T = any> {
|
|
37
|
-
[GG_TEST_RESOURCE]: T;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface GGTestConfig {
|
|
41
|
-
serviceStartupTimeout: number
|
|
42
|
-
verboseProxy: boolean
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export class GGTestRunner {
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Unique identifier for this test context.
|
|
49
|
-
* Used for test resource isolation (e.g., creating isolated DB schemas).
|
|
50
|
-
*/
|
|
51
|
-
public readonly testId: string
|
|
52
|
-
public readonly ipcServer: IPCServer;
|
|
53
|
-
public readonly discoveryServer: IGGLocalDiscoveryServer
|
|
54
|
-
public readonly config: GGTestConfig = {
|
|
55
|
-
serviceStartupTimeout: 30000,
|
|
56
|
-
verboseProxy: false
|
|
57
|
-
}
|
|
58
|
-
private _started: boolean = false
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Runtimes added in the describe block (before start).
|
|
62
|
-
* These are managed by beforeAll/afterAll.
|
|
63
|
-
*/
|
|
64
|
-
private readonly globalRuntimes: GGTestRuntime[] = []
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Runtimes added within test blocks (after start).
|
|
68
|
-
* These are managed by afterEach and cleared after each test.
|
|
69
|
-
*/
|
|
70
|
-
private readonly inTestRuntimes: GGTestRuntime[] = []
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Extension instances - extensions are some "describe block level components".
|
|
74
|
-
* Some examples EventsServer, HttpInterceptorsServer, MockableInterceptorsServer etc.
|
|
75
|
-
*/
|
|
76
|
-
private readonly extensionInstances = new Map<GGTestComponentType<any>, GGTestComponent>();
|
|
77
|
-
|
|
78
|
-
private readonly hooks: Map<string, GGTestRunnerHook> = new Map()
|
|
79
|
-
|
|
80
|
-
constructor(ipcServer: IPCServer, discoveryServer: IGGLocalDiscoveryServer, userConfig?: Partial<GGTestConfig>) {
|
|
81
|
-
this.testId = "t" + Math.random().toString(36).substring(2, 8);
|
|
82
|
-
this.config = {...this.config, ...userConfig};
|
|
83
|
-
this.ipcServer = ipcServer;
|
|
84
|
-
this.discoveryServer = discoveryServer;
|
|
85
|
-
|
|
86
|
-
// Register IPC handler for key registration from workers
|
|
87
|
-
this.ipcServer.onFrameworkMessage(TestableIPC.server.registerKeys, async (payload) => {
|
|
88
|
-
const runtime = this.getRuntimeById(payload.runtimeId);
|
|
89
|
-
if (runtime) {
|
|
90
|
-
runtime.registerLocatorKeys(payload.keys);
|
|
91
|
-
GGLog.debug(this, `Registered ${payload.keys.length} keys for runtime ${payload.runtimeId}`);
|
|
92
|
-
} else {
|
|
93
|
-
GGLog.warn(this, `Received key registration for unknown runtime: ${payload.runtimeId}`);
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// -----------------------------------------------
|
|
99
|
-
// Static component factory registry
|
|
100
|
-
// -----------------------------------------------
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Extension factories - extensions are some "describe block level components".
|
|
104
|
-
* Some examples EventsServer, HttpInterceptorsServer, MockableInterceptorsServer etc.
|
|
105
|
-
*/
|
|
106
|
-
private static extensionFactories: GGTestComponentType<any>[] = [];
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Register a component type.
|
|
110
|
-
* Components must accept GGTestRunner as their constructor argument.
|
|
111
|
-
*/
|
|
112
|
-
public static registerExtension<T extends GGTestComponent>(type: GGTestComponentType<T>): void {
|
|
113
|
-
this.extensionFactories.push(type);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// -----------------------------------------------
|
|
117
|
-
// Component registry
|
|
118
|
-
// -----------------------------------------------
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Get a component by type. Creates it lazily if not yet instantiated.
|
|
122
|
-
*/
|
|
123
|
-
public getExtensionInstance<T extends GGTestComponent>(type: GGTestComponentType<T>): T {
|
|
124
|
-
if (!this.extensionInstances.has(type)) {
|
|
125
|
-
const instance = new type(this);
|
|
126
|
-
this.extensionInstances.set(type, instance);
|
|
127
|
-
}
|
|
128
|
-
return this.extensionInstances.get(type) as T;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// -----------------------------------------------
|
|
133
|
-
// Instance methods
|
|
134
|
-
// -----------------------------------------------
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Whether the test has started (services are running).
|
|
138
|
-
*/
|
|
139
|
-
public get started(): boolean {
|
|
140
|
-
return this._started;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Register a test lifecycle hook.
|
|
145
|
-
* Hooks run beforeAll (during start) and afterAll (during teardown).
|
|
146
|
-
* Duplicate registrations with the same keyName are skipped.
|
|
147
|
-
*/
|
|
148
|
-
public registerHook(hook: GGTestRunnerHook): void {
|
|
149
|
-
if (this.hooks.has(hook.keyName)) {
|
|
150
|
-
GGLog.debug(this, `Hook already registered for ${hook.keyName}, skipping duplicate`);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
this.hooks.set(hook.keyName, hook);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Send a command to ALL runtimes.
|
|
158
|
-
* Used by resource hooks (e.g., DB config) that apply globally.
|
|
159
|
-
*/
|
|
160
|
-
public async sendCommand<Payload>(type: IPCClientRequest<Payload, any>, payload: Payload): Promise<void> {
|
|
161
|
-
const promises = this.globalRuntimes.map(runtime => runtime.sendCommand(type, payload));
|
|
162
|
-
await Promise.allSettled(promises)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* Add a runtime to this test runner.
|
|
168
|
-
* Automatically routes to the appropriate list based on lifecycle stage.
|
|
169
|
-
*/
|
|
170
|
-
public addRuntime(runtime: GGTestRuntime): void {
|
|
171
|
-
if (this._started) {
|
|
172
|
-
// Added within a test block - managed by afterEach
|
|
173
|
-
this.inTestRuntimes.push(runtime);
|
|
174
|
-
} else {
|
|
175
|
-
// Added in describe block - managed by beforeAll/afterAll
|
|
176
|
-
this.globalRuntimes.push(runtime);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Get all active runtimes (both global and in-test).
|
|
182
|
-
*/
|
|
183
|
-
public getAllRuntimes(): GGTestRuntime[] {
|
|
184
|
-
return [...this.globalRuntimes, ...this.inTestRuntimes];
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Find a runtime by its unique runtimeId.
|
|
189
|
-
*/
|
|
190
|
-
public getRuntimeById(runtimeId: string): GGTestRuntime | undefined {
|
|
191
|
-
return this.globalRuntimes.find(r => r.runtimeId === runtimeId)
|
|
192
|
-
?? this.inTestRuntimes.find(r => r.runtimeId === runtimeId);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
public async start(): Promise<void> {
|
|
196
|
-
await new GGContext("Test").run(async () => {
|
|
197
|
-
GG_TRACE.init();
|
|
198
|
-
if (this._started) {
|
|
199
|
-
throw new Error("Already started!");
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// 0. Initialize extension (testkits already loaded by vitest setup)
|
|
203
|
-
for (const type of GGTestRunner.extensionFactories) {
|
|
204
|
-
// Create extension instance, which registers its IPC handlers
|
|
205
|
-
this.extensionInstances.set(type, new type(this));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// 1. Execute all hook beforeAll handlers in parallel
|
|
209
|
-
const hookPromises = Array.from(this.hooks.entries()).map(async ([keyName, hook]) => {
|
|
210
|
-
const startTime = performance.now();
|
|
211
|
-
GGLog.debug(this, `Running beforeAll hook: ${keyName}`);
|
|
212
|
-
await hook.beforeAll();
|
|
213
|
-
const duration = (performance.now() - startTime).toFixed(0);
|
|
214
|
-
GGLog.debug(this, `Completed beforeAll hook: ${keyName} (${duration}ms)`);
|
|
215
|
-
return keyName;
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
const results = await Promise.allSettled(hookPromises);
|
|
219
|
-
const failures = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected');
|
|
220
|
-
if (failures.length > 0) {
|
|
221
|
-
const errorMessages = failures.map(f => {
|
|
222
|
-
const reason = f.reason;
|
|
223
|
-
if (reason instanceof AggregateError) {
|
|
224
|
-
const innerErrors = reason.errors?.map((e: any) => e?.message || String(e)).join(', ');
|
|
225
|
-
return `AggregateError[${innerErrors}]`;
|
|
226
|
-
}
|
|
227
|
-
return reason?.message || String(reason);
|
|
228
|
-
}).join('; ');
|
|
229
|
-
throw new Error(`Failed to run beforeAll hooks: ${errorMessages}`);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// 2. Start router
|
|
233
|
-
await this.discoveryServer.start();
|
|
234
|
-
|
|
235
|
-
// 3. Start components
|
|
236
|
-
for (const [type, component] of this.extensionInstances) {
|
|
237
|
-
if (component.start) {
|
|
238
|
-
GGLog.debug(this, `Starting component: ${type.name}`);
|
|
239
|
-
await component.start();
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// 4. Start runtimes (they will receive commands via env)
|
|
244
|
-
for (const runtime of this.globalRuntimes) {
|
|
245
|
-
await runtime.start()
|
|
246
|
-
}
|
|
247
|
-
this._started = true;
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
public async runAfterEachHooks(): Promise<void> {
|
|
252
|
-
for (const [, component] of this.extensionInstances) {
|
|
253
|
-
await component.afterEach?.();
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
GGLog.debug(this, 'Stopping in-test runtimes...');
|
|
257
|
-
for (const runtime of this.inTestRuntimes) {
|
|
258
|
-
await runtime.shutdown();
|
|
259
|
-
}
|
|
260
|
-
this.inTestRuntimes.length = 0;
|
|
261
|
-
GGLog.debug(this, 'All in-test runtimes stopped!');
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
public async teardown(): Promise<void> {
|
|
265
|
-
await new GGContext("Test").run(async () => {
|
|
266
|
-
GG_TRACE.init();
|
|
267
|
-
// Stop global runtimes (teardown services, keep IPC alive)
|
|
268
|
-
GGLog.debug(this, 'Stopping global runtimes...');
|
|
269
|
-
for (const runtime of this.globalRuntimes) {
|
|
270
|
-
await runtime.stop()
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Shutdown global runtimes (terminate workers/processes)
|
|
274
|
-
GGLog.debug(this, 'Shutting down global runtimes...');
|
|
275
|
-
for (const runtime of this.globalRuntimes) {
|
|
276
|
-
await runtime.shutdown()
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
GGLog.debug(this, 'Tearing down router...');
|
|
280
|
-
await this.discoveryServer.teardown();
|
|
281
|
-
|
|
282
|
-
// Teardown components
|
|
283
|
-
for (const [type, component] of this.extensionInstances) {
|
|
284
|
-
if (component.teardown) {
|
|
285
|
-
GGLog.debug(this, `Tearing down component: ${type.name}`);
|
|
286
|
-
await component.teardown();
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
this.extensionInstances.clear();
|
|
290
|
-
|
|
291
|
-
GGLog.debug(this, 'Router torn down');
|
|
292
|
-
|
|
293
|
-
// Run afterAll hooks
|
|
294
|
-
GGLog.debug(this, 'Running afterAll hooks...');
|
|
295
|
-
for (const [keyName, hook] of this.hooks) {
|
|
296
|
-
try {
|
|
297
|
-
await hook.afterAll();
|
|
298
|
-
GGLog.debug(this, `Completed afterAll hook: ${keyName}`);
|
|
299
|
-
} catch (error) {
|
|
300
|
-
GGLog.error(this, `Failed afterAll hook ${keyName}:`, error);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
this.globalRuntimes.length = 0
|
|
305
|
-
this.hooks.clear()
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
}
|
|
1
|
+
import {GGTestRuntime} from "./GGTestRuntime"
|
|
2
|
+
import {GGLog} from "@grest-ts/logger"
|
|
3
|
+
import {IPCClientRequest, IPCServer} from "@grest-ts/ipc";
|
|
4
|
+
import {GGContext} from "@grest-ts/context";
|
|
5
|
+
import {GG_TRACE} from "@grest-ts/trace";
|
|
6
|
+
import {GGLocatorKey} from "@grest-ts/locator";
|
|
7
|
+
import {GGTestComponent, GGTestComponentType} from "./testers/GGTestComponent";
|
|
8
|
+
import {IGGLocalDiscoveryServer} from "./IGGLocalDiscoveryServer";
|
|
9
|
+
import {TestableIPC} from "./callOn/TestableIPC";
|
|
10
|
+
|
|
11
|
+
export const GG_TEST_RUNNER = new GGLocatorKey<GGTestRunner>("GGTestRunner");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Interface for test lifecycle hooks.
|
|
15
|
+
* Registered via GGTest.registerHook() to run setup/teardown logic.
|
|
16
|
+
*/
|
|
17
|
+
export interface GGTestRunnerHook {
|
|
18
|
+
/** Key name for deduplication (e.g., config key name) */
|
|
19
|
+
keyName: string;
|
|
20
|
+
/** Runs in beforeAll - setup logic */
|
|
21
|
+
beforeAll: () => Promise<void>;
|
|
22
|
+
/** Runs in afterAll - cleanup logic */
|
|
23
|
+
afterAll: () => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Symbol for resources that support test operations.
|
|
28
|
+
* Resources implement this to expose operations like clone().
|
|
29
|
+
*/
|
|
30
|
+
export const GG_TEST_RESOURCE = Symbol('GG_TEST_RESOURCE');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Type for objects that expose test resource operations.
|
|
34
|
+
* Used by GGTest.with() to get available operations.
|
|
35
|
+
*/
|
|
36
|
+
export interface TestResource<T = any> {
|
|
37
|
+
[GG_TEST_RESOURCE]: T;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface GGTestConfig {
|
|
41
|
+
serviceStartupTimeout: number
|
|
42
|
+
verboseProxy: boolean
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class GGTestRunner {
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Unique identifier for this test context.
|
|
49
|
+
* Used for test resource isolation (e.g., creating isolated DB schemas).
|
|
50
|
+
*/
|
|
51
|
+
public readonly testId: string
|
|
52
|
+
public readonly ipcServer: IPCServer;
|
|
53
|
+
public readonly discoveryServer: IGGLocalDiscoveryServer
|
|
54
|
+
public readonly config: GGTestConfig = {
|
|
55
|
+
serviceStartupTimeout: 30000,
|
|
56
|
+
verboseProxy: false
|
|
57
|
+
}
|
|
58
|
+
private _started: boolean = false
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Runtimes added in the describe block (before start).
|
|
62
|
+
* These are managed by beforeAll/afterAll.
|
|
63
|
+
*/
|
|
64
|
+
private readonly globalRuntimes: GGTestRuntime[] = []
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Runtimes added within test blocks (after start).
|
|
68
|
+
* These are managed by afterEach and cleared after each test.
|
|
69
|
+
*/
|
|
70
|
+
private readonly inTestRuntimes: GGTestRuntime[] = []
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extension instances - extensions are some "describe block level components".
|
|
74
|
+
* Some examples EventsServer, HttpInterceptorsServer, MockableInterceptorsServer etc.
|
|
75
|
+
*/
|
|
76
|
+
private readonly extensionInstances = new Map<GGTestComponentType<any>, GGTestComponent>();
|
|
77
|
+
|
|
78
|
+
private readonly hooks: Map<string, GGTestRunnerHook> = new Map()
|
|
79
|
+
|
|
80
|
+
constructor(ipcServer: IPCServer, discoveryServer: IGGLocalDiscoveryServer, userConfig?: Partial<GGTestConfig>) {
|
|
81
|
+
this.testId = "t" + Math.random().toString(36).substring(2, 8);
|
|
82
|
+
this.config = {...this.config, ...userConfig};
|
|
83
|
+
this.ipcServer = ipcServer;
|
|
84
|
+
this.discoveryServer = discoveryServer;
|
|
85
|
+
|
|
86
|
+
// Register IPC handler for key registration from workers
|
|
87
|
+
this.ipcServer.onFrameworkMessage(TestableIPC.server.registerKeys, async (payload) => {
|
|
88
|
+
const runtime = this.getRuntimeById(payload.runtimeId);
|
|
89
|
+
if (runtime) {
|
|
90
|
+
runtime.registerLocatorKeys(payload.keys);
|
|
91
|
+
GGLog.debug(this, `Registered ${payload.keys.length} keys for runtime ${payload.runtimeId}`);
|
|
92
|
+
} else {
|
|
93
|
+
GGLog.warn(this, `Received key registration for unknown runtime: ${payload.runtimeId}`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// -----------------------------------------------
|
|
99
|
+
// Static component factory registry
|
|
100
|
+
// -----------------------------------------------
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Extension factories - extensions are some "describe block level components".
|
|
104
|
+
* Some examples EventsServer, HttpInterceptorsServer, MockableInterceptorsServer etc.
|
|
105
|
+
*/
|
|
106
|
+
private static extensionFactories: GGTestComponentType<any>[] = [];
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Register a component type.
|
|
110
|
+
* Components must accept GGTestRunner as their constructor argument.
|
|
111
|
+
*/
|
|
112
|
+
public static registerExtension<T extends GGTestComponent>(type: GGTestComponentType<T>): void {
|
|
113
|
+
this.extensionFactories.push(type);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// -----------------------------------------------
|
|
117
|
+
// Component registry
|
|
118
|
+
// -----------------------------------------------
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Get a component by type. Creates it lazily if not yet instantiated.
|
|
122
|
+
*/
|
|
123
|
+
public getExtensionInstance<T extends GGTestComponent>(type: GGTestComponentType<T>): T {
|
|
124
|
+
if (!this.extensionInstances.has(type)) {
|
|
125
|
+
const instance = new type(this);
|
|
126
|
+
this.extensionInstances.set(type, instance);
|
|
127
|
+
}
|
|
128
|
+
return this.extensionInstances.get(type) as T;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
// -----------------------------------------------
|
|
133
|
+
// Instance methods
|
|
134
|
+
// -----------------------------------------------
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Whether the test has started (services are running).
|
|
138
|
+
*/
|
|
139
|
+
public get started(): boolean {
|
|
140
|
+
return this._started;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Register a test lifecycle hook.
|
|
145
|
+
* Hooks run beforeAll (during start) and afterAll (during teardown).
|
|
146
|
+
* Duplicate registrations with the same keyName are skipped.
|
|
147
|
+
*/
|
|
148
|
+
public registerHook(hook: GGTestRunnerHook): void {
|
|
149
|
+
if (this.hooks.has(hook.keyName)) {
|
|
150
|
+
GGLog.debug(this, `Hook already registered for ${hook.keyName}, skipping duplicate`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
this.hooks.set(hook.keyName, hook);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Send a command to ALL runtimes.
|
|
158
|
+
* Used by resource hooks (e.g., DB config) that apply globally.
|
|
159
|
+
*/
|
|
160
|
+
public async sendCommand<Payload>(type: IPCClientRequest<Payload, any>, payload: Payload): Promise<void> {
|
|
161
|
+
const promises = this.globalRuntimes.map(runtime => runtime.sendCommand(type, payload));
|
|
162
|
+
await Promise.allSettled(promises)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Add a runtime to this test runner.
|
|
168
|
+
* Automatically routes to the appropriate list based on lifecycle stage.
|
|
169
|
+
*/
|
|
170
|
+
public addRuntime(runtime: GGTestRuntime): void {
|
|
171
|
+
if (this._started) {
|
|
172
|
+
// Added within a test block - managed by afterEach
|
|
173
|
+
this.inTestRuntimes.push(runtime);
|
|
174
|
+
} else {
|
|
175
|
+
// Added in describe block - managed by beforeAll/afterAll
|
|
176
|
+
this.globalRuntimes.push(runtime);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get all active runtimes (both global and in-test).
|
|
182
|
+
*/
|
|
183
|
+
public getAllRuntimes(): GGTestRuntime[] {
|
|
184
|
+
return [...this.globalRuntimes, ...this.inTestRuntimes];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Find a runtime by its unique runtimeId.
|
|
189
|
+
*/
|
|
190
|
+
public getRuntimeById(runtimeId: string): GGTestRuntime | undefined {
|
|
191
|
+
return this.globalRuntimes.find(r => r.runtimeId === runtimeId)
|
|
192
|
+
?? this.inTestRuntimes.find(r => r.runtimeId === runtimeId);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public async start(): Promise<void> {
|
|
196
|
+
await new GGContext("Test").run(async () => {
|
|
197
|
+
GG_TRACE.init();
|
|
198
|
+
if (this._started) {
|
|
199
|
+
throw new Error("Already started!");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 0. Initialize extension (testkits already loaded by vitest setup)
|
|
203
|
+
for (const type of GGTestRunner.extensionFactories) {
|
|
204
|
+
// Create extension instance, which registers its IPC handlers
|
|
205
|
+
this.extensionInstances.set(type, new type(this));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// 1. Execute all hook beforeAll handlers in parallel
|
|
209
|
+
const hookPromises = Array.from(this.hooks.entries()).map(async ([keyName, hook]) => {
|
|
210
|
+
const startTime = performance.now();
|
|
211
|
+
GGLog.debug(this, `Running beforeAll hook: ${keyName}`);
|
|
212
|
+
await hook.beforeAll();
|
|
213
|
+
const duration = (performance.now() - startTime).toFixed(0);
|
|
214
|
+
GGLog.debug(this, `Completed beforeAll hook: ${keyName} (${duration}ms)`);
|
|
215
|
+
return keyName;
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const results = await Promise.allSettled(hookPromises);
|
|
219
|
+
const failures = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected');
|
|
220
|
+
if (failures.length > 0) {
|
|
221
|
+
const errorMessages = failures.map(f => {
|
|
222
|
+
const reason = f.reason;
|
|
223
|
+
if (reason instanceof AggregateError) {
|
|
224
|
+
const innerErrors = reason.errors?.map((e: any) => e?.message || String(e)).join(', ');
|
|
225
|
+
return `AggregateError[${innerErrors}]`;
|
|
226
|
+
}
|
|
227
|
+
return reason?.message || String(reason);
|
|
228
|
+
}).join('; ');
|
|
229
|
+
throw new Error(`Failed to run beforeAll hooks: ${errorMessages}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 2. Start router
|
|
233
|
+
await this.discoveryServer.start();
|
|
234
|
+
|
|
235
|
+
// 3. Start components
|
|
236
|
+
for (const [type, component] of this.extensionInstances) {
|
|
237
|
+
if (component.start) {
|
|
238
|
+
GGLog.debug(this, `Starting component: ${type.name}`);
|
|
239
|
+
await component.start();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 4. Start runtimes (they will receive commands via env)
|
|
244
|
+
for (const runtime of this.globalRuntimes) {
|
|
245
|
+
await runtime.start()
|
|
246
|
+
}
|
|
247
|
+
this._started = true;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
public async runAfterEachHooks(): Promise<void> {
|
|
252
|
+
for (const [, component] of this.extensionInstances) {
|
|
253
|
+
await component.afterEach?.();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
GGLog.debug(this, 'Stopping in-test runtimes...');
|
|
257
|
+
for (const runtime of this.inTestRuntimes) {
|
|
258
|
+
await runtime.shutdown();
|
|
259
|
+
}
|
|
260
|
+
this.inTestRuntimes.length = 0;
|
|
261
|
+
GGLog.debug(this, 'All in-test runtimes stopped!');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public async teardown(): Promise<void> {
|
|
265
|
+
await new GGContext("Test").run(async () => {
|
|
266
|
+
GG_TRACE.init();
|
|
267
|
+
// Stop global runtimes (teardown services, keep IPC alive)
|
|
268
|
+
GGLog.debug(this, 'Stopping global runtimes...');
|
|
269
|
+
for (const runtime of this.globalRuntimes) {
|
|
270
|
+
await runtime.stop()
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Shutdown global runtimes (terminate workers/processes)
|
|
274
|
+
GGLog.debug(this, 'Shutting down global runtimes...');
|
|
275
|
+
for (const runtime of this.globalRuntimes) {
|
|
276
|
+
await runtime.shutdown()
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
GGLog.debug(this, 'Tearing down router...');
|
|
280
|
+
await this.discoveryServer.teardown();
|
|
281
|
+
|
|
282
|
+
// Teardown components
|
|
283
|
+
for (const [type, component] of this.extensionInstances) {
|
|
284
|
+
if (component.teardown) {
|
|
285
|
+
GGLog.debug(this, `Tearing down component: ${type.name}`);
|
|
286
|
+
await component.teardown();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
this.extensionInstances.clear();
|
|
290
|
+
|
|
291
|
+
GGLog.debug(this, 'Router torn down');
|
|
292
|
+
|
|
293
|
+
// Run afterAll hooks
|
|
294
|
+
GGLog.debug(this, 'Running afterAll hooks...');
|
|
295
|
+
for (const [keyName, hook] of this.hooks) {
|
|
296
|
+
try {
|
|
297
|
+
await hook.afterAll();
|
|
298
|
+
GGLog.debug(this, `Completed afterAll hook: ${keyName}`);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
GGLog.error(this, `Failed afterAll hook ${keyName}:`, error);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
this.globalRuntimes.length = 0
|
|
305
|
+
this.hooks.clear()
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|