@grest-ts/testkit 0.0.6 → 0.0.8
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 +413 -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 +13 -13
- 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
|
@@ -1,179 +1,179 @@
|
|
|
1
|
-
import type {RuntimeRunner} from "./RuntimeRunner";
|
|
2
|
-
import {Worker} from "worker_threads";
|
|
3
|
-
import {GGLog} from "@grest-ts/logger";
|
|
4
|
-
import {GGTestEnvConfig} from "../GGTestRuntime";
|
|
5
|
-
|
|
6
|
-
export class WorkerRunner implements RuntimeRunner {
|
|
7
|
-
private worker?: Worker;
|
|
8
|
-
|
|
9
|
-
private static workerLoaderPath: string | undefined;
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Set the path to the worker-loader.mjs file.
|
|
13
|
-
* Called by @grest-ts/testkit-vitest to inject the path.
|
|
14
|
-
*/
|
|
15
|
-
public static setWorkerLoaderPath(path: string): void {
|
|
16
|
-
this.workerLoaderPath = path;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
constructor(private readonly config: GGTestEnvConfig) {
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async start(): Promise<void> {
|
|
23
|
-
GGLog.debug(this, 'Starting worker: ' + this.config.executablePath);
|
|
24
|
-
|
|
25
|
-
if (!WorkerRunner.workerLoaderPath) {
|
|
26
|
-
throw new Error(
|
|
27
|
-
"Worker loader path not set!\n" +
|
|
28
|
-
"Make sure to import '@grest-ts/testkit-vitest' in your vitest setup."
|
|
29
|
-
);
|
|
30
|
-
}
|
|
31
|
-
const workerLoaderPath = WorkerRunner.workerLoaderPath;
|
|
32
|
-
|
|
33
|
-
this.worker = new Worker(workerLoaderPath, {
|
|
34
|
-
workerData: this.config,
|
|
35
|
-
stdout: true,
|
|
36
|
-
stderr: true
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// Forward worker stdout to parent's console using console.log
|
|
40
|
-
// so vitest can capture and sequence it properly with test output
|
|
41
|
-
this.worker.stdout.on('data', (data: Buffer) => {
|
|
42
|
-
const str = data.toString();
|
|
43
|
-
// Split by newlines and log each line (trimming trailing newline)
|
|
44
|
-
const lines = str.split('\n');
|
|
45
|
-
for (const line of lines) {
|
|
46
|
-
if (line) console.log(line);
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// Forward worker stderr to parent's console
|
|
51
|
-
this.worker.stderr.on('data', (data: Buffer) => {
|
|
52
|
-
const str = data.toString();
|
|
53
|
-
const lines = str.split('\n');
|
|
54
|
-
for (const line of lines) {
|
|
55
|
-
if (line) console.error(line);
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// Wait for worker to signal ready
|
|
60
|
-
try {
|
|
61
|
-
await new Promise<void>((resolve, reject) => {
|
|
62
|
-
if (!this.worker) {
|
|
63
|
-
reject(new Error('Worker failed to start'));
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const timeout = setTimeout(() => {
|
|
68
|
-
// CRITICAL: Terminate worker on timeout to prevent leak
|
|
69
|
-
this.worker?.terminate();
|
|
70
|
-
reject(new Error('Worker did not start within 10 seconds'));
|
|
71
|
-
}, 10000);
|
|
72
|
-
|
|
73
|
-
const messageHandler = (msg: any) => {
|
|
74
|
-
if (msg.type === 'ready') {
|
|
75
|
-
clearTimeout(timeout);
|
|
76
|
-
this.worker?.off('message', messageHandler);
|
|
77
|
-
// Monitor for unexpected worker exit after successful startup
|
|
78
|
-
this.worker?.on('exit', (code) => {
|
|
79
|
-
GGLog.error(this, `Worker exited unexpectedly with code ${code}`);
|
|
80
|
-
});
|
|
81
|
-
GGLog.debug(this, 'Worker ready');
|
|
82
|
-
resolve();
|
|
83
|
-
} else if (msg.type === 'error') {
|
|
84
|
-
clearTimeout(timeout);
|
|
85
|
-
this.worker?.off('message', messageHandler);
|
|
86
|
-
this.worker?.terminate();
|
|
87
|
-
reject(new Error(`Runtime worker startup failed! ${msg.error}`));
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
this.worker.on('message', messageHandler);
|
|
92
|
-
|
|
93
|
-
this.worker.once('error', (err) => {
|
|
94
|
-
clearTimeout(timeout);
|
|
95
|
-
this.worker?.terminate();
|
|
96
|
-
reject(err);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
this.worker.once('exit', (code) => {
|
|
100
|
-
if (code !== 0) {
|
|
101
|
-
clearTimeout(timeout);
|
|
102
|
-
reject(new Error(`Worker exited with code ${code}`));
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
} catch (error) {
|
|
107
|
-
// Ensure worker is cleaned up on any error
|
|
108
|
-
this.worker = undefined;
|
|
109
|
-
throw error;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
async stopRuntime(): Promise<void> {
|
|
114
|
-
if (!this.worker) {
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
GGLog.debug(this, 'Stopping runtime in worker...');
|
|
119
|
-
|
|
120
|
-
return new Promise<void>((resolve, reject) => {
|
|
121
|
-
if (!this.worker) {
|
|
122
|
-
resolve();
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const timeout = setTimeout(() => {
|
|
127
|
-
GGLog.error(this, 'Timeout waiting for runtime to stop');
|
|
128
|
-
reject(new Error('Timeout waiting for runtime to stop'));
|
|
129
|
-
}, 5000);
|
|
130
|
-
|
|
131
|
-
const messageHandler = (msg: any) => {
|
|
132
|
-
if (msg.type === 'runtimeStopped') {
|
|
133
|
-
clearTimeout(timeout);
|
|
134
|
-
this.worker?.off('message', messageHandler);
|
|
135
|
-
GGLog.debug(this, 'Runtime stopped in worker');
|
|
136
|
-
resolve();
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
this.worker.on('message', messageHandler);
|
|
141
|
-
this.worker.postMessage({type: 'stopRuntime'});
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async shutdown(): Promise<void> {
|
|
146
|
-
if (!this.worker) {
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
GGLog.debug(this, 'Shutting down worker...');
|
|
151
|
-
|
|
152
|
-
return new Promise<void>((resolve) => {
|
|
153
|
-
if (!this.worker) {
|
|
154
|
-
resolve();
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
this.worker.once('exit', () => {
|
|
159
|
-
clearTimeout(forceTerminateTimeout);
|
|
160
|
-
this.worker = undefined;
|
|
161
|
-
GGLog.debug(this, 'Worker shut down');
|
|
162
|
-
resolve();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Send shutdown message for graceful shutdown
|
|
166
|
-
this.worker.postMessage({type: 'shutdown'});
|
|
167
|
-
|
|
168
|
-
// Force terminate after 2 seconds if graceful shutdown doesn't work
|
|
169
|
-
const forceTerminateTimeout = setTimeout(() => {
|
|
170
|
-
if (this.worker) {
|
|
171
|
-
GGLog.debug(this, 'Force terminating worker (timeout)');
|
|
172
|
-
this.worker.terminate();
|
|
173
|
-
this.worker = undefined;
|
|
174
|
-
resolve();
|
|
175
|
-
}
|
|
176
|
-
}, 2000);
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
}
|
|
1
|
+
import type {RuntimeRunner} from "./RuntimeRunner";
|
|
2
|
+
import {Worker} from "worker_threads";
|
|
3
|
+
import {GGLog} from "@grest-ts/logger";
|
|
4
|
+
import {GGTestEnvConfig} from "../GGTestRuntime";
|
|
5
|
+
|
|
6
|
+
export class WorkerRunner implements RuntimeRunner {
|
|
7
|
+
private worker?: Worker;
|
|
8
|
+
|
|
9
|
+
private static workerLoaderPath: string | undefined;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Set the path to the worker-loader.mjs file.
|
|
13
|
+
* Called by @grest-ts/testkit-vitest to inject the path.
|
|
14
|
+
*/
|
|
15
|
+
public static setWorkerLoaderPath(path: string): void {
|
|
16
|
+
this.workerLoaderPath = path;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
constructor(private readonly config: GGTestEnvConfig) {
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async start(): Promise<void> {
|
|
23
|
+
GGLog.debug(this, 'Starting worker: ' + this.config.executablePath);
|
|
24
|
+
|
|
25
|
+
if (!WorkerRunner.workerLoaderPath) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Worker loader path not set!\n" +
|
|
28
|
+
"Make sure to import '@grest-ts/testkit-vitest' in your vitest setup."
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
const workerLoaderPath = WorkerRunner.workerLoaderPath;
|
|
32
|
+
|
|
33
|
+
this.worker = new Worker(workerLoaderPath, {
|
|
34
|
+
workerData: this.config,
|
|
35
|
+
stdout: true,
|
|
36
|
+
stderr: true
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Forward worker stdout to parent's console using console.log
|
|
40
|
+
// so vitest can capture and sequence it properly with test output
|
|
41
|
+
this.worker.stdout.on('data', (data: Buffer) => {
|
|
42
|
+
const str = data.toString();
|
|
43
|
+
// Split by newlines and log each line (trimming trailing newline)
|
|
44
|
+
const lines = str.split('\n');
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
if (line) console.log(line);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Forward worker stderr to parent's console
|
|
51
|
+
this.worker.stderr.on('data', (data: Buffer) => {
|
|
52
|
+
const str = data.toString();
|
|
53
|
+
const lines = str.split('\n');
|
|
54
|
+
for (const line of lines) {
|
|
55
|
+
if (line) console.error(line);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Wait for worker to signal ready
|
|
60
|
+
try {
|
|
61
|
+
await new Promise<void>((resolve, reject) => {
|
|
62
|
+
if (!this.worker) {
|
|
63
|
+
reject(new Error('Worker failed to start'));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const timeout = setTimeout(() => {
|
|
68
|
+
// CRITICAL: Terminate worker on timeout to prevent leak
|
|
69
|
+
this.worker?.terminate();
|
|
70
|
+
reject(new Error('Worker did not start within 10 seconds'));
|
|
71
|
+
}, 10000);
|
|
72
|
+
|
|
73
|
+
const messageHandler = (msg: any) => {
|
|
74
|
+
if (msg.type === 'ready') {
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
this.worker?.off('message', messageHandler);
|
|
77
|
+
// Monitor for unexpected worker exit after successful startup
|
|
78
|
+
this.worker?.on('exit', (code) => {
|
|
79
|
+
GGLog.error(this, `Worker exited unexpectedly with code ${code}`);
|
|
80
|
+
});
|
|
81
|
+
GGLog.debug(this, 'Worker ready');
|
|
82
|
+
resolve();
|
|
83
|
+
} else if (msg.type === 'error') {
|
|
84
|
+
clearTimeout(timeout);
|
|
85
|
+
this.worker?.off('message', messageHandler);
|
|
86
|
+
this.worker?.terminate();
|
|
87
|
+
reject(new Error(`Runtime worker startup failed! ${msg.error}`));
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this.worker.on('message', messageHandler);
|
|
92
|
+
|
|
93
|
+
this.worker.once('error', (err) => {
|
|
94
|
+
clearTimeout(timeout);
|
|
95
|
+
this.worker?.terminate();
|
|
96
|
+
reject(err);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
this.worker.once('exit', (code) => {
|
|
100
|
+
if (code !== 0) {
|
|
101
|
+
clearTimeout(timeout);
|
|
102
|
+
reject(new Error(`Worker exited with code ${code}`));
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
// Ensure worker is cleaned up on any error
|
|
108
|
+
this.worker = undefined;
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async stopRuntime(): Promise<void> {
|
|
114
|
+
if (!this.worker) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
GGLog.debug(this, 'Stopping runtime in worker...');
|
|
119
|
+
|
|
120
|
+
return new Promise<void>((resolve, reject) => {
|
|
121
|
+
if (!this.worker) {
|
|
122
|
+
resolve();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const timeout = setTimeout(() => {
|
|
127
|
+
GGLog.error(this, 'Timeout waiting for runtime to stop');
|
|
128
|
+
reject(new Error('Timeout waiting for runtime to stop'));
|
|
129
|
+
}, 5000);
|
|
130
|
+
|
|
131
|
+
const messageHandler = (msg: any) => {
|
|
132
|
+
if (msg.type === 'runtimeStopped') {
|
|
133
|
+
clearTimeout(timeout);
|
|
134
|
+
this.worker?.off('message', messageHandler);
|
|
135
|
+
GGLog.debug(this, 'Runtime stopped in worker');
|
|
136
|
+
resolve();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
this.worker.on('message', messageHandler);
|
|
141
|
+
this.worker.postMessage({type: 'stopRuntime'});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async shutdown(): Promise<void> {
|
|
146
|
+
if (!this.worker) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
GGLog.debug(this, 'Shutting down worker...');
|
|
151
|
+
|
|
152
|
+
return new Promise<void>((resolve) => {
|
|
153
|
+
if (!this.worker) {
|
|
154
|
+
resolve();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.worker.once('exit', () => {
|
|
159
|
+
clearTimeout(forceTerminateTimeout);
|
|
160
|
+
this.worker = undefined;
|
|
161
|
+
GGLog.debug(this, 'Worker shut down');
|
|
162
|
+
resolve();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Send shutdown message for graceful shutdown
|
|
166
|
+
this.worker.postMessage({type: 'shutdown'});
|
|
167
|
+
|
|
168
|
+
// Force terminate after 2 seconds if graceful shutdown doesn't work
|
|
169
|
+
const forceTerminateTimeout = setTimeout(() => {
|
|
170
|
+
if (this.worker) {
|
|
171
|
+
GGLog.debug(this, 'Force terminating worker (timeout)');
|
|
172
|
+
this.worker.terminate();
|
|
173
|
+
this.worker = undefined;
|
|
174
|
+
resolve();
|
|
175
|
+
}
|
|
176
|
+
}, 2000);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -1,91 +1,91 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Isolated process loader for ES modules
|
|
3
|
-
* This file is the entry point for isolated test processes
|
|
4
|
-
*
|
|
5
|
-
* Usage: npx tsx isolated-loader.mjs <runtime-source-url>
|
|
6
|
-
* Environment: GG_ISOLATED_CONFIG must be set with JSON config
|
|
7
|
-
*/
|
|
8
|
-
import {register} from 'tsx/esm/api';
|
|
9
|
-
import {pathToFileURL} from 'url';
|
|
10
|
-
|
|
11
|
-
// Register tsx to handle TypeScript with ESM
|
|
12
|
-
register();
|
|
13
|
-
|
|
14
|
-
// Get the runtime source URL from command line args (can be file:// URL or path)
|
|
15
|
-
let runtimeSourceUrl = process.argv[2];
|
|
16
|
-
if (!runtimeSourceUrl) {
|
|
17
|
-
console.error('Usage: npx tsx isolated-loader.mjs <runtime-source-url>');
|
|
18
|
-
process.exit(1);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Convert to file:// URL if it's a path
|
|
22
|
-
if (!runtimeSourceUrl.startsWith('file:')) {
|
|
23
|
-
runtimeSourceUrl = pathToFileURL(runtimeSourceUrl).href;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const GG_ISOLATED_CONFIG = "GG_ISOLATED_CONFIG";
|
|
27
|
-
const PROCESS_READY = "IsolatedRunner:READY";
|
|
28
|
-
|
|
29
|
-
// Get config from environment
|
|
30
|
-
const configJson = process.env[GG_ISOLATED_CONFIG];
|
|
31
|
-
if (!configJson) {
|
|
32
|
-
console.error('GG_ISOLATED_CONFIG not set');
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Import runtime and testkit dependencies
|
|
37
|
-
const {GGRuntime} = await import('@grest-ts/runtime');
|
|
38
|
-
const {GGTestRuntimeWorker} = await import('@grest-ts/testkit');
|
|
39
|
-
const {GGLog} = await import('@grest-ts/logger');
|
|
40
|
-
|
|
41
|
-
const config = JSON.parse(configJson);
|
|
42
|
-
|
|
43
|
-
// Override cli() to intercept runtime startup in isolated mode
|
|
44
|
-
|
|
45
|
-
GGRuntime.cli = async function(moduleUrl) {
|
|
46
|
-
// Still set SOURCE_MODULE_URL for testkit compatibility
|
|
47
|
-
this.SOURCE_MODULE_URL = moduleUrl;
|
|
48
|
-
|
|
49
|
-
// Start the runtime via test worker instead of normal startup
|
|
50
|
-
const controlClient = new GGTestRuntimeWorker(config);
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
await controlClient.start(() => new this());
|
|
54
|
-
|
|
55
|
-
// Signal to parent process that we're ready
|
|
56
|
-
console.log(PROCESS_READY);
|
|
57
|
-
|
|
58
|
-
const stopRuntime = async () => {
|
|
59
|
-
// Stop the GGRuntime but keep process alive for log retrieval
|
|
60
|
-
await controlClient.stopRuntime();
|
|
61
|
-
console.log('IsolatedRunner:RUNTIME_STOPPED');
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const shutdown = async () => {
|
|
65
|
-
// Fully shutdown process
|
|
66
|
-
await controlClient.shutdown();
|
|
67
|
-
process.exit(0);
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
// Listen for commands via stdin
|
|
71
|
-
process.stdin.setEncoding('utf8');
|
|
72
|
-
process.stdin.on('data', (data) => {
|
|
73
|
-
const message = data.toString().trim();
|
|
74
|
-
if (message === 'STOP_RUNTIME') {
|
|
75
|
-
GGLog.debug({name: 'IsolatedLoader'}, 'Received stop runtime command');
|
|
76
|
-
stopRuntime();
|
|
77
|
-
} else if (message === 'SHUTDOWN') {
|
|
78
|
-
GGLog.debug({name: 'IsolatedLoader'}, 'Received shutdown command');
|
|
79
|
-
shutdown();
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
process.on('SIGTERM', shutdown);
|
|
83
|
-
process.on('SIGINT', shutdown);
|
|
84
|
-
} catch (err) {
|
|
85
|
-
GGLog.error({name: 'IsolatedLoader'}, 'Failed to start isolated runtime', err);
|
|
86
|
-
process.exit(1);
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// Import the runtime source - this will call the overridden cli()
|
|
91
|
-
await import(runtimeSourceUrl);
|
|
1
|
+
/**
|
|
2
|
+
* Isolated process loader for ES modules
|
|
3
|
+
* This file is the entry point for isolated test processes
|
|
4
|
+
*
|
|
5
|
+
* Usage: npx tsx isolated-loader.mjs <runtime-source-url>
|
|
6
|
+
* Environment: GG_ISOLATED_CONFIG must be set with JSON config
|
|
7
|
+
*/
|
|
8
|
+
import {register} from 'tsx/esm/api';
|
|
9
|
+
import {pathToFileURL} from 'url';
|
|
10
|
+
|
|
11
|
+
// Register tsx to handle TypeScript with ESM
|
|
12
|
+
register();
|
|
13
|
+
|
|
14
|
+
// Get the runtime source URL from command line args (can be file:// URL or path)
|
|
15
|
+
let runtimeSourceUrl = process.argv[2];
|
|
16
|
+
if (!runtimeSourceUrl) {
|
|
17
|
+
console.error('Usage: npx tsx isolated-loader.mjs <runtime-source-url>');
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Convert to file:// URL if it's a path
|
|
22
|
+
if (!runtimeSourceUrl.startsWith('file:')) {
|
|
23
|
+
runtimeSourceUrl = pathToFileURL(runtimeSourceUrl).href;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const GG_ISOLATED_CONFIG = "GG_ISOLATED_CONFIG";
|
|
27
|
+
const PROCESS_READY = "IsolatedRunner:READY";
|
|
28
|
+
|
|
29
|
+
// Get config from environment
|
|
30
|
+
const configJson = process.env[GG_ISOLATED_CONFIG];
|
|
31
|
+
if (!configJson) {
|
|
32
|
+
console.error('GG_ISOLATED_CONFIG not set');
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Import runtime and testkit dependencies
|
|
37
|
+
const {GGRuntime} = await import('@grest-ts/runtime');
|
|
38
|
+
const {GGTestRuntimeWorker} = await import('@grest-ts/testkit');
|
|
39
|
+
const {GGLog} = await import('@grest-ts/logger');
|
|
40
|
+
|
|
41
|
+
const config = JSON.parse(configJson);
|
|
42
|
+
|
|
43
|
+
// Override cli() to intercept runtime startup in isolated mode
|
|
44
|
+
|
|
45
|
+
GGRuntime.cli = async function(moduleUrl) {
|
|
46
|
+
// Still set SOURCE_MODULE_URL for testkit compatibility
|
|
47
|
+
this.SOURCE_MODULE_URL = moduleUrl;
|
|
48
|
+
|
|
49
|
+
// Start the runtime via test worker instead of normal startup
|
|
50
|
+
const controlClient = new GGTestRuntimeWorker(config);
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
await controlClient.start(() => new this());
|
|
54
|
+
|
|
55
|
+
// Signal to parent process that we're ready
|
|
56
|
+
console.log(PROCESS_READY);
|
|
57
|
+
|
|
58
|
+
const stopRuntime = async () => {
|
|
59
|
+
// Stop the GGRuntime but keep process alive for log retrieval
|
|
60
|
+
await controlClient.stopRuntime();
|
|
61
|
+
console.log('IsolatedRunner:RUNTIME_STOPPED');
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const shutdown = async () => {
|
|
65
|
+
// Fully shutdown process
|
|
66
|
+
await controlClient.shutdown();
|
|
67
|
+
process.exit(0);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// Listen for commands via stdin
|
|
71
|
+
process.stdin.setEncoding('utf8');
|
|
72
|
+
process.stdin.on('data', (data) => {
|
|
73
|
+
const message = data.toString().trim();
|
|
74
|
+
if (message === 'STOP_RUNTIME') {
|
|
75
|
+
GGLog.debug({name: 'IsolatedLoader'}, 'Received stop runtime command');
|
|
76
|
+
stopRuntime();
|
|
77
|
+
} else if (message === 'SHUTDOWN') {
|
|
78
|
+
GGLog.debug({name: 'IsolatedLoader'}, 'Received shutdown command');
|
|
79
|
+
shutdown();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
process.on('SIGTERM', shutdown);
|
|
83
|
+
process.on('SIGINT', shutdown);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
GGLog.error({name: 'IsolatedLoader'}, 'Failed to start isolated runtime', err);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Import the runtime source - this will call the overridden cli()
|
|
91
|
+
await import(runtimeSourceUrl);
|
|
@@ -1,49 +1,49 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Worker loader for ES modules
|
|
3
|
-
* This file is loaded by worker threads to execute test runtimes
|
|
4
|
-
*
|
|
5
|
-
* workerData is GGTestEnvConfig: { executablePath, testRouterPort, testId, runtimeId, initialCommands }
|
|
6
|
-
*/
|
|
7
|
-
import {register} from 'tsx/esm/api';
|
|
8
|
-
import {parentPort, workerData} from 'worker_threads';
|
|
9
|
-
|
|
10
|
-
// Register tsx to handle TypeScript with ESM
|
|
11
|
-
register();
|
|
12
|
-
|
|
13
|
-
// Catch unhandled errors in worker thread
|
|
14
|
-
process.on('uncaughtException', (err) => {
|
|
15
|
-
console.error(`[WorkerThread ${workerData?.runtimeId}] Uncaught exception:`, err);
|
|
16
|
-
});
|
|
17
|
-
process.on('unhandledRejection', (reason) => {
|
|
18
|
-
console.error(`[WorkerThread ${workerData?.runtimeId}] Unhandled rejection:`, reason);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
// workerData is GGTestEnvConfig directly
|
|
22
|
-
const config = workerData;
|
|
23
|
-
|
|
24
|
-
// Import control client from @grest-ts/testkit
|
|
25
|
-
const {GGTestRuntimeWorker} = await import('@grest-ts/testkit');
|
|
26
|
-
|
|
27
|
-
// Create control client
|
|
28
|
-
const controlClient = new GGTestRuntimeWorker(config);
|
|
29
|
-
|
|
30
|
-
// Start the runtime
|
|
31
|
-
try {
|
|
32
|
-
await controlClient.start();
|
|
33
|
-
parentPort.postMessage({type: 'ready'});
|
|
34
|
-
} catch (err) {
|
|
35
|
-
parentPort.postMessage({type: 'error', error: err.stack || err.message});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Handle messages from parent
|
|
39
|
-
parentPort.on('message', async (msg) => {
|
|
40
|
-
if (msg.type === 'stopRuntime') {
|
|
41
|
-
// Stop the GGRuntime but keep worker alive for log retrieval
|
|
42
|
-
await controlClient.stopRuntime();
|
|
43
|
-
parentPort.postMessage({type: 'runtimeStopped'});
|
|
44
|
-
} else if (msg.type === 'shutdown') {
|
|
45
|
-
// Fully shutdown worker
|
|
46
|
-
await controlClient.shutdown();
|
|
47
|
-
process.exit(0);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* Worker loader for ES modules
|
|
3
|
+
* This file is loaded by worker threads to execute test runtimes
|
|
4
|
+
*
|
|
5
|
+
* workerData is GGTestEnvConfig: { executablePath, testRouterPort, testId, runtimeId, initialCommands }
|
|
6
|
+
*/
|
|
7
|
+
import {register} from 'tsx/esm/api';
|
|
8
|
+
import {parentPort, workerData} from 'worker_threads';
|
|
9
|
+
|
|
10
|
+
// Register tsx to handle TypeScript with ESM
|
|
11
|
+
register();
|
|
12
|
+
|
|
13
|
+
// Catch unhandled errors in worker thread
|
|
14
|
+
process.on('uncaughtException', (err) => {
|
|
15
|
+
console.error(`[WorkerThread ${workerData?.runtimeId}] Uncaught exception:`, err);
|
|
16
|
+
});
|
|
17
|
+
process.on('unhandledRejection', (reason) => {
|
|
18
|
+
console.error(`[WorkerThread ${workerData?.runtimeId}] Unhandled rejection:`, reason);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// workerData is GGTestEnvConfig directly
|
|
22
|
+
const config = workerData;
|
|
23
|
+
|
|
24
|
+
// Import control client from @grest-ts/testkit
|
|
25
|
+
const {GGTestRuntimeWorker} = await import('@grest-ts/testkit');
|
|
26
|
+
|
|
27
|
+
// Create control client
|
|
28
|
+
const controlClient = new GGTestRuntimeWorker(config);
|
|
29
|
+
|
|
30
|
+
// Start the runtime
|
|
31
|
+
try {
|
|
32
|
+
await controlClient.start();
|
|
33
|
+
parentPort.postMessage({type: 'ready'});
|
|
34
|
+
} catch (err) {
|
|
35
|
+
parentPort.postMessage({type: 'error', error: err.stack || err.message});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Handle messages from parent
|
|
39
|
+
parentPort.on('message', async (msg) => {
|
|
40
|
+
if (msg.type === 'stopRuntime') {
|
|
41
|
+
// Stop the GGRuntime but keep worker alive for log retrieval
|
|
42
|
+
await controlClient.stopRuntime();
|
|
43
|
+
parentPort.postMessage({type: 'runtimeStopped'});
|
|
44
|
+
} else if (msg.type === 'shutdown') {
|
|
45
|
+
// Fully shutdown worker
|
|
46
|
+
await controlClient.shutdown();
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
});
|