@git.zone/tstest 4.1.1 → 6.0.0
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/.smartconfig.json +5 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/tstest.classes.runtime.bun.js +4 -3
- package/dist_ts/tstest.classes.runtime.chromium.js +5 -4
- package/dist_ts/tstest.classes.runtime.deno.js +49 -117
- package/dist_ts/tstest.classes.runtime.docker.js +9 -10
- package/dist_ts/tstest.classes.runtime.node.js +8 -3
- package/dist_ts/tstest.classes.tap.combinator.js +2 -4
- package/dist_ts/tstest.classes.tap.parser.d.ts +5 -1
- package/dist_ts/tstest.classes.tap.parser.js +47 -68
- package/dist_ts/tstest.classes.testfile.directives.js +2 -2
- package/dist_ts/tstest.classes.tstest.d.ts +2 -0
- package/dist_ts/tstest.classes.tstest.js +24 -10
- package/dist_ts/tstest.logging.d.ts +5 -1
- package/dist_ts/tstest.logging.js +30 -13
- package/dist_ts/tstest.plugins.d.ts +2 -1
- package/dist_ts/tstest.plugins.js +3 -2
- package/dist_ts_tapbundle_serverside/classes.tapnodetools.d.ts +2 -2
- package/dist_ts_tapbundle_serverside/classes.tapnodetools.js +10 -10
- package/dist_ts_tapbundle_serverside/plugins.d.ts +1 -0
- package/dist_ts_tapbundle_serverside/plugins.js +3 -1
- package/package.json +4 -4
- package/readme.md +17 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/tstest.classes.runtime.bun.ts +3 -2
- package/ts/tstest.classes.runtime.chromium.ts +4 -3
- package/ts/tstest.classes.runtime.deno.ts +47 -131
- package/ts/tstest.classes.runtime.docker.ts +8 -9
- package/ts/tstest.classes.runtime.node.ts +7 -2
- package/ts/tstest.classes.tap.combinator.ts +1 -3
- package/ts/tstest.classes.tap.parser.ts +46 -75
- package/ts/tstest.classes.testfile.directives.ts +1 -1
- package/ts/tstest.classes.tstest.ts +33 -10
- package/ts/tstest.logging.ts +33 -13
- package/ts/tstest.plugins.ts +2 -1
|
@@ -20,7 +20,6 @@ export const DENO_DEFAULT_PERMISSIONS = [
|
|
|
20
20
|
'--allow-write',
|
|
21
21
|
'--allow-sys',
|
|
22
22
|
'--allow-import',
|
|
23
|
-
'--node-modules-dir',
|
|
24
23
|
'--sloppy-imports',
|
|
25
24
|
];
|
|
26
25
|
|
|
@@ -45,20 +44,9 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
|
|
|
45
44
|
* Get default Deno options
|
|
46
45
|
*/
|
|
47
46
|
protected getDefaultOptions(): DenoOptions {
|
|
48
|
-
//
|
|
49
|
-
let configPath: string | undefined;
|
|
50
|
-
const denoJsonPath = plugins.path.join(process.cwd(), 'deno.json');
|
|
51
|
-
const denoJsoncPath = plugins.path.join(process.cwd(), 'deno.jsonc');
|
|
52
|
-
|
|
53
|
-
if (plugins.fs.existsSync(denoJsonPath)) {
|
|
54
|
-
configPath = denoJsonPath;
|
|
55
|
-
} else if (plugins.fs.existsSync(denoJsoncPath)) {
|
|
56
|
-
configPath = denoJsoncPath;
|
|
57
|
-
}
|
|
58
|
-
|
|
47
|
+
// Deno discovers the project's config and node_modules mode itself.
|
|
59
48
|
return {
|
|
60
49
|
...super.getDefaultOptions(),
|
|
61
|
-
configPath,
|
|
62
50
|
permissions: [...DENO_DEFAULT_PERMISSIONS],
|
|
63
51
|
};
|
|
64
52
|
}
|
|
@@ -152,130 +140,58 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
|
|
|
152
140
|
total: number,
|
|
153
141
|
options?: DenoOptions
|
|
154
142
|
): Promise<TapParser> {
|
|
155
|
-
this.logger.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const mergedOptions = this.mergeOptions(options) as DenoOptions;
|
|
159
|
-
|
|
160
|
-
// Build Deno command
|
|
161
|
-
const command = this.createCommand(testFile, mergedOptions);
|
|
162
|
-
const fullCommand = `${command.command} ${command.args.join(' ')}`;
|
|
163
|
-
|
|
164
|
-
// Set filter tags as environment variable
|
|
165
|
-
if (this.filterTags.length > 0) {
|
|
166
|
-
process.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Check for 00init.ts file in test directory
|
|
170
|
-
const testDir = plugins.path.dirname(testFile);
|
|
171
|
-
const initFile = plugins.path.join(testDir, '00init.ts');
|
|
172
|
-
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
|
|
173
|
-
|
|
174
|
-
let runCommand = fullCommand;
|
|
175
|
-
let loaderPath: string | null = null;
|
|
176
|
-
|
|
177
|
-
// If 00init.ts exists, create a loader file
|
|
178
|
-
if (initFileExists) {
|
|
179
|
-
const absoluteInitFile = plugins.path.resolve(initFile);
|
|
180
|
-
const absoluteTestFile = plugins.path.resolve(testFile);
|
|
181
|
-
const loaderContent = `
|
|
182
|
-
import '${absoluteInitFile.replace(/\\/g, '/')}';
|
|
183
|
-
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|
184
|
-
`;
|
|
185
|
-
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
|
186
|
-
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
|
|
187
|
-
|
|
188
|
-
// Rebuild command with loader file
|
|
189
|
-
const loaderCommand = this.createCommand(loaderPath, mergedOptions);
|
|
190
|
-
runCommand = `${loaderCommand.command} ${loaderCommand.args.join(' ')}`;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Pre-resolve dependencies for the Deno test entrypoint
|
|
194
|
-
const installTarget = loaderPath || testFile;
|
|
195
|
-
const installArgs = ['install', '--entrypoint', installTarget];
|
|
196
|
-
if (mergedOptions.configPath) {
|
|
197
|
-
installArgs.push('--config', mergedOptions.configPath);
|
|
198
|
-
}
|
|
199
|
-
const installCommand = `deno ${installArgs.join(' ')}`;
|
|
200
|
-
console.log(cs(` ⏳ Resolving Deno dependencies for ${plugins.path.basename(testFile)}...`, 'blue'));
|
|
201
|
-
await this.smartshellInstance.execSilent(installCommand, { cwd: process.cwd() });
|
|
202
|
-
console.log(cs(` ✓ Deno dependencies resolved`, 'green'));
|
|
203
|
-
|
|
204
|
-
const execResultStreaming = await this.smartshellInstance.execStreamingSilent(runCommand);
|
|
143
|
+
const logger = this.logger.createTestFileLogger();
|
|
144
|
+
logger.testFileStart(testFile, this.displayName, index, total);
|
|
145
|
+
const tapParser = new TapParser(testFile + ':deno', logger);
|
|
205
146
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
console.error(cs('⚠️ WARNING: Test file is running for more than 1 minute', 'orange'));
|
|
228
|
-
console.error(cs(` File: ${testFile}`, 'orange'));
|
|
229
|
-
console.error(cs(' Consider using --timeout option to set a timeout for test files.', 'orange'));
|
|
230
|
-
console.error(cs(' Example: tstest test --timeout=300 (for 5 minutes)', 'orange'));
|
|
231
|
-
console.error('');
|
|
232
|
-
}, 60000); // 1 minute
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Handle timeout if specified
|
|
236
|
-
if (this.timeoutSeconds !== null) {
|
|
237
|
-
const timeoutMs = this.timeoutSeconds * 1000;
|
|
238
|
-
let timeoutId: NodeJS.Timeout;
|
|
239
|
-
|
|
240
|
-
const timeoutPromise = new Promise<void>((_resolve, reject) => {
|
|
241
|
-
timeoutId = setTimeout(async () => {
|
|
242
|
-
// Use smartshell's terminate() to kill entire process tree
|
|
243
|
-
await execResultStreaming.terminate();
|
|
244
|
-
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
|
|
245
|
-
}, timeoutMs);
|
|
147
|
+
let loaderPath: string | undefined;
|
|
148
|
+
let timer: NodeJS.Timeout | undefined;
|
|
149
|
+
try {
|
|
150
|
+
const mergedOptions = this.mergeOptions(options) as DenoOptions;
|
|
151
|
+
const absoluteTestFile = plugins.path.resolve(mergedOptions.cwd || process.cwd(), testFile);
|
|
152
|
+
const testDir = plugins.path.dirname(absoluteTestFile);
|
|
153
|
+
const initFile = plugins.path.join(testDir, '00init.ts');
|
|
154
|
+
if (await plugins.smartfsInstance.file(initFile).exists()) {
|
|
155
|
+
loaderPath = plugins.path.join(testDir,
|
|
156
|
+
`.${plugins.path.basename(testFile)}.${process.pid}.${Date.now()}.loader.ts`);
|
|
157
|
+
const loaderContent = [initFile, absoluteTestFile].map((fileArg) =>
|
|
158
|
+
`import ${JSON.stringify(plugins.url.pathToFileURL(fileArg).href)};`).join('\n');
|
|
159
|
+
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
|
|
160
|
+
}
|
|
161
|
+
const command = this.createCommand(loaderPath || absoluteTestFile, mergedOptions);
|
|
162
|
+
const quote = (valueArg: string) => "'" + valueArg.replaceAll("'", "'\\''") + "'";
|
|
163
|
+
const runCommand = [command.command, ...command.args].map(quote).join(' ');
|
|
164
|
+
// One Deno invocation honors project defaults and explicit manual/auto flags.
|
|
165
|
+
const execution = await this.smartshellInstance.execStreamingSilent(runCommand, {
|
|
166
|
+
cwd: command.cwd,
|
|
167
|
+
env: { ...process.env, ...command.env },
|
|
246
168
|
});
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
169
|
+
if (this.timeoutSeconds !== null) {
|
|
170
|
+
timer = setTimeout(() => {
|
|
171
|
+
tapParser.handleTimeout(this.timeoutSeconds);
|
|
172
|
+
void execution.kill().catch((errorArg: Error) => {
|
|
173
|
+
tapParser.handleExecutionError(`Unable to terminate timed out test: ${errorArg.message}`);
|
|
174
|
+
});
|
|
175
|
+
}, this.timeoutSeconds * 1000);
|
|
176
|
+
} else {
|
|
177
|
+
timer = setTimeout(() => {
|
|
178
|
+
logger.warning(`Test file ${testFile} has run for more than one minute; use --timeout to limit execution.`);
|
|
179
|
+
}, 60000);
|
|
180
|
+
}
|
|
181
|
+
await tapParser.handleTapProcess(execution.childProcess, false);
|
|
182
|
+
} catch (errorArg) {
|
|
183
|
+
tapParser.handleExecutionError(errorArg instanceof Error ? errorArg.message : String(errorArg));
|
|
184
|
+
} finally {
|
|
185
|
+
if (timer) clearTimeout(timer);
|
|
186
|
+
if (loaderPath) {
|
|
263
187
|
try {
|
|
264
|
-
await
|
|
265
|
-
} catch (
|
|
266
|
-
|
|
188
|
+
await plugins.fs.promises.rm(loaderPath, { force: true });
|
|
189
|
+
} catch (errorArg) {
|
|
190
|
+
tapParser.handleExecutionError(`Failed to remove Deno loader: ${String(errorArg)}`);
|
|
267
191
|
}
|
|
268
|
-
await tapParser.evaluateFinalResult();
|
|
269
192
|
}
|
|
270
|
-
|
|
271
|
-
await tapParser.handleTapProcess(execResultStreaming.childProcess);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Clear warning timer if it was set
|
|
275
|
-
if (warningTimer) {
|
|
276
|
-
clearTimeout(warningTimer);
|
|
193
|
+
await tapParser.evaluateFinalResult();
|
|
277
194
|
}
|
|
278
|
-
|
|
279
195
|
return tapParser;
|
|
280
196
|
}
|
|
281
197
|
}
|
|
@@ -139,7 +139,8 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
|
|
|
139
139
|
total: number,
|
|
140
140
|
options?: RuntimeOptions
|
|
141
141
|
): Promise<TapParser> {
|
|
142
|
-
this.logger.
|
|
142
|
+
const logger = this.logger.createTestFileLogger();
|
|
143
|
+
logger.testFileStart(testFile, this.displayName, index, total);
|
|
143
144
|
|
|
144
145
|
// Parse the Docker test filename
|
|
145
146
|
const parsed = parseDockerTestFilename(testFile);
|
|
@@ -158,7 +159,7 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
|
|
|
158
159
|
const relativeTestPath = plugins.path.relative(this.cwd, absoluteTestPath);
|
|
159
160
|
|
|
160
161
|
// Create TAP parser
|
|
161
|
-
const tapParser = new TapParser(testFile + ':docker',
|
|
162
|
+
const tapParser = new TapParser(testFile + ':docker', logger);
|
|
162
163
|
|
|
163
164
|
try {
|
|
164
165
|
// Build docker run command
|
|
@@ -172,7 +173,7 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
|
|
|
172
173
|
`/test/${plugins.path.basename(testFile)}`
|
|
173
174
|
];
|
|
174
175
|
|
|
175
|
-
|
|
176
|
+
logger.tapOutput(`Executing: docker ${dockerArgs.join(' ')}`);
|
|
176
177
|
|
|
177
178
|
// Execute the Docker container
|
|
178
179
|
const execPromise = this.smartshellInstance.execStreaming(
|
|
@@ -186,7 +187,7 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
|
|
|
186
187
|
let timeoutHandle: NodeJS.Timeout | null = null;
|
|
187
188
|
if (this.timeoutSeconds) {
|
|
188
189
|
timeoutHandle = setTimeout(() => {
|
|
189
|
-
|
|
190
|
+
logger.tapOutput(`⏱️ Test timeout (${this.timeoutSeconds}s) - killing container`);
|
|
190
191
|
// Try to kill any running containers with this image
|
|
191
192
|
this.smartshellInstance.exec(`docker ps -q --filter ancestor=${imageName} | xargs -r docker kill`);
|
|
192
193
|
}, this.timeoutSeconds * 1000);
|
|
@@ -205,7 +206,7 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
|
|
|
205
206
|
|
|
206
207
|
execPromise.childProcess.stderr.on('data', (data: Buffer) => {
|
|
207
208
|
const output = data.toString();
|
|
208
|
-
|
|
209
|
+
logger.tapOutput(cs(`[stderr] ${output}`, 'orange'));
|
|
209
210
|
});
|
|
210
211
|
|
|
211
212
|
// Wait for completion
|
|
@@ -217,16 +218,14 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
|
|
|
217
218
|
}
|
|
218
219
|
|
|
219
220
|
if (result.exitCode !== 0) {
|
|
220
|
-
|
|
221
|
+
tapParser.handleExecutionError(`Docker test failed with exit code ${result.exitCode}`);
|
|
221
222
|
}
|
|
222
223
|
|
|
223
224
|
// Evaluate final result
|
|
224
225
|
await tapParser.evaluateFinalResult();
|
|
225
226
|
|
|
226
227
|
} catch (error) {
|
|
227
|
-
|
|
228
|
-
// Add a failing test result to the parser
|
|
229
|
-
tapParser.handleTapLog('not ok 1 - Docker test execution failed');
|
|
228
|
+
tapParser.handleExecutionError(`Error running Docker test: ${error.message}`);
|
|
230
229
|
await tapParser.evaluateFinalResult();
|
|
231
230
|
}
|
|
232
231
|
|
|
@@ -97,8 +97,9 @@ export class NodeRuntimeAdapter extends RuntimeAdapter {
|
|
|
97
97
|
total: number,
|
|
98
98
|
options?: RuntimeOptions
|
|
99
99
|
): Promise<TapParser> {
|
|
100
|
-
this.logger.
|
|
101
|
-
|
|
100
|
+
const logger = this.logger.createTestFileLogger();
|
|
101
|
+
logger.testFileStart(testFile, this.displayName, index, total);
|
|
102
|
+
const tapParser = new TapParser(testFile + ':node', logger);
|
|
102
103
|
|
|
103
104
|
const mergedOptions = this.mergeOptions(options);
|
|
104
105
|
|
|
@@ -145,6 +146,10 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|
|
145
146
|
// Spawn the test process using tsrun's spawnPath API
|
|
146
147
|
// Pass undefined for fromFileUrl since fileToRun is already an absolute path
|
|
147
148
|
const tsrunProcess = plugins.tsrun.spawnPath(fileToRun, undefined, spawnOptions);
|
|
149
|
+
// The parser owns the outcome; also observe tsrun's rejected spawn/signal promise.
|
|
150
|
+
void tsrunProcess.exitCode.catch((errorArg: Error) => {
|
|
151
|
+
tapParser.handleExecutionError(errorArg.message);
|
|
152
|
+
});
|
|
148
153
|
|
|
149
154
|
// If we created a loader file, clean it up after test execution
|
|
150
155
|
if (loaderPath) {
|
|
@@ -32,9 +32,7 @@ export class TapCombinator {
|
|
|
32
32
|
// Check for failures
|
|
33
33
|
let failGlobal = false;
|
|
34
34
|
for (const tapParser of this.tapParserStore) {
|
|
35
|
-
if (
|
|
36
|
-
tapParser.expectedTests !== tapParser.receivedTests ||
|
|
37
|
-
tapParser.getErrorTests().length > 0) {
|
|
35
|
+
if (tapParser.failed) {
|
|
38
36
|
failGlobal = true;
|
|
39
37
|
break;
|
|
40
38
|
}
|
|
@@ -16,6 +16,17 @@ export class TapParser {
|
|
|
16
16
|
|
|
17
17
|
expectedTests: number = 0;
|
|
18
18
|
receivedTests: number = 0;
|
|
19
|
+
public readonly executionErrors: string[] = [];
|
|
20
|
+
private finalized = false;
|
|
21
|
+
|
|
22
|
+
public get failed(): boolean {
|
|
23
|
+
return this.executionErrors.length > 0 || !this.expectedTests ||
|
|
24
|
+
this.expectedTests !== this.receivedTests || this.getErrorTests().length > 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public handleExecutionError(messageArg: string): void {
|
|
28
|
+
if (!this.executionErrors.includes(messageArg)) this.executionErrors.push(messageArg);
|
|
29
|
+
}
|
|
19
30
|
|
|
20
31
|
activeTapTestResult: TapTestResult;
|
|
21
32
|
|
|
@@ -38,30 +49,7 @@ export class TapParser {
|
|
|
38
49
|
* Handle test file timeout
|
|
39
50
|
*/
|
|
40
51
|
public handleTimeout(timeoutSeconds: number) {
|
|
41
|
-
|
|
42
|
-
if (this.expectedTests === 0) {
|
|
43
|
-
this.expectedTests = 1;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Create a fake failing test result for timeout
|
|
47
|
-
this._getNewTapTestResult();
|
|
48
|
-
this.activeTapTestResult.testOk = false;
|
|
49
|
-
this.activeTapTestResult.testSettled = true;
|
|
50
|
-
this.testStore.push(this.activeTapTestResult);
|
|
51
|
-
|
|
52
|
-
// Log the timeout error
|
|
53
|
-
if (this.logger) {
|
|
54
|
-
// First log the test result
|
|
55
|
-
this.logger.testResult(
|
|
56
|
-
`Test file timeout`,
|
|
57
|
-
false,
|
|
58
|
-
timeoutSeconds * 1000,
|
|
59
|
-
`Error: Test file exceeded timeout of ${timeoutSeconds} seconds`
|
|
60
|
-
);
|
|
61
|
-
this.logger.testErrorDetails(`Test execution was terminated after ${timeoutSeconds} seconds`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Don't call evaluateFinalResult here, let the caller handle it
|
|
52
|
+
this.handleExecutionError(`Test file exceeded timeout of ${timeoutSeconds} seconds`);
|
|
65
53
|
}
|
|
66
54
|
|
|
67
55
|
private _getNewTapTestResult() {
|
|
@@ -259,9 +247,7 @@ export class TapParser {
|
|
|
259
247
|
break;
|
|
260
248
|
|
|
261
249
|
case 'bailout':
|
|
262
|
-
|
|
263
|
-
this.logger.error(`Bail out! ${message.content}`);
|
|
264
|
-
}
|
|
250
|
+
this.handleExecutionError(`Bail out! ${message.content}`);
|
|
265
251
|
break;
|
|
266
252
|
|
|
267
253
|
case 'error':
|
|
@@ -466,24 +452,32 @@ export class TapParser {
|
|
|
466
452
|
* handles a tap process
|
|
467
453
|
* @param childProcessArg
|
|
468
454
|
*/
|
|
469
|
-
public async handleTapProcess(childProcessArg: ChildProcess) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
this.
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
455
|
+
public async handleTapProcess(childProcessArg: ChildProcess, finalizeArg = true) {
|
|
456
|
+
await new Promise<void>((resolve) => {
|
|
457
|
+
const onData = (dataArg: Buffer) => this._processLog(dataArg);
|
|
458
|
+
const onError = (errorArg: Error) => this.handleExecutionError(errorArg.message);
|
|
459
|
+
childProcessArg.stdout?.on('data', onData);
|
|
460
|
+
childProcessArg.stderr?.on('data', onData);
|
|
461
|
+
childProcessArg.on('error', onError);
|
|
462
|
+
// close follows both process exit and pipe drainage, including failed spawn.
|
|
463
|
+
childProcessArg.once('close', (codeArg, signalArg) => {
|
|
464
|
+
childProcessArg.stdout?.off('data', onData);
|
|
465
|
+
childProcessArg.stderr?.off('data', onData);
|
|
466
|
+
childProcessArg.off('error', onError);
|
|
467
|
+
if (codeArg !== 0) {
|
|
468
|
+
this.handleExecutionError(signalArg
|
|
469
|
+
? `Test process terminated by ${signalArg}`
|
|
470
|
+
: `Test process exited with code ${codeArg}`);
|
|
471
|
+
}
|
|
472
|
+
if (this.lineBuffer) {
|
|
473
|
+
const remaining = this.lineBuffer;
|
|
474
|
+
this.lineBuffer = '';
|
|
475
|
+
this._processLog(remaining + '\n');
|
|
476
|
+
}
|
|
477
|
+
resolve();
|
|
478
|
+
});
|
|
485
479
|
});
|
|
486
|
-
await
|
|
480
|
+
if (finalizeArg) await this.evaluateFinalResult();
|
|
487
481
|
}
|
|
488
482
|
|
|
489
483
|
public async handleTapLog(tapLog: string) {
|
|
@@ -544,40 +538,17 @@ export class TapParser {
|
|
|
544
538
|
}
|
|
545
539
|
|
|
546
540
|
public async evaluateFinalResult() {
|
|
541
|
+
if (this.finalized) return;
|
|
542
|
+
this.finalized = true;
|
|
547
543
|
this.receivedTests = this.testStore.length;
|
|
548
544
|
const duration = Date.now() - this.startTime;
|
|
549
|
-
|
|
550
|
-
// check wether all tests ran
|
|
551
|
-
if (this.expectedTests === this.receivedTests) {
|
|
552
|
-
if (this.logger) {
|
|
553
|
-
this.logger.tapOutput(`${this.receivedTests} out of ${this.expectedTests} Tests completed!`);
|
|
554
|
-
}
|
|
555
|
-
} else {
|
|
556
|
-
if (this.logger) {
|
|
557
|
-
this.logger.error(`Only ${this.receivedTests} out of ${this.expectedTests} completed!`);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
545
|
if (!this.expectedTests && this.receivedTests === 0) {
|
|
561
|
-
|
|
562
|
-
this.logger.error('No tests were defined. Therefore the testfile failed!');
|
|
563
|
-
this.logger.testFileEnd(0, 1, duration); // Count as 1 failure
|
|
564
|
-
}
|
|
546
|
+
this.handleExecutionError('No tests were defined.');
|
|
565
547
|
} else if (this.expectedTests !== this.receivedTests) {
|
|
566
|
-
|
|
567
|
-
this.logger.error('The amount of received tests and expectedTests is unequal! Therefore the testfile failed');
|
|
568
|
-
const errorCount = this.getErrorTests().length || 1; // At least 1 error
|
|
569
|
-
this.logger.testFileEnd(this.receivedTests - errorCount, errorCount, duration);
|
|
570
|
-
}
|
|
571
|
-
} else if (this.getErrorTests().length === 0) {
|
|
572
|
-
if (this.logger) {
|
|
573
|
-
this.logger.tapOutput('All tests are successfull!!!');
|
|
574
|
-
this.logger.testFileEnd(this.receivedTests, 0, duration);
|
|
575
|
-
}
|
|
576
|
-
} else {
|
|
577
|
-
if (this.logger) {
|
|
578
|
-
this.logger.tapOutput(`${this.getErrorTests().length} tests threw an error!!!`, true);
|
|
579
|
-
this.logger.testFileEnd(this.receivedTests - this.getErrorTests().length, this.getErrorTests().length, duration);
|
|
580
|
-
}
|
|
548
|
+
this.handleExecutionError(`Only ${this.receivedTests} out of ${this.expectedTests} tests completed.`);
|
|
581
549
|
}
|
|
550
|
+
for (const error of this.executionErrors) this.logger?.testErrorDetails(error);
|
|
551
|
+
const failedTests = this.getErrorTests().length;
|
|
552
|
+
this.logger?.testFileEnd(this.receivedTests - failedTests, failedTests, duration, this.executionErrors);
|
|
582
553
|
}
|
|
583
|
-
}
|
|
554
|
+
}
|
|
@@ -165,7 +165,7 @@ function directivesToDenoOptions(directives: IParsedDirectives): DenoOptions | u
|
|
|
165
165
|
|
|
166
166
|
if (useAllowAll) {
|
|
167
167
|
// --allow-all replaces individual permissions, but keep compatibility flags
|
|
168
|
-
options.permissions = ['--allow-all', '--
|
|
168
|
+
options.permissions = ['--allow-all', '--sloppy-imports'];
|
|
169
169
|
} else if (extraPermissions.length > 0) {
|
|
170
170
|
// Start with defaults and add extra permissions (deduplicated)
|
|
171
171
|
const allPermissions = [...DENO_DEFAULT_PERMISSIONS];
|
|
@@ -12,7 +12,7 @@ import type { LogOptions } from './tstest.logging.js';
|
|
|
12
12
|
|
|
13
13
|
// Runtime adapters
|
|
14
14
|
import { parseTestFilename, isDockerTestFile, parseDockerTestFilename } from './tstest.classes.runtime.parser.js';
|
|
15
|
-
import { RuntimeAdapterRegistry } from './tstest.classes.runtime.adapter.js';
|
|
15
|
+
import { RuntimeAdapterRegistry, type RuntimeAdapter, type RuntimeOptions } from './tstest.classes.runtime.adapter.js';
|
|
16
16
|
import { NodeRuntimeAdapter } from './tstest.classes.runtime.node.js';
|
|
17
17
|
import { ChromiumRuntimeAdapter } from './tstest.classes.runtime.chromium.js';
|
|
18
18
|
import { DenoRuntimeAdapter } from './tstest.classes.runtime.deno.js';
|
|
@@ -318,14 +318,14 @@ export class TsTest {
|
|
|
318
318
|
{ TSTEST_FILE: fileNameArg, TSTEST_RUNTIME: adapter.id },
|
|
319
319
|
);
|
|
320
320
|
if (!success) {
|
|
321
|
-
this.
|
|
321
|
+
await this.recordRuntimeFailure(fileNameArg, adapter.displayName, fileIndex, totalFiles,
|
|
322
|
+
tapCombinator, 'test:before:testfile failed.');
|
|
322
323
|
return;
|
|
323
324
|
}
|
|
324
325
|
}
|
|
325
326
|
|
|
326
327
|
const options = hasDirectives(directives) ? directivesToRuntimeOptions(directives, adapter.id) : undefined;
|
|
327
|
-
|
|
328
|
-
tapCombinator.addTapParser(tapParser);
|
|
328
|
+
await this.runAdapter(adapter, fileNameArg, fileIndex, totalFiles, tapCombinator, options);
|
|
329
329
|
} else {
|
|
330
330
|
// Multiple runtimes - use sections
|
|
331
331
|
for (let i = 0; i < adapters.length; i++) {
|
|
@@ -342,20 +342,40 @@ export class TsTest {
|
|
|
342
342
|
{ TSTEST_FILE: fileNameArg, TSTEST_RUNTIME: adapter.id },
|
|
343
343
|
);
|
|
344
344
|
if (!success) {
|
|
345
|
-
this.
|
|
345
|
+
await this.recordRuntimeFailure(fileNameArg, adapter.displayName, fileIndex, totalFiles,
|
|
346
|
+
tapCombinator, 'test:before:testfile failed.');
|
|
346
347
|
this.logger.sectionEnd();
|
|
347
348
|
continue;
|
|
348
349
|
}
|
|
349
350
|
}
|
|
350
351
|
|
|
351
352
|
const options = hasDirectives(directives) ? directivesToRuntimeOptions(directives, adapter.id) : undefined;
|
|
352
|
-
|
|
353
|
-
tapCombinator.addTapParser(tapParser);
|
|
353
|
+
await this.runAdapter(adapter, fileNameArg, fileIndex, totalFiles, tapCombinator, options);
|
|
354
354
|
this.logger.sectionEnd();
|
|
355
355
|
}
|
|
356
356
|
}
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
+
private async recordRuntimeFailure(fileArg: string, runtimeArg: string, indexArg: number,
|
|
360
|
+
totalArg: number, combinatorArg: TapCombinator, errorArg: unknown): Promise<void> {
|
|
361
|
+
const logger = this.logger.createTestFileLogger();
|
|
362
|
+
logger.testFileStart(fileArg, runtimeArg, indexArg, totalArg);
|
|
363
|
+
const parser = new TapParser(`${fileArg}:${runtimeArg}`, logger);
|
|
364
|
+
parser.handleExecutionError(errorArg instanceof Error ? errorArg.message : String(errorArg));
|
|
365
|
+
await parser.evaluateFinalResult();
|
|
366
|
+
combinatorArg.addTapParser(parser);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private async runAdapter(adapterArg: RuntimeAdapter, fileArg: string, indexArg: number,
|
|
370
|
+
totalArg: number, combinatorArg: TapCombinator, optionsArg?: RuntimeOptions): Promise<void> {
|
|
371
|
+
try {
|
|
372
|
+
combinatorArg.addTapParser(await adapterArg.run(fileArg, indexArg, totalArg, optionsArg));
|
|
373
|
+
} catch (errorArg) {
|
|
374
|
+
await this.recordRuntimeFailure(fileArg, adapterArg.displayName, indexArg, totalArg,
|
|
375
|
+
combinatorArg, errorArg);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
359
379
|
/**
|
|
360
380
|
* Execute a Docker test file
|
|
361
381
|
*/
|
|
@@ -366,7 +386,8 @@ export class TsTest {
|
|
|
366
386
|
tapCombinator: TapCombinator
|
|
367
387
|
): Promise<void> {
|
|
368
388
|
if (!this.dockerAdapter) {
|
|
369
|
-
this.
|
|
389
|
+
await this.recordRuntimeFailure(fileNameArg, 'Docker', fileIndex, totalFiles,
|
|
390
|
+
tapCombinator, 'Docker adapter not initialized.');
|
|
370
391
|
return;
|
|
371
392
|
}
|
|
372
393
|
|
|
@@ -380,7 +401,8 @@ export class TsTest {
|
|
|
380
401
|
{ TSTEST_FILE: fileNameArg, TSTEST_RUNTIME: 'docker' },
|
|
381
402
|
);
|
|
382
403
|
if (!success) {
|
|
383
|
-
this.
|
|
404
|
+
await this.recordRuntimeFailure(fileNameArg, 'Docker', fileIndex, totalFiles,
|
|
405
|
+
tapCombinator, 'test:before:testfile failed.');
|
|
384
406
|
return;
|
|
385
407
|
}
|
|
386
408
|
}
|
|
@@ -389,7 +411,8 @@ export class TsTest {
|
|
|
389
411
|
const tapParser = await this.dockerAdapter.run(fileNameArg, fileIndex, totalFiles);
|
|
390
412
|
tapCombinator.addTapParser(tapParser);
|
|
391
413
|
} catch (error) {
|
|
392
|
-
this.
|
|
414
|
+
await this.recordRuntimeFailure(fileNameArg, 'Docker', fileIndex, totalFiles,
|
|
415
|
+
tapCombinator, error);
|
|
393
416
|
}
|
|
394
417
|
}
|
|
395
418
|
|