@grest-ts/testkit 0.0.5
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 -0
- package/README.md +413 -0
- package/dist/src/GGBundleTest.d.ts +8 -0
- package/dist/src/GGBundleTest.d.ts.map +1 -0
- package/dist/src/GGBundleTest.js +75 -0
- package/dist/src/GGBundleTest.js.map +1 -0
- package/dist/src/GGTest.d.ts +131 -0
- package/dist/src/GGTest.d.ts.map +1 -0
- package/dist/src/GGTest.js +245 -0
- package/dist/src/GGTest.js.map +1 -0
- package/dist/src/GGTestContext.d.ts +36 -0
- package/dist/src/GGTestContext.d.ts.map +1 -0
- package/dist/src/GGTestContext.js +63 -0
- package/dist/src/GGTestContext.js.map +1 -0
- package/dist/src/GGTestRunner.d.ts +108 -0
- package/dist/src/GGTestRunner.d.ts.map +1 -0
- package/dist/src/GGTestRunner.js +242 -0
- package/dist/src/GGTestRunner.js.map +1 -0
- package/dist/src/GGTestRuntime.d.ts +103 -0
- package/dist/src/GGTestRuntime.d.ts.map +1 -0
- package/dist/src/GGTestRuntime.js +219 -0
- package/dist/src/GGTestRuntime.js.map +1 -0
- package/dist/src/GGTestRuntimeWorker.d.ts +41 -0
- package/dist/src/GGTestRuntimeWorker.d.ts.map +1 -0
- package/dist/src/GGTestRuntimeWorker.js +136 -0
- package/dist/src/GGTestRuntimeWorker.js.map +1 -0
- package/dist/src/GGTestSharedRef.d.ts +35 -0
- package/dist/src/GGTestSharedRef.d.ts.map +1 -0
- package/dist/src/GGTestSharedRef.js +126 -0
- package/dist/src/GGTestSharedRef.js.map +1 -0
- package/dist/src/GGTestkitExtensionsDiscovery.d.ts +21 -0
- package/dist/src/GGTestkitExtensionsDiscovery.d.ts.map +1 -0
- package/dist/src/GGTestkitExtensionsDiscovery.js +24 -0
- package/dist/src/GGTestkitExtensionsDiscovery.js.map +1 -0
- package/dist/src/IGGLocalDiscoveryServer.d.ts +16 -0
- package/dist/src/IGGLocalDiscoveryServer.d.ts.map +1 -0
- package/dist/src/IGGLocalDiscoveryServer.js +2 -0
- package/dist/src/IGGLocalDiscoveryServer.js.map +1 -0
- package/dist/src/callOn/GGCallOnSelector.d.ts +42 -0
- package/dist/src/callOn/GGCallOnSelector.d.ts.map +1 -0
- package/dist/src/callOn/GGCallOnSelector.js +35 -0
- package/dist/src/callOn/GGCallOnSelector.js.map +1 -0
- package/dist/src/callOn/GGContractClass.implement.d.ts +8 -0
- package/dist/src/callOn/GGContractClass.implement.d.ts.map +1 -0
- package/dist/src/callOn/GGContractClass.implement.js +31 -0
- package/dist/src/callOn/GGContractClass.implement.js.map +1 -0
- package/dist/src/callOn/GGTestActionForLocatorOnCall.d.ts +28 -0
- package/dist/src/callOn/GGTestActionForLocatorOnCall.d.ts.map +1 -0
- package/dist/src/callOn/GGTestActionForLocatorOnCall.js +118 -0
- package/dist/src/callOn/GGTestActionForLocatorOnCall.js.map +1 -0
- package/dist/src/callOn/TestableIPC.d.ts +72 -0
- package/dist/src/callOn/TestableIPC.d.ts.map +1 -0
- package/dist/src/callOn/TestableIPC.js +34 -0
- package/dist/src/callOn/TestableIPC.js.map +1 -0
- package/dist/src/callOn/callOn.d.ts +113 -0
- package/dist/src/callOn/callOn.d.ts.map +1 -0
- package/dist/src/callOn/callOn.js +122 -0
- package/dist/src/callOn/callOn.js.map +1 -0
- package/dist/src/callOn/registerOnCallHandler.d.ts +13 -0
- package/dist/src/callOn/registerOnCallHandler.d.ts.map +1 -0
- package/dist/src/callOn/registerOnCallHandler.js +111 -0
- package/dist/src/callOn/registerOnCallHandler.js.map +1 -0
- package/dist/src/index-node.d.ts +35 -0
- package/dist/src/index-node.d.ts.map +1 -0
- package/dist/src/index-node.js +50 -0
- package/dist/src/index-node.js.map +1 -0
- package/dist/src/mockable/GGMockable.d.ts +19 -0
- package/dist/src/mockable/GGMockable.d.ts.map +1 -0
- package/dist/src/mockable/GGMockable.js +2 -0
- package/dist/src/mockable/GGMockable.js.map +1 -0
- package/dist/src/mockable/GGMockableCall.d.ts +2 -0
- package/dist/src/mockable/GGMockableCall.d.ts.map +1 -0
- package/dist/src/mockable/GGMockableCall.js +41 -0
- package/dist/src/mockable/GGMockableCall.js.map +1 -0
- package/dist/src/mockable/GGMockableIPC.d.ts +17 -0
- package/dist/src/mockable/GGMockableIPC.d.ts.map +1 -0
- package/dist/src/mockable/GGMockableIPC.js +8 -0
- package/dist/src/mockable/GGMockableIPC.js.map +1 -0
- package/dist/src/mockable/GGMockableInterceptor.d.ts +24 -0
- package/dist/src/mockable/GGMockableInterceptor.d.ts.map +1 -0
- package/dist/src/mockable/GGMockableInterceptor.js +32 -0
- package/dist/src/mockable/GGMockableInterceptor.js.map +1 -0
- package/dist/src/mockable/GGMockableInterceptorsServer.d.ts +12 -0
- package/dist/src/mockable/GGMockableInterceptorsServer.d.ts.map +1 -0
- package/dist/src/mockable/GGMockableInterceptorsServer.js +55 -0
- package/dist/src/mockable/GGMockableInterceptorsServer.js.map +1 -0
- package/dist/src/mockable/mockable.d.ts +46 -0
- package/dist/src/mockable/mockable.d.ts.map +1 -0
- package/dist/src/mockable/mockable.js +47 -0
- package/dist/src/mockable/mockable.js.map +1 -0
- package/dist/src/runner/InlineRunner.d.ts +12 -0
- package/dist/src/runner/InlineRunner.d.ts.map +1 -0
- package/dist/src/runner/InlineRunner.js +42 -0
- package/dist/src/runner/InlineRunner.js.map +1 -0
- package/dist/src/runner/IsolatedRunner.d.ts +17 -0
- package/dist/src/runner/IsolatedRunner.d.ts.map +1 -0
- package/dist/src/runner/IsolatedRunner.js +155 -0
- package/dist/src/runner/IsolatedRunner.js.map +1 -0
- package/dist/src/runner/RuntimeRunner.d.ts +14 -0
- package/dist/src/runner/RuntimeRunner.d.ts.map +1 -0
- package/dist/src/runner/RuntimeRunner.js +2 -0
- package/dist/src/runner/RuntimeRunner.js.map +1 -0
- package/dist/src/runner/WorkerRunner.d.ts +17 -0
- package/dist/src/runner/WorkerRunner.d.ts.map +1 -0
- package/dist/src/runner/WorkerRunner.js +155 -0
- package/dist/src/runner/WorkerRunner.js.map +1 -0
- package/dist/src/runner/isolated-loader.mjs +91 -0
- package/dist/src/runner/worker-loader.mjs +49 -0
- package/dist/src/testers/GGCallInterceptor.d.ts +71 -0
- package/dist/src/testers/GGCallInterceptor.d.ts.map +1 -0
- package/dist/src/testers/GGCallInterceptor.js +170 -0
- package/dist/src/testers/GGCallInterceptor.js.map +1 -0
- package/dist/src/testers/GGMockWith.d.ts +30 -0
- package/dist/src/testers/GGMockWith.d.ts.map +1 -0
- package/dist/src/testers/GGMockWith.js +70 -0
- package/dist/src/testers/GGMockWith.js.map +1 -0
- package/dist/src/testers/GGSpyWith.d.ts +40 -0
- package/dist/src/testers/GGSpyWith.d.ts.map +1 -0
- package/dist/src/testers/GGSpyWith.js +90 -0
- package/dist/src/testers/GGSpyWith.js.map +1 -0
- package/dist/src/testers/GGTestAction.d.ts +126 -0
- package/dist/src/testers/GGTestAction.d.ts.map +1 -0
- package/dist/src/testers/GGTestAction.js +245 -0
- package/dist/src/testers/GGTestAction.js.map +1 -0
- package/dist/src/testers/GGTestComponent.d.ts +15 -0
- package/dist/src/testers/GGTestComponent.d.ts.map +1 -0
- package/dist/src/testers/GGTestComponent.js +2 -0
- package/dist/src/testers/GGTestComponent.js.map +1 -0
- package/dist/src/testers/GGTestSelector.d.ts +54 -0
- package/dist/src/testers/GGTestSelector.d.ts.map +1 -0
- package/dist/src/testers/GGTestSelector.js +179 -0
- package/dist/src/testers/GGTestSelector.js.map +1 -0
- package/dist/src/testers/IGGTestInterceptor.d.ts +8 -0
- package/dist/src/testers/IGGTestInterceptor.d.ts.map +1 -0
- package/dist/src/testers/IGGTestInterceptor.js +2 -0
- package/dist/src/testers/IGGTestInterceptor.js.map +1 -0
- package/dist/src/testers/IGGTestWith.d.ts +13 -0
- package/dist/src/testers/IGGTestWith.d.ts.map +1 -0
- package/dist/src/testers/IGGTestWith.js +2 -0
- package/dist/src/testers/IGGTestWith.js.map +1 -0
- package/dist/src/testers/RuntimeSelector.d.ts +117 -0
- package/dist/src/testers/RuntimeSelector.d.ts.map +1 -0
- package/dist/src/testers/RuntimeSelector.js +2 -0
- package/dist/src/testers/RuntimeSelector.js.map +1 -0
- package/dist/src/tsconfig.json +17 -0
- package/dist/src/utils/GGExpectations.d.ts +18 -0
- package/dist/src/utils/GGExpectations.d.ts.map +1 -0
- package/dist/src/utils/GGExpectations.js +59 -0
- package/dist/src/utils/GGExpectations.js.map +1 -0
- package/dist/src/utils/GGTestError.d.ts +13 -0
- package/dist/src/utils/GGTestError.d.ts.map +1 -0
- package/dist/src/utils/GGTestError.js +26 -0
- package/dist/src/utils/GGTestError.js.map +1 -0
- package/dist/src/utils/captureStack.d.ts +9 -0
- package/dist/src/utils/captureStack.d.ts.map +1 -0
- package/dist/src/utils/captureStack.js +51 -0
- package/dist/src/utils/captureStack.js.map +1 -0
- package/dist/tsconfig.publish.tsbuildinfo +1 -0
- package/package.json +66 -0
- package/src/GGBundleTest.ts +89 -0
- package/src/GGTest.ts +318 -0
- package/src/GGTestContext.ts +74 -0
- package/src/GGTestRunner.ts +308 -0
- package/src/GGTestRuntime.ts +265 -0
- package/src/GGTestRuntimeWorker.ts +159 -0
- package/src/GGTestSharedRef.ts +116 -0
- package/src/GGTestkitExtensionsDiscovery.ts +26 -0
- package/src/IGGLocalDiscoveryServer.ts +16 -0
- package/src/callOn/GGCallOnSelector.ts +61 -0
- package/src/callOn/GGContractClass.implement.ts +43 -0
- package/src/callOn/GGTestActionForLocatorOnCall.ts +134 -0
- package/src/callOn/TestableIPC.ts +81 -0
- package/src/callOn/callOn.ts +224 -0
- package/src/callOn/registerOnCallHandler.ts +123 -0
- package/src/index-node.ts +64 -0
- package/src/mockable/GGMockable.ts +22 -0
- package/src/mockable/GGMockableCall.ts +45 -0
- package/src/mockable/GGMockableIPC.ts +20 -0
- package/src/mockable/GGMockableInterceptor.ts +44 -0
- package/src/mockable/GGMockableInterceptorsServer.ts +69 -0
- package/src/mockable/mockable.ts +71 -0
- package/src/runner/InlineRunner.ts +47 -0
- package/src/runner/IsolatedRunner.ts +179 -0
- package/src/runner/RuntimeRunner.ts +15 -0
- package/src/runner/WorkerRunner.ts +179 -0
- package/src/runner/isolated-loader.mjs +91 -0
- package/src/runner/worker-loader.mjs +49 -0
- package/src/testers/GGCallInterceptor.ts +224 -0
- package/src/testers/GGMockWith.ts +92 -0
- package/src/testers/GGSpyWith.ts +115 -0
- package/src/testers/GGTestAction.ts +333 -0
- package/src/testers/GGTestComponent.ts +16 -0
- package/src/testers/GGTestSelector.ts +223 -0
- package/src/testers/IGGTestInterceptor.ts +11 -0
- package/src/testers/IGGTestWith.ts +15 -0
- package/src/testers/RuntimeSelector.ts +151 -0
- package/src/tsconfig.json +17 -0
- package/src/utils/GGExpectations.ts +78 -0
- package/src/utils/GGTestError.ts +37 -0
- package/src/utils/captureStack.ts +54 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import {IPCClient, IPCClientRequest} from "@grest-ts/ipc";
|
|
2
|
+
import {GGLog} from "@grest-ts/logger";
|
|
3
|
+
import {GGLoggerConsole} from "@grest-ts/logger-console";
|
|
4
|
+
import {GGRuntime} from "@grest-ts/runtime";
|
|
5
|
+
import {GGLocatorKey, GGLocatorScope} from "@grest-ts/locator";
|
|
6
|
+
import {pathToFileURL} from "url";
|
|
7
|
+
import {GGTestCommand, GGTestEnvConfig} from "./GGTestRuntime";
|
|
8
|
+
import {GGExtensionDiscovery} from "@grest-ts/common";
|
|
9
|
+
import {type MockableTestContext, runWithMockableContext} from "@grest-ts/testkit-runtime";
|
|
10
|
+
import {CALL_THROUGH} from "./mockable/GGMockableInterceptorsServer";
|
|
11
|
+
import {GGMockableIPC} from "./mockable/GGMockableIPC";
|
|
12
|
+
import {registerOnCallHandler} from "./callOn/registerOnCallHandler";
|
|
13
|
+
import {TestableIPC} from "./callOn/TestableIPC";
|
|
14
|
+
|
|
15
|
+
export const GG_TEST_RUNTIME_WORKER = new GGLocatorKey<GGTestRuntimeWorker>("GGTestRuntimeWorker");
|
|
16
|
+
|
|
17
|
+
export class GGTestRuntimeWorker {
|
|
18
|
+
|
|
19
|
+
private static beforeRuntimeStartHandlers: (() => void)[] = [];
|
|
20
|
+
private static beforeRuntimeStartExecuted = false;
|
|
21
|
+
|
|
22
|
+
public readonly ipcClient: IPCClient;
|
|
23
|
+
public readonly config: GGTestEnvConfig;
|
|
24
|
+
public runtime: GGRuntime = undefined
|
|
25
|
+
private runtimeStopped = false;
|
|
26
|
+
|
|
27
|
+
private readonly scope: GGLocatorScope;
|
|
28
|
+
|
|
29
|
+
constructor(config: GGTestEnvConfig) {
|
|
30
|
+
this.ipcClient = new IPCClient(config.testRouterPort);
|
|
31
|
+
this.config = config;
|
|
32
|
+
this.scope = new GGLocatorScope("GGTestRuntimeWorker").enter();
|
|
33
|
+
this.scope.set(GG_TEST_RUNTIME_WORKER, this);
|
|
34
|
+
GGLog.init();
|
|
35
|
+
GGLog.add(new GGLoggerConsole({showData: true}));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register a function to be called before the runtime starts.
|
|
40
|
+
* Safe to call at module load time - just adds to array.
|
|
41
|
+
* Handlers are executed during start() after extensions are loaded but before runtime creation.
|
|
42
|
+
*/
|
|
43
|
+
public static onBeforeRuntimeStart(handler: () => void): void {
|
|
44
|
+
if (this.beforeRuntimeStartExecuted) {
|
|
45
|
+
throw new Error("Cannot register beforeRuntimeStart handler after worker has started");
|
|
46
|
+
}
|
|
47
|
+
this.beforeRuntimeStartHandlers.push(handler);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public async start(createRuntime?: () => GGRuntime): Promise<void> {
|
|
51
|
+
process.env.GG_LOCAL_ROUTER_PORT = String(this.config.testRouterPort);
|
|
52
|
+
|
|
53
|
+
await new GGExtensionDiscovery('testkit').load();
|
|
54
|
+
|
|
55
|
+
// Create mockable context that bridges to IPC
|
|
56
|
+
const mockableContext: MockableTestContext = {
|
|
57
|
+
CALL_THROUGH,
|
|
58
|
+
sendCall: async (className, methodName, callArgs) => {
|
|
59
|
+
return this.ipcClient.sendFrameworkRequest(GGMockableIPC.testServer.call, {
|
|
60
|
+
className,
|
|
61
|
+
methodName,
|
|
62
|
+
callArgs
|
|
63
|
+
});
|
|
64
|
+
},
|
|
65
|
+
sendSpyResult: async (className, methodName, callResult) => {
|
|
66
|
+
await this.ipcClient.sendFrameworkRequest(GGMockableIPC.testServer.spyResult, {
|
|
67
|
+
className,
|
|
68
|
+
methodName,
|
|
69
|
+
callResult
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Wrap in mockable context so @mockable decorators can intercept
|
|
75
|
+
await runWithMockableContext(mockableContext, async () => {
|
|
76
|
+
await this.ipcClient.connect(this.config.runtimeId);
|
|
77
|
+
GGLog.debug(this, 'Connected to test router');
|
|
78
|
+
|
|
79
|
+
// Register testable handler for direct service invocation from tests
|
|
80
|
+
registerOnCallHandler(this);
|
|
81
|
+
|
|
82
|
+
GGTestRuntimeWorker.beforeRuntimeStartExecuted = true;
|
|
83
|
+
GGTestRuntimeWorker.beforeRuntimeStartHandlers.forEach(handler => handler());
|
|
84
|
+
|
|
85
|
+
if (createRuntime) {
|
|
86
|
+
this.runtime = createRuntime();
|
|
87
|
+
} else {
|
|
88
|
+
// Dynamic import of the runtime source file.
|
|
89
|
+
// Always use file:// URL — required by Node ESM on Windows.
|
|
90
|
+
const moduleUrl = this.config.executablePath.startsWith('file:')
|
|
91
|
+
? this.config.executablePath
|
|
92
|
+
: pathToFileURL(this.config.executablePath).href;
|
|
93
|
+
const module = await import(moduleUrl);
|
|
94
|
+
const RuntimeClass = module[this.config.className];
|
|
95
|
+
if (!RuntimeClass) {
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Runtime class '${this.config.className}' not found in module '${this.config.executablePath}'. ` +
|
|
98
|
+
`Make sure the class is exported.`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
this.runtime = new RuntimeClass();
|
|
102
|
+
}
|
|
103
|
+
await this.runtime!.start();
|
|
104
|
+
|
|
105
|
+
// Send registered locator keys to test runner for callOn routing
|
|
106
|
+
const keys = this.runtime!.scope.getKeys();
|
|
107
|
+
await this.ipcClient.sendFrameworkRequest(TestableIPC.server.registerKeys, {
|
|
108
|
+
runtimeId: this.config.runtimeId,
|
|
109
|
+
keys
|
|
110
|
+
});
|
|
111
|
+
GGLog.debug(this, `Sent ${keys.length} locator keys to test runner`);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Stop the GGRuntime (teardown services) but keep IPC alive.
|
|
117
|
+
* The runtime reference is kept so IPC handlers can still access
|
|
118
|
+
* async contexts (logs, metrics, config) via runInContext().
|
|
119
|
+
*/
|
|
120
|
+
public async stopRuntime(): Promise<void> {
|
|
121
|
+
if (this.runtimeStopped) return;
|
|
122
|
+
this.runtimeStopped = true;
|
|
123
|
+
if (this.runtime) {
|
|
124
|
+
await this.runtime.scope.run(() => this.runtime!.teardown());
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Register an IPC request handler that automatically runs within the runtime's context.
|
|
130
|
+
* Use this instead of ipcClient.onFrameworkRequest() for handlers that need access to
|
|
131
|
+
* runtime services (logs, metrics, config, etc.) even after runtime.stop().
|
|
132
|
+
*/
|
|
133
|
+
public onIpcRequest<Req, Res>(
|
|
134
|
+
type: IPCClientRequest<Req, Res>,
|
|
135
|
+
handler: (payload: Req) => Res | Promise<Res>
|
|
136
|
+
): void {
|
|
137
|
+
this.ipcClient.onFrameworkRequest(type, async (payload) => {
|
|
138
|
+
if (this.runtime) {
|
|
139
|
+
return this.runtime.scope.run(() => handler(payload));
|
|
140
|
+
}
|
|
141
|
+
return handler(payload);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Fully shutdown the worker, including IPC disconnection.
|
|
147
|
+
* Calls stopRuntime() if not already stopped.
|
|
148
|
+
*/
|
|
149
|
+
public async shutdown(): Promise<void> {
|
|
150
|
+
if (!this.runtimeStopped) {
|
|
151
|
+
await this.stopRuntime();
|
|
152
|
+
}
|
|
153
|
+
this.ipcClient.disconnect();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public getInitialCommandsFor<Payload>(type: IPCClientRequest<Payload, any>): GGTestCommand<Payload>[] {
|
|
157
|
+
return this.config.initialCommands.filter(cmd => cmd.method === type) as GGTestCommand<Payload>[];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reference counting for shared test resources across workers.
|
|
7
|
+
*
|
|
8
|
+
* Used to coordinate shared resources (e.g. database schemas)
|
|
9
|
+
* that multiple workers use simultaneously. Handles file-system
|
|
10
|
+
* based locking so callers don't need their own locking mechanism.
|
|
11
|
+
*
|
|
12
|
+
* - acquire(key, onCreate): first caller runs onCreate, others wait and skip
|
|
13
|
+
* - release(key, onLast): last caller runs onLast (e.g. cleanup)
|
|
14
|
+
*/
|
|
15
|
+
export class GGTestSharedRef {
|
|
16
|
+
|
|
17
|
+
private static getRefDir(): string {
|
|
18
|
+
const runId = process.env.GG_TEST_RUN_ID;
|
|
19
|
+
if (!runId) {
|
|
20
|
+
throw new Error("GG_TEST_RUN_ID not set. Add globalSetup '@grest-ts/testkit-vitest/globalSetup' to vitest.config.ts.");
|
|
21
|
+
}
|
|
22
|
+
const dir = path.join(os.tmpdir(), `gg-test-${runId}`);
|
|
23
|
+
fs.mkdirSync(dir, {recursive: true});
|
|
24
|
+
return dir;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private static getRefFile(key: string): string {
|
|
28
|
+
return path.join(this.getRefDir(), `${key}.ref`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
private static getLockPath(key: string): string {
|
|
32
|
+
return path.join(this.getRefDir(), `${key}.lock`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private static async acquireLock(key: string): Promise<void> {
|
|
36
|
+
const lockPath = this.getLockPath(key);
|
|
37
|
+
const timeout = 60000;
|
|
38
|
+
const start = Date.now();
|
|
39
|
+
while (true) {
|
|
40
|
+
try {
|
|
41
|
+
fs.mkdirSync(lockPath);
|
|
42
|
+
return;
|
|
43
|
+
} catch {
|
|
44
|
+
if (Date.now() - start > timeout) {
|
|
45
|
+
throw new Error(`[GGTestSharedRef] Failed to acquire lock for '${key}' after ${timeout}ms`);
|
|
46
|
+
}
|
|
47
|
+
await new Promise(resolve => setTimeout(resolve, 50 + Math.random() * 50));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private static releaseLock(key: string): void {
|
|
53
|
+
try { fs.rmdirSync(this.getLockPath(key)); } catch {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Acquire a shared reference. If this is the first reference,
|
|
58
|
+
* the onCreate callback is called (e.g. to create a shared resource).
|
|
59
|
+
* Other callers wait for the lock and skip onCreate.
|
|
60
|
+
*/
|
|
61
|
+
static async acquire(key: string, onCreate: () => Promise<void>): Promise<void> {
|
|
62
|
+
await this.acquireLock(key);
|
|
63
|
+
try {
|
|
64
|
+
const file = this.getRefFile(key);
|
|
65
|
+
let count = 0;
|
|
66
|
+
try { count = parseInt(fs.readFileSync(file, 'utf-8')); } catch {}
|
|
67
|
+
|
|
68
|
+
if (count === 0) {
|
|
69
|
+
await onCreate();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fs.writeFileSync(file, (count + 1).toString());
|
|
73
|
+
} finally {
|
|
74
|
+
this.releaseLock(key);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Release a shared reference. If this is the last reference,
|
|
80
|
+
* the onLast callback is called (e.g. to drop a shared resource).
|
|
81
|
+
* Errors in onLast are caught and logged (cleanup is best-effort).
|
|
82
|
+
*/
|
|
83
|
+
static async release(key: string, onLast: () => Promise<void>): Promise<void> {
|
|
84
|
+
await this.acquireLock(key);
|
|
85
|
+
try {
|
|
86
|
+
const file = this.getRefFile(key);
|
|
87
|
+
let count = 0;
|
|
88
|
+
try { count = parseInt(fs.readFileSync(file, 'utf-8')); } catch {}
|
|
89
|
+
count -= 1;
|
|
90
|
+
|
|
91
|
+
if (count <= 0) {
|
|
92
|
+
try { fs.unlinkSync(file); } catch {}
|
|
93
|
+
try {
|
|
94
|
+
await onLast();
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error('[GGTestSharedRef] Cleanup failed:', err);
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
fs.writeFileSync(file, count.toString());
|
|
100
|
+
}
|
|
101
|
+
} finally {
|
|
102
|
+
this.releaseLock(key);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Remove the ref counting temp directory for the current test run.
|
|
108
|
+
* Called by globalSetup teardown after all workers have finished.
|
|
109
|
+
*/
|
|
110
|
+
static cleanup(): void {
|
|
111
|
+
const runId = process.env.GG_TEST_RUN_ID;
|
|
112
|
+
if (!runId) return;
|
|
113
|
+
const dir = path.join(os.tmpdir(), `gg-test-${runId}`);
|
|
114
|
+
try { fs.rmSync(dir, {recursive: true}); } catch {}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {GGExtensionDiscovery} from '@grest-ts/common';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Discovers testkit extensions by scanning node_modules for packages
|
|
5
|
+
* that follow the convention of having a testkit/index-testkit.ts file.
|
|
6
|
+
*
|
|
7
|
+
* @deprecated Use GGExtensionDiscovery from @grest-ts/common directly:
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const discovery = new GGExtensionDiscovery('testkit');
|
|
10
|
+
* await discovery.load();
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export class GGTestkitExtensionsDiscovery {
|
|
14
|
+
|
|
15
|
+
private static discovery = new GGExtensionDiscovery('testkit');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Discover and load all testkits.
|
|
19
|
+
* - Scans for testkit packages
|
|
20
|
+
* - Generates .d.ts file for IDE support
|
|
21
|
+
* - Dynamically imports testkits for runtime
|
|
22
|
+
*/
|
|
23
|
+
public static async load(): Promise<void> {
|
|
24
|
+
await this.discovery.load();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interface for discovery server used by GGTestRunner.
|
|
3
|
+
* Implemented by GGLocalDiscoveryServer in @grest-ts/discovery.
|
|
4
|
+
* This interface allows @grest-ts/testkit to not depend on @grest-ts/discovery.
|
|
5
|
+
*/
|
|
6
|
+
export interface IGGLocalDiscoveryServer {
|
|
7
|
+
start(): Promise<boolean>;
|
|
8
|
+
teardown(): Promise<void>;
|
|
9
|
+
getRoutingUrl(api: string): string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface IServiceRoute {
|
|
13
|
+
api: string;
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
pathPrefix: string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selector extension that adds targeted callOn to selectors.
|
|
3
|
+
*
|
|
4
|
+
* This allows explicitly targeting specific runtimes when multiple
|
|
5
|
+
* different runtime classes have the same service registered.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const f = GGTest.startWorker({chain: ChainRuntime, weather: WeatherOnlyRuntime});
|
|
9
|
+
*
|
|
10
|
+
* // Both have WeatherService - explicitly target one:
|
|
11
|
+
* await f.chain.callOn(WeatherService).getWeather("Test").toMatchObject({...})
|
|
12
|
+
* await f.weather.callOn(WeatherService).getWeather("Test").toMatchObject({...})
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {GGContext} from "@grest-ts/context";
|
|
16
|
+
import {GGTestSelector, GGTestSelectorExtension} from "../testers/GGTestSelector";
|
|
17
|
+
import {RuntimeConstructor} from "../testers/RuntimeSelector";
|
|
18
|
+
import {callOnTargeted, GGTestCallOn} from "./callOn";
|
|
19
|
+
import type {GGTestRuntime} from "../GGTestRuntime";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Type for the callable callOn extension.
|
|
23
|
+
* Can be called as a function to invoke targeted callOn.
|
|
24
|
+
*/
|
|
25
|
+
export interface GGCallOnSelectorCallable {
|
|
26
|
+
<T>(target: T, ctx?: GGContext): GGTestCallOn<T>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extension that adds .callOn() as a callable to Selectors.
|
|
31
|
+
* Uses the selector's runtimes to target specific instances.
|
|
32
|
+
*
|
|
33
|
+
* Returns a callable that can be used directly: f.chain.callOn(WeatherService)
|
|
34
|
+
*/
|
|
35
|
+
export class GGCallOnSelector extends GGTestSelectorExtension {
|
|
36
|
+
|
|
37
|
+
public static readonly PROPERTY_NAME = "callOn";
|
|
38
|
+
|
|
39
|
+
constructor(runtimes: GGTestRuntime[]) {
|
|
40
|
+
super(runtimes);
|
|
41
|
+
// Return a callable function instead of this instance
|
|
42
|
+
const callable = <T>(target: T, ctx?: GGContext): GGTestCallOn<T> => {
|
|
43
|
+
return callOnTargeted(target, runtimes, ctx);
|
|
44
|
+
};
|
|
45
|
+
return callable as unknown as this;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Declaration merging to add 'callOn' to SelectorExtensions
|
|
50
|
+
declare module "@grest-ts/testkit" {
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
52
|
+
interface SelectorExtensions<T extends RuntimeConstructor[]> {
|
|
53
|
+
callOn: GGCallOnSelectorCallable;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Ensure RuntimeConstructor import is recognized (for declaration merging above)
|
|
58
|
+
export type _RuntimeConstructorRef = RuntimeConstructor;
|
|
59
|
+
|
|
60
|
+
// Register the extension
|
|
61
|
+
GGTestSelector.addExtension(GGCallOnSelector);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Patches GGContractClass.implement() to auto-register contract instances in GGLocator.
|
|
3
|
+
*
|
|
4
|
+
* This enables callOn(ContractClass) to work without each protocol (HTTP, WebSocket, etc.)
|
|
5
|
+
* needing to manually register the contract instance.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {GGContractClass} from "@grest-ts/schema";
|
|
9
|
+
import {GGLocator, GGLocatorKey} from "@grest-ts/locator";
|
|
10
|
+
|
|
11
|
+
// Store the original implement method
|
|
12
|
+
const originalImplement = GGContractClass.prototype.implement;
|
|
13
|
+
|
|
14
|
+
export const LOCATOR_KEY_PREFIX_FOR_CONTRACT = "@contract:"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Patched implement() that registers the returned client in GGLocator.
|
|
18
|
+
*/
|
|
19
|
+
GGContractClass.prototype.implement = function (
|
|
20
|
+
this: GGContractClass<any>,
|
|
21
|
+
instance: any,
|
|
22
|
+
options?: any
|
|
23
|
+
) {
|
|
24
|
+
const contractName = this.name;
|
|
25
|
+
|
|
26
|
+
// Get the client from original implement
|
|
27
|
+
const client = originalImplement.call(this, instance, options);
|
|
28
|
+
|
|
29
|
+
// Register in GGLocator for callOn(Contract) access
|
|
30
|
+
const scope = GGLocator.tryGetScope();
|
|
31
|
+
if (scope) {
|
|
32
|
+
const key = new GGLocatorKey<typeof client>(LOCATOR_KEY_PREFIX_FOR_CONTRACT + contractName);
|
|
33
|
+
if (scope.has(key)) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Contract '${contractName}' is already registered in this scope. ` +
|
|
36
|
+
`If you need multiple instances, use different contract names.`
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
scope.set(key, client);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return client;
|
|
43
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test action for invoking methods via GGLocator lookup.
|
|
3
|
+
*
|
|
4
|
+
* Used for:
|
|
5
|
+
* - @testable services (key: @testable:ClassName)
|
|
6
|
+
* - Direct contract calls (key: @contract:ContractName)
|
|
7
|
+
* - Custom GGLocatorKey lookups
|
|
8
|
+
*
|
|
9
|
+
* Extends GGTestAction to support .with() for mocks/spies and
|
|
10
|
+
* response expectations like .toMatchObject(), .toEqual().
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {GGTestAction, GGTestActionConfig, tActionRawData} from "../testers/GGTestAction";
|
|
14
|
+
import {GG_TEST_RUNNER} from "../GGTestRunner";
|
|
15
|
+
import {SerializedContext, TestableInvokeResult, TestableIPC} from "./TestableIPC";
|
|
16
|
+
import {GGTestError} from "../utils/GGTestError";
|
|
17
|
+
import {GGContext} from "@grest-ts/context";
|
|
18
|
+
import type {GGTestRuntime} from "../GGTestRuntime";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Serialize a GGContext to a plain object for IPC transfer.
|
|
22
|
+
* Flattens the context hierarchy into a single object.
|
|
23
|
+
*/
|
|
24
|
+
function serializeContext(ctx: GGContext): SerializedContext {
|
|
25
|
+
const result: SerializedContext = {};
|
|
26
|
+
// Access the private values map via type assertion
|
|
27
|
+
const ctxAny = ctx as any;
|
|
28
|
+
if (ctxAny.values instanceof Map) {
|
|
29
|
+
for (const [key, value] of ctxAny.values) {
|
|
30
|
+
result[key] = value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Also serialize parent context values (child values take precedence)
|
|
34
|
+
if (ctxAny.parent) {
|
|
35
|
+
const parentValues = serializeContext(ctxAny.parent);
|
|
36
|
+
return {...parentValues, ...result};
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Action for invoking a method via GGLocator lookup over IPC.
|
|
43
|
+
*
|
|
44
|
+
* @typeParam T - The expected return type of the method
|
|
45
|
+
*/
|
|
46
|
+
export class GGTestActionForLocatorOnCall<T> extends GGTestAction<T> {
|
|
47
|
+
|
|
48
|
+
private readonly keyName: string;
|
|
49
|
+
private readonly methodName: string;
|
|
50
|
+
private readonly args: any[];
|
|
51
|
+
private readonly targetRuntimes?: GGTestRuntime[];
|
|
52
|
+
|
|
53
|
+
constructor(ctx: GGContext, keyName: string, methodName: string, args: any[], targetRuntimes?: GGTestRuntime[]) {
|
|
54
|
+
const config: GGTestActionConfig = {
|
|
55
|
+
noResponse: false,
|
|
56
|
+
logData: {
|
|
57
|
+
message: `[${keyName}.${methodName}]`,
|
|
58
|
+
request: args.length > 0 ? args : undefined
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
super(ctx, config);
|
|
62
|
+
this.keyName = keyName;
|
|
63
|
+
this.methodName = methodName;
|
|
64
|
+
this.args = args;
|
|
65
|
+
this.targetRuntimes = targetRuntimes;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// -------------------------------------------------
|
|
69
|
+
// Action execution
|
|
70
|
+
// -------------------------------------------------
|
|
71
|
+
|
|
72
|
+
protected async executeAction(): Promise<tActionRawData> {
|
|
73
|
+
const runner = GG_TEST_RUNNER.get();
|
|
74
|
+
|
|
75
|
+
// Use target runtimes if provided, otherwise all runtimes
|
|
76
|
+
const searchRuntimes = this.targetRuntimes ?? runner.getAllRuntimes();
|
|
77
|
+
|
|
78
|
+
// Find runtimes that have this key registered
|
|
79
|
+
const candidates = searchRuntimes.filter(r => r.hasLocatorKey(this.keyName));
|
|
80
|
+
|
|
81
|
+
if (candidates.length === 0) {
|
|
82
|
+
// No runtime has this key - provide helpful error
|
|
83
|
+
const allRuntimes = runner.getAllRuntimes();
|
|
84
|
+
if (allRuntimes.length === 0) {
|
|
85
|
+
throw new GGTestError({
|
|
86
|
+
test: `No runtimes available to invoke ${this.keyName}.${this.methodName}`,
|
|
87
|
+
expected: "At least one runtime to be started",
|
|
88
|
+
received: "No runtimes found"
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
throw new GGTestError({
|
|
92
|
+
test: `Instance '${this.keyName}' not found in any runtime`,
|
|
93
|
+
expected: `A @testable instance registered with key '${this.keyName}'`,
|
|
94
|
+
received: `Key not registered in any of ${allRuntimes.length} runtime(s): ${allRuntimes.map(r => r.name).join(', ')}`
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (candidates.length > 1) {
|
|
99
|
+
// Check if all candidates are the same runtime class - if so, just use first one
|
|
100
|
+
const uniqueClassNames = new Set(candidates.map(r => r.className));
|
|
101
|
+
if (uniqueClassNames.size > 1) {
|
|
102
|
+
// Different runtime classes have this key - actual ambiguity
|
|
103
|
+
throw new GGTestError({
|
|
104
|
+
test: `Multiple different runtimes have '${this.keyName}' registered`,
|
|
105
|
+
expected: `Key '${this.keyName}' to be unique across different runtime classes, or use explicit runtime targeting`,
|
|
106
|
+
received: `Found in ${candidates.length} runtimes with different classes: ${[...uniqueClassNames].join(', ')}`
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// Same runtime class - just use first instance (they're identical)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Exactly one runtime has this key - send IPC directly
|
|
113
|
+
const runtime = candidates[0];
|
|
114
|
+
const context = this.ctx ? serializeContext(this.ctx) : undefined;
|
|
115
|
+
|
|
116
|
+
const result: TestableInvokeResult = await runtime.sendCommand(TestableIPC.invoke, {
|
|
117
|
+
keyName: this.keyName,
|
|
118
|
+
methodName: this.methodName,
|
|
119
|
+
args: this.args,
|
|
120
|
+
context
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (result.success) {
|
|
124
|
+
return result.result as tActionRawData;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw new GGTestError({
|
|
128
|
+
test: `Error invoking ${this.keyName}.${this.methodName}`,
|
|
129
|
+
expected: "Method to execute successfully",
|
|
130
|
+
received: result.error || "Unknown error",
|
|
131
|
+
extra: result.stack
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IPC protocol definitions for GGLocator-based service invocation.
|
|
3
|
+
*
|
|
4
|
+
* Defines the messages exchanged between test runner and runtime worker
|
|
5
|
+
* for invoking methods on registered instances (@testable, @contract, etc.).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {IPCClient, IPCServer} from "@grest-ts/ipc";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Serialized context data for IPC transfer.
|
|
12
|
+
* Contains flattened key-value pairs from GGContext.
|
|
13
|
+
*/
|
|
14
|
+
export type SerializedContext = Record<string, any>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Payload for invoking a method on a registered instance.
|
|
18
|
+
*/
|
|
19
|
+
export interface TestableInvokePayload {
|
|
20
|
+
/** The GGLocatorKey name (e.g., "@testable:ServiceB", "@contract:ChainApi", or custom key) */
|
|
21
|
+
keyName: string;
|
|
22
|
+
/** The method name to invoke */
|
|
23
|
+
methodName: string;
|
|
24
|
+
/** Arguments to pass to the method (as array) */
|
|
25
|
+
args: any[];
|
|
26
|
+
/** Serialized GGContext data to restore on the worker side */
|
|
27
|
+
context?: SerializedContext;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Result from invoking a testable method.
|
|
32
|
+
*/
|
|
33
|
+
export interface TestableInvokeResult {
|
|
34
|
+
/** Whether the invocation succeeded */
|
|
35
|
+
success: boolean;
|
|
36
|
+
/** The return value from the method (if success) */
|
|
37
|
+
result?: any;
|
|
38
|
+
/** Error message (if failed) */
|
|
39
|
+
error?: string;
|
|
40
|
+
/** Error stack trace (if failed) */
|
|
41
|
+
stack?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Payload for registering locator keys from a runtime.
|
|
46
|
+
*/
|
|
47
|
+
export interface KeyRegistrationPayload {
|
|
48
|
+
/** Runtime ID sending the registration */
|
|
49
|
+
runtimeId: string;
|
|
50
|
+
/** All GGLocatorKey names registered in this runtime */
|
|
51
|
+
keys: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* IPC endpoints for testable service invocation.
|
|
56
|
+
*/
|
|
57
|
+
export const TestableIPC = {
|
|
58
|
+
/**
|
|
59
|
+
* Messages FROM test server TO worker.
|
|
60
|
+
*/
|
|
61
|
+
client: {
|
|
62
|
+
/**
|
|
63
|
+
* Invoke a method on a testable service instance.
|
|
64
|
+
*/
|
|
65
|
+
invoke: IPCClient.defineRequest<TestableInvokePayload, TestableInvokeResult>("testable/invoke"),
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Messages FROM worker TO test server.
|
|
70
|
+
*/
|
|
71
|
+
server: {
|
|
72
|
+
/**
|
|
73
|
+
* Register all locator keys available in a runtime.
|
|
74
|
+
* Sent from worker to test runner after compose completes.
|
|
75
|
+
*/
|
|
76
|
+
registerKeys: IPCServer.defineRequest<KeyRegistrationPayload, void>("testable/register-keys"),
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
// Legacy alias for backwards compatibility
|
|
80
|
+
invoke: IPCClient.defineRequest<TestableInvokePayload, TestableInvokeResult>("testable/invoke"),
|
|
81
|
+
};
|