@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/GGTestRuntime.ts
CHANGED
|
@@ -1,265 +1,265 @@
|
|
|
1
|
-
import {GGLog} from "@grest-ts/logger";
|
|
2
|
-
import {withTimeout} from "@grest-ts/common";
|
|
3
|
-
import {GGContext} from "@grest-ts/context";
|
|
4
|
-
import {GG_TRACE} from "@grest-ts/trace";
|
|
5
|
-
import {IPCClientRequest} from "@grest-ts/ipc";
|
|
6
|
-
import type {RuntimeRunner} from "./runner/RuntimeRunner";
|
|
7
|
-
import {InlineRunner} from "./runner/InlineRunner";
|
|
8
|
-
import {WorkerRunner} from "./runner/WorkerRunner";
|
|
9
|
-
import {IsolatedRunner} from "./runner/IsolatedRunner";
|
|
10
|
-
import type {GGTestRunner} from "./GGTestRunner";
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Lifecycle state of a runtime instance.
|
|
14
|
-
*/
|
|
15
|
-
export enum GGTestRuntimeState {
|
|
16
|
-
/** Initial state, not yet started */
|
|
17
|
-
CREATED = 'created',
|
|
18
|
-
/** Running successfully */
|
|
19
|
-
STARTED = 'started',
|
|
20
|
-
/** Startup failed, but worker/IPC still alive for diagnostics */
|
|
21
|
-
FAILED = 'failed',
|
|
22
|
-
/** Runtime stopped, but worker/IPC still alive for log retrieval */
|
|
23
|
-
STOPPED = 'stopped',
|
|
24
|
-
/** Fully shut down, no IPC available */
|
|
25
|
-
SHUTDOWN = 'shutdown',
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface GGTestEnvConfig {
|
|
29
|
-
executablePath: string;
|
|
30
|
-
className: string;
|
|
31
|
-
testRouterPort: number;
|
|
32
|
-
testId: string;
|
|
33
|
-
runtimeId: string;
|
|
34
|
-
initialCommands: GGTestCommand[];
|
|
35
|
-
/** When true, the runtime runs inline (same process). Affects module loading strategy. */
|
|
36
|
-
inline?: boolean;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface GGTestCommand<Payload = unknown> {
|
|
40
|
-
method: string;
|
|
41
|
-
payload: Payload;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface GGTestRuntimeConfig {
|
|
45
|
-
mode?: GGTestMode
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export enum GGTestMode {
|
|
49
|
-
INLINE = 'INLINE',
|
|
50
|
-
WORKER = 'WORKER',
|
|
51
|
-
ISOLATED = 'ISOLATED'
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export class GGTestRuntime {
|
|
55
|
-
|
|
56
|
-
public readonly runner: GGTestRunner;
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Unique identifier for this runtime instance.
|
|
60
|
-
* Used for targeted communication (e.g., "checklist-0", "checklist-1").
|
|
61
|
-
*/
|
|
62
|
-
public readonly runtimeId: string
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* The runtime name used for selector access (e.g., "checklist").
|
|
66
|
-
* This is the static NAME property from the runtime class.
|
|
67
|
-
*/
|
|
68
|
-
public readonly name: string
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* The class name of the runtime (e.g., "ChecklistRuntime").
|
|
72
|
-
* Used for file matching and logging.
|
|
73
|
-
*/
|
|
74
|
-
public readonly className: string
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Lifecycle state of this runtime instance.
|
|
78
|
-
*/
|
|
79
|
-
private _state: GGTestRuntimeState = GGTestRuntimeState.CREATED
|
|
80
|
-
|
|
81
|
-
public get state(): GGTestRuntimeState {
|
|
82
|
-
return this._state;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Locator keys registered by this runtime.
|
|
87
|
-
* Populated via IPC after compose completes.
|
|
88
|
-
*/
|
|
89
|
-
private readonly registeredLocatorKeys: Set<string> = new Set()
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Commands to be sent to this runtime on startup.
|
|
93
|
-
* Queued before start(), passed to worker via env.
|
|
94
|
-
*/
|
|
95
|
-
private readonly initialCommands: GGTestCommand[] = []
|
|
96
|
-
|
|
97
|
-
private readonly executablePath: string;
|
|
98
|
-
private readonly config: GGTestRuntimeConfig;
|
|
99
|
-
private runtimeRunner?: RuntimeRunner
|
|
100
|
-
/** Factory to create the runtime without dynamic import (used by inline mode) */
|
|
101
|
-
public runtimeFactory?: () => any;
|
|
102
|
-
|
|
103
|
-
/** Counter for generating unique runtime IDs per name */
|
|
104
|
-
private static runtimeCounters: Map<string, number> = new Map();
|
|
105
|
-
|
|
106
|
-
public constructor(runner: GGTestRunner, executablePath: string, className: string, name: string, config?: GGTestRuntimeConfig) {
|
|
107
|
-
this.executablePath = executablePath;
|
|
108
|
-
this.className = className;
|
|
109
|
-
this.name = name;
|
|
110
|
-
this.runtimeId = GGTestRuntime.generateRuntimeId(name);
|
|
111
|
-
this.config = config ?? {}
|
|
112
|
-
this.config.mode ??= GGTestMode.WORKER
|
|
113
|
-
this.runner = runner
|
|
114
|
-
this.runner.addRuntime(this);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
private static generateRuntimeId(name: string): string {
|
|
118
|
-
const count = GGTestRuntime.runtimeCounters.get(name) ?? 0;
|
|
119
|
-
GGTestRuntime.runtimeCounters.set(name, count + 1);
|
|
120
|
-
return `${name}-${count}`;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
public async start(): Promise<this> {
|
|
124
|
-
if (this._state !== GGTestRuntimeState.CREATED) {
|
|
125
|
-
throw new Error("Can only start runtimes if they are in CREATED state! Current state: " + this._state);
|
|
126
|
-
}
|
|
127
|
-
await new GGContext("Test").run(async () => {
|
|
128
|
-
GG_TRACE.init();
|
|
129
|
-
GGLog.debug(this, 'Launching ' + this.className + ' in ' + this.config.mode + ' mode...')
|
|
130
|
-
|
|
131
|
-
const config: GGTestEnvConfig = {
|
|
132
|
-
executablePath: this.executablePath,
|
|
133
|
-
className: this.className,
|
|
134
|
-
testRouterPort: this.runner.ipcServer.getPort(),
|
|
135
|
-
testId: this.runner.testId,
|
|
136
|
-
runtimeId: this.runtimeId,
|
|
137
|
-
initialCommands: this.initialCommands
|
|
138
|
-
}
|
|
139
|
-
switch (this.config.mode) {
|
|
140
|
-
case GGTestMode.INLINE:
|
|
141
|
-
config.inline = true;
|
|
142
|
-
this.runtimeRunner = new InlineRunner(config, this.runtimeFactory);
|
|
143
|
-
break;
|
|
144
|
-
case GGTestMode.WORKER:
|
|
145
|
-
this.runtimeRunner = new WorkerRunner(config);
|
|
146
|
-
break;
|
|
147
|
-
case GGTestMode.ISOLATED:
|
|
148
|
-
this.runtimeRunner = new IsolatedRunner(config);
|
|
149
|
-
break;
|
|
150
|
-
default:
|
|
151
|
-
throw new Error(`Unknown test mode: ${this.config.mode}`);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const startupTimeout = 30000;
|
|
155
|
-
try {
|
|
156
|
-
await withTimeout(
|
|
157
|
-
this.runtimeRunner.start(),
|
|
158
|
-
startupTimeout,
|
|
159
|
-
'Service ' + this.className + ' failed to start within ' + startupTimeout + 'ms'
|
|
160
|
-
);
|
|
161
|
-
this._state = GGTestRuntimeState.STARTED;
|
|
162
|
-
GGLog.debug(this, this.className + ' started successfully')
|
|
163
|
-
} catch (error) {
|
|
164
|
-
// Mark as failed but keep runner alive for diagnostics (e.g., log retrieval)
|
|
165
|
-
this._state = GGTestRuntimeState.FAILED;
|
|
166
|
-
throw error;
|
|
167
|
-
}
|
|
168
|
-
});
|
|
169
|
-
return this
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Stop the GGRuntime (teardown services) but keep worker/IPC alive.
|
|
174
|
-
* This allows log retrieval after the runtime has stopped.
|
|
175
|
-
* Idempotent - safe to call multiple times.
|
|
176
|
-
*/
|
|
177
|
-
public async stop(): Promise<void> {
|
|
178
|
-
await new GGContext("Test").run(async () => {
|
|
179
|
-
GG_TRACE.init();
|
|
180
|
-
if (this._state === GGTestRuntimeState.STOPPED || this._state === GGTestRuntimeState.SHUTDOWN) {
|
|
181
|
-
// Already stopped or shutdown, nothing to do
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
if (this._state === GGTestRuntimeState.FAILED) {
|
|
185
|
-
// Already failed, just mark as stopped
|
|
186
|
-
this._state = GGTestRuntimeState.STOPPED;
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
if (this._state === GGTestRuntimeState.CREATED) {
|
|
190
|
-
// Never started (e.g., another runtime or hook failed first), nothing to stop
|
|
191
|
-
this._state = GGTestRuntimeState.STOPPED;
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
if (this._state !== GGTestRuntimeState.STARTED) {
|
|
195
|
-
throw new Error("Can only stop runtimes in STARTED state! Current state: " + this._state);
|
|
196
|
-
}
|
|
197
|
-
GGLog.debug(this, 'Stopping ' + this.className + '...')
|
|
198
|
-
try {
|
|
199
|
-
await this.runtimeRunner?.stopRuntime()
|
|
200
|
-
} catch (error) {
|
|
201
|
-
GGLog.error(this, 'Error stopping ' + this.className, error)
|
|
202
|
-
}
|
|
203
|
-
this._state = GGTestRuntimeState.STOPPED;
|
|
204
|
-
GGLog.debug(this, this.className + ' stopped')
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Fully shutdown the runtime and worker. IPC will be disconnected.
|
|
210
|
-
* After this, no commands can be sent.
|
|
211
|
-
*/
|
|
212
|
-
public async shutdown(): Promise<void> {
|
|
213
|
-
await new GGContext("Test").run(async () => {
|
|
214
|
-
GG_TRACE.init();
|
|
215
|
-
if (this._state === GGTestRuntimeState.SHUTDOWN) {
|
|
216
|
-
return; // Already shutdown
|
|
217
|
-
}
|
|
218
|
-
GGLog.debug(this, 'Shutting down ' + this.className + '...')
|
|
219
|
-
try {
|
|
220
|
-
await this.runtimeRunner?.shutdown()
|
|
221
|
-
} catch (error) {
|
|
222
|
-
GGLog.error(this, 'Error shutting down ' + this.className, error)
|
|
223
|
-
}
|
|
224
|
-
this.runtimeRunner = undefined
|
|
225
|
-
this._state = GGTestRuntimeState.SHUTDOWN;
|
|
226
|
-
GGLog.debug(this, this.className + ' shut down')
|
|
227
|
-
});
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
public async sendCommand<Payload, Result>(type: IPCClientRequest<Payload, Result>, payload: Payload): Promise<Result> {
|
|
231
|
-
switch (this._state) {
|
|
232
|
-
case GGTestRuntimeState.CREATED:
|
|
233
|
-
this.initialCommands.push({method: type, payload: payload});
|
|
234
|
-
return undefined as Result;
|
|
235
|
-
case GGTestRuntimeState.STARTED:
|
|
236
|
-
case GGTestRuntimeState.FAILED:
|
|
237
|
-
case GGTestRuntimeState.STOPPED:
|
|
238
|
-
// IPC still available in these states
|
|
239
|
-
return await this.runner.ipcServer.sendFrameworkMessage(this.runtimeId, type, payload);
|
|
240
|
-
case GGTestRuntimeState.SHUTDOWN:
|
|
241
|
-
throw new Error(`Cannot send command to shut down runtime ${this.className}`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// -----------
|
|
246
|
-
// Key registration (for callOn routing)
|
|
247
|
-
// -----------
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Register multiple locator keys at once.
|
|
251
|
-
*/
|
|
252
|
-
public registerLocatorKeys(keys: string[]): void {
|
|
253
|
-
for (const key of keys) {
|
|
254
|
-
this.registeredLocatorKeys.add(key);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Check if this runtime has a specific locator key.
|
|
260
|
-
*/
|
|
261
|
-
public hasLocatorKey(key: string): boolean {
|
|
262
|
-
return this.registeredLocatorKeys.has(key);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
}
|
|
1
|
+
import {GGLog} from "@grest-ts/logger";
|
|
2
|
+
import {withTimeout} from "@grest-ts/common";
|
|
3
|
+
import {GGContext} from "@grest-ts/context";
|
|
4
|
+
import {GG_TRACE} from "@grest-ts/trace";
|
|
5
|
+
import {IPCClientRequest} from "@grest-ts/ipc";
|
|
6
|
+
import type {RuntimeRunner} from "./runner/RuntimeRunner";
|
|
7
|
+
import {InlineRunner} from "./runner/InlineRunner";
|
|
8
|
+
import {WorkerRunner} from "./runner/WorkerRunner";
|
|
9
|
+
import {IsolatedRunner} from "./runner/IsolatedRunner";
|
|
10
|
+
import type {GGTestRunner} from "./GGTestRunner";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Lifecycle state of a runtime instance.
|
|
14
|
+
*/
|
|
15
|
+
export enum GGTestRuntimeState {
|
|
16
|
+
/** Initial state, not yet started */
|
|
17
|
+
CREATED = 'created',
|
|
18
|
+
/** Running successfully */
|
|
19
|
+
STARTED = 'started',
|
|
20
|
+
/** Startup failed, but worker/IPC still alive for diagnostics */
|
|
21
|
+
FAILED = 'failed',
|
|
22
|
+
/** Runtime stopped, but worker/IPC still alive for log retrieval */
|
|
23
|
+
STOPPED = 'stopped',
|
|
24
|
+
/** Fully shut down, no IPC available */
|
|
25
|
+
SHUTDOWN = 'shutdown',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface GGTestEnvConfig {
|
|
29
|
+
executablePath: string;
|
|
30
|
+
className: string;
|
|
31
|
+
testRouterPort: number;
|
|
32
|
+
testId: string;
|
|
33
|
+
runtimeId: string;
|
|
34
|
+
initialCommands: GGTestCommand[];
|
|
35
|
+
/** When true, the runtime runs inline (same process). Affects module loading strategy. */
|
|
36
|
+
inline?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface GGTestCommand<Payload = unknown> {
|
|
40
|
+
method: string;
|
|
41
|
+
payload: Payload;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface GGTestRuntimeConfig {
|
|
45
|
+
mode?: GGTestMode
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export enum GGTestMode {
|
|
49
|
+
INLINE = 'INLINE',
|
|
50
|
+
WORKER = 'WORKER',
|
|
51
|
+
ISOLATED = 'ISOLATED'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class GGTestRuntime {
|
|
55
|
+
|
|
56
|
+
public readonly runner: GGTestRunner;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Unique identifier for this runtime instance.
|
|
60
|
+
* Used for targeted communication (e.g., "checklist-0", "checklist-1").
|
|
61
|
+
*/
|
|
62
|
+
public readonly runtimeId: string
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The runtime name used for selector access (e.g., "checklist").
|
|
66
|
+
* This is the static NAME property from the runtime class.
|
|
67
|
+
*/
|
|
68
|
+
public readonly name: string
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The class name of the runtime (e.g., "ChecklistRuntime").
|
|
72
|
+
* Used for file matching and logging.
|
|
73
|
+
*/
|
|
74
|
+
public readonly className: string
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Lifecycle state of this runtime instance.
|
|
78
|
+
*/
|
|
79
|
+
private _state: GGTestRuntimeState = GGTestRuntimeState.CREATED
|
|
80
|
+
|
|
81
|
+
public get state(): GGTestRuntimeState {
|
|
82
|
+
return this._state;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Locator keys registered by this runtime.
|
|
87
|
+
* Populated via IPC after compose completes.
|
|
88
|
+
*/
|
|
89
|
+
private readonly registeredLocatorKeys: Set<string> = new Set()
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Commands to be sent to this runtime on startup.
|
|
93
|
+
* Queued before start(), passed to worker via env.
|
|
94
|
+
*/
|
|
95
|
+
private readonly initialCommands: GGTestCommand[] = []
|
|
96
|
+
|
|
97
|
+
private readonly executablePath: string;
|
|
98
|
+
private readonly config: GGTestRuntimeConfig;
|
|
99
|
+
private runtimeRunner?: RuntimeRunner
|
|
100
|
+
/** Factory to create the runtime without dynamic import (used by inline mode) */
|
|
101
|
+
public runtimeFactory?: () => any;
|
|
102
|
+
|
|
103
|
+
/** Counter for generating unique runtime IDs per name */
|
|
104
|
+
private static runtimeCounters: Map<string, number> = new Map();
|
|
105
|
+
|
|
106
|
+
public constructor(runner: GGTestRunner, executablePath: string, className: string, name: string, config?: GGTestRuntimeConfig) {
|
|
107
|
+
this.executablePath = executablePath;
|
|
108
|
+
this.className = className;
|
|
109
|
+
this.name = name;
|
|
110
|
+
this.runtimeId = GGTestRuntime.generateRuntimeId(name);
|
|
111
|
+
this.config = config ?? {}
|
|
112
|
+
this.config.mode ??= GGTestMode.WORKER
|
|
113
|
+
this.runner = runner
|
|
114
|
+
this.runner.addRuntime(this);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private static generateRuntimeId(name: string): string {
|
|
118
|
+
const count = GGTestRuntime.runtimeCounters.get(name) ?? 0;
|
|
119
|
+
GGTestRuntime.runtimeCounters.set(name, count + 1);
|
|
120
|
+
return `${name}-${count}`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public async start(): Promise<this> {
|
|
124
|
+
if (this._state !== GGTestRuntimeState.CREATED) {
|
|
125
|
+
throw new Error("Can only start runtimes if they are in CREATED state! Current state: " + this._state);
|
|
126
|
+
}
|
|
127
|
+
await new GGContext("Test").run(async () => {
|
|
128
|
+
GG_TRACE.init();
|
|
129
|
+
GGLog.debug(this, 'Launching ' + this.className + ' in ' + this.config.mode + ' mode...')
|
|
130
|
+
|
|
131
|
+
const config: GGTestEnvConfig = {
|
|
132
|
+
executablePath: this.executablePath,
|
|
133
|
+
className: this.className,
|
|
134
|
+
testRouterPort: this.runner.ipcServer.getPort(),
|
|
135
|
+
testId: this.runner.testId,
|
|
136
|
+
runtimeId: this.runtimeId,
|
|
137
|
+
initialCommands: this.initialCommands
|
|
138
|
+
}
|
|
139
|
+
switch (this.config.mode) {
|
|
140
|
+
case GGTestMode.INLINE:
|
|
141
|
+
config.inline = true;
|
|
142
|
+
this.runtimeRunner = new InlineRunner(config, this.runtimeFactory);
|
|
143
|
+
break;
|
|
144
|
+
case GGTestMode.WORKER:
|
|
145
|
+
this.runtimeRunner = new WorkerRunner(config);
|
|
146
|
+
break;
|
|
147
|
+
case GGTestMode.ISOLATED:
|
|
148
|
+
this.runtimeRunner = new IsolatedRunner(config);
|
|
149
|
+
break;
|
|
150
|
+
default:
|
|
151
|
+
throw new Error(`Unknown test mode: ${this.config.mode}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const startupTimeout = 30000;
|
|
155
|
+
try {
|
|
156
|
+
await withTimeout(
|
|
157
|
+
this.runtimeRunner.start(),
|
|
158
|
+
startupTimeout,
|
|
159
|
+
'Service ' + this.className + ' failed to start within ' + startupTimeout + 'ms'
|
|
160
|
+
);
|
|
161
|
+
this._state = GGTestRuntimeState.STARTED;
|
|
162
|
+
GGLog.debug(this, this.className + ' started successfully')
|
|
163
|
+
} catch (error) {
|
|
164
|
+
// Mark as failed but keep runner alive for diagnostics (e.g., log retrieval)
|
|
165
|
+
this._state = GGTestRuntimeState.FAILED;
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
return this
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Stop the GGRuntime (teardown services) but keep worker/IPC alive.
|
|
174
|
+
* This allows log retrieval after the runtime has stopped.
|
|
175
|
+
* Idempotent - safe to call multiple times.
|
|
176
|
+
*/
|
|
177
|
+
public async stop(): Promise<void> {
|
|
178
|
+
await new GGContext("Test").run(async () => {
|
|
179
|
+
GG_TRACE.init();
|
|
180
|
+
if (this._state === GGTestRuntimeState.STOPPED || this._state === GGTestRuntimeState.SHUTDOWN) {
|
|
181
|
+
// Already stopped or shutdown, nothing to do
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (this._state === GGTestRuntimeState.FAILED) {
|
|
185
|
+
// Already failed, just mark as stopped
|
|
186
|
+
this._state = GGTestRuntimeState.STOPPED;
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
if (this._state === GGTestRuntimeState.CREATED) {
|
|
190
|
+
// Never started (e.g., another runtime or hook failed first), nothing to stop
|
|
191
|
+
this._state = GGTestRuntimeState.STOPPED;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (this._state !== GGTestRuntimeState.STARTED) {
|
|
195
|
+
throw new Error("Can only stop runtimes in STARTED state! Current state: " + this._state);
|
|
196
|
+
}
|
|
197
|
+
GGLog.debug(this, 'Stopping ' + this.className + '...')
|
|
198
|
+
try {
|
|
199
|
+
await this.runtimeRunner?.stopRuntime()
|
|
200
|
+
} catch (error) {
|
|
201
|
+
GGLog.error(this, 'Error stopping ' + this.className, error)
|
|
202
|
+
}
|
|
203
|
+
this._state = GGTestRuntimeState.STOPPED;
|
|
204
|
+
GGLog.debug(this, this.className + ' stopped')
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Fully shutdown the runtime and worker. IPC will be disconnected.
|
|
210
|
+
* After this, no commands can be sent.
|
|
211
|
+
*/
|
|
212
|
+
public async shutdown(): Promise<void> {
|
|
213
|
+
await new GGContext("Test").run(async () => {
|
|
214
|
+
GG_TRACE.init();
|
|
215
|
+
if (this._state === GGTestRuntimeState.SHUTDOWN) {
|
|
216
|
+
return; // Already shutdown
|
|
217
|
+
}
|
|
218
|
+
GGLog.debug(this, 'Shutting down ' + this.className + '...')
|
|
219
|
+
try {
|
|
220
|
+
await this.runtimeRunner?.shutdown()
|
|
221
|
+
} catch (error) {
|
|
222
|
+
GGLog.error(this, 'Error shutting down ' + this.className, error)
|
|
223
|
+
}
|
|
224
|
+
this.runtimeRunner = undefined
|
|
225
|
+
this._state = GGTestRuntimeState.SHUTDOWN;
|
|
226
|
+
GGLog.debug(this, this.className + ' shut down')
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
public async sendCommand<Payload, Result>(type: IPCClientRequest<Payload, Result>, payload: Payload): Promise<Result> {
|
|
231
|
+
switch (this._state) {
|
|
232
|
+
case GGTestRuntimeState.CREATED:
|
|
233
|
+
this.initialCommands.push({method: type, payload: payload});
|
|
234
|
+
return undefined as Result;
|
|
235
|
+
case GGTestRuntimeState.STARTED:
|
|
236
|
+
case GGTestRuntimeState.FAILED:
|
|
237
|
+
case GGTestRuntimeState.STOPPED:
|
|
238
|
+
// IPC still available in these states
|
|
239
|
+
return await this.runner.ipcServer.sendFrameworkMessage(this.runtimeId, type, payload);
|
|
240
|
+
case GGTestRuntimeState.SHUTDOWN:
|
|
241
|
+
throw new Error(`Cannot send command to shut down runtime ${this.className}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// -----------
|
|
246
|
+
// Key registration (for callOn routing)
|
|
247
|
+
// -----------
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Register multiple locator keys at once.
|
|
251
|
+
*/
|
|
252
|
+
public registerLocatorKeys(keys: string[]): void {
|
|
253
|
+
for (const key of keys) {
|
|
254
|
+
this.registeredLocatorKeys.add(key);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Check if this runtime has a specific locator key.
|
|
260
|
+
*/
|
|
261
|
+
public hasLocatorKey(key: string): boolean {
|
|
262
|
+
return this.registeredLocatorKeys.has(key);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
}
|