Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
b3f8a28766 | |||
86db2491a3 | |||
b9fd8c7b02 | |||
d6842326ad | |||
175b4463fa | |||
6a8417e400 | |||
4714d5e8ad | |||
ff6aae7159 |
39
changelog.md
39
changelog.md
@@ -1,5 +1,44 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-10-17 - 2.6.0 - feat(runtime-adapters)
|
||||||
|
Add runtime environment availability check and logger output; normalize runtime version strings
|
||||||
|
|
||||||
|
- Introduce checkEnvironment() in TsTest and invoke it at the start of run() to detect available runtimes before executing tests.
|
||||||
|
- Add environmentCheck(availability) to TsTestLogger to print a human-friendly environment summary (with JSON and quiet-mode handling).
|
||||||
|
- Normalize reported runtime version strings from adapters: prefix Deno and Bun versions with 'v' and simplify Chromium version text.
|
||||||
|
- Display runtime availability information to the user before moving previous logs or running tests.
|
||||||
|
- Includes addition of local .claude/settings.local.json (local dev/tooling settings).
|
||||||
|
|
||||||
|
## 2025-10-17 - 2.5.2 - fix(runtime.node)
|
||||||
|
Improve Node runtime adapter to use tsrun.spawnPath, strengthen tsrun detection, and improve process lifecycle and loader handling; update tsrun dependency.
|
||||||
|
|
||||||
|
- Use tsrun.spawnPath to spawn Node test processes and pass structured spawn options (cwd, env, args, stdio).
|
||||||
|
- Detect tsrun availability via plugins.tsrun and require spawnPath; provide a clearer error message when tsrun is missing or outdated.
|
||||||
|
- Pass --web via spawn args and set TSTEST_FILTER_TAGS on the spawned process env instead of mutating the parent process.env.
|
||||||
|
- When a 00init.ts exists, create a temporary loader that imports both 00init.ts and the test file, run the loader via tsrun.spawnPath, and clean up the loader after execution.
|
||||||
|
- Use tsrunProcess.terminate()/kill for timeouts to ensure proper process termination and improve cleanup handling.
|
||||||
|
- Export tsrun from ts/tstest.plugins.ts so runtime code can access tsrun APIs via the plugins object.
|
||||||
|
- Bump dependency @git.zone/tsrun from ^1.3.4 to ^1.6.2 in package.json.
|
||||||
|
|
||||||
|
## 2025-10-16 - 2.5.1 - fix(deps)
|
||||||
|
Bump dependencies and add local tooling settings
|
||||||
|
|
||||||
|
- Bumped @api.global/typedserver from ^3.0.78 to ^3.0.79
|
||||||
|
- Bumped @git.zone/tsrun from ^1.3.3 to ^1.3.4
|
||||||
|
- Bumped @push.rocks/smartjson from ^5.0.20 to ^5.2.0
|
||||||
|
- Bumped @push.rocks/smartlog from ^3.1.9 to ^3.1.10
|
||||||
|
- Add local settings configuration file for developer tooling
|
||||||
|
|
||||||
|
## 2025-10-12 - 2.5.0 - feat(tstest.classes.runtime.parser)
|
||||||
|
Add support for "all" runtime token and update docs/tests; regenerate lockfile and add local settings
|
||||||
|
|
||||||
|
- Add support for the `all` runtime token (expands to node, chromium, deno, bun) in tstest filename parser (tstest.classes.runtime.parser)
|
||||||
|
- Handle `all` with modifiers (e.g. `*.all.nonci.ts`) and mixed tokens (e.g. `node+all`) so it expands to the full runtime set
|
||||||
|
- Add unit tests covering `all` cases in test/test.runtime.parser.node.ts
|
||||||
|
- Update README (examples and tables) to document `.all.ts` and `.all.nonci.ts` usage and include a universal example
|
||||||
|
- Update ts files' parser comments and constants to include ALL_RUNTIMES
|
||||||
|
- Add deno.lock (dependency lockfile) and a local .claude/settings.local.json for project permissions / local settings
|
||||||
|
|
||||||
## 2025-10-11 - 2.4.3 - fix(docs)
|
## 2025-10-11 - 2.4.3 - fix(docs)
|
||||||
Update documentation: expand README with multi-runtime architecture, add module READMEs, and add local dev settings
|
Update documentation: expand README with multi-runtime architecture, add module READMEs, and add local dev settings
|
||||||
|
|
||||||
|
10
package.json
10
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tstest",
|
"name": "@git.zone/tstest",
|
||||||
"version": "2.4.3",
|
"version": "2.6.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "a test utility to run tests that match test/**/*.ts",
|
"description": "a test utility to run tests that match test/**/*.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -28,9 +28,9 @@
|
|||||||
"@types/node": "^22.15.21"
|
"@types/node": "^22.15.21"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedserver": "^3.0.78",
|
"@api.global/typedserver": "^3.0.79",
|
||||||
"@git.zone/tsbundle": "^2.5.1",
|
"@git.zone/tsbundle": "^2.5.1",
|
||||||
"@git.zone/tsrun": "^1.3.3",
|
"@git.zone/tsrun": "^1.6.2",
|
||||||
"@push.rocks/consolecolor": "^2.0.3",
|
"@push.rocks/consolecolor": "^2.0.3",
|
||||||
"@push.rocks/qenv": "^6.1.3",
|
"@push.rocks/qenv": "^6.1.3",
|
||||||
"@push.rocks/smartbrowser": "^2.0.8",
|
"@push.rocks/smartbrowser": "^2.0.8",
|
||||||
@@ -40,8 +40,8 @@
|
|||||||
"@push.rocks/smartenv": "^5.0.13",
|
"@push.rocks/smartenv": "^5.0.13",
|
||||||
"@push.rocks/smartexpect": "^2.5.0",
|
"@push.rocks/smartexpect": "^2.5.0",
|
||||||
"@push.rocks/smartfile": "^11.2.7",
|
"@push.rocks/smartfile": "^11.2.7",
|
||||||
"@push.rocks/smartjson": "^5.0.20",
|
"@push.rocks/smartjson": "^5.2.0",
|
||||||
"@push.rocks/smartlog": "^3.1.9",
|
"@push.rocks/smartlog": "^3.1.10",
|
||||||
"@push.rocks/smartmongo": "^2.0.12",
|
"@push.rocks/smartmongo": "^2.0.12",
|
||||||
"@push.rocks/smartnetwork": "^4.4.0",
|
"@push.rocks/smartnetwork": "^4.4.0",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
|
588
pnpm-lock.yaml
generated
588
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
17
readme.md
17
readme.md
@@ -61,15 +61,29 @@ Name your test files with runtime specifiers to control where they run:
|
|||||||
| `*.chromium.ts` | Chromium browser | `test.dom.chromium.ts` |
|
| `*.chromium.ts` | Chromium browser | `test.dom.chromium.ts` |
|
||||||
| `*.deno.ts` | Deno runtime | `test.http.deno.ts` |
|
| `*.deno.ts` | Deno runtime | `test.http.deno.ts` |
|
||||||
| `*.bun.ts` | Bun runtime | `test.fast.bun.ts` |
|
| `*.bun.ts` | Bun runtime | `test.fast.bun.ts` |
|
||||||
|
| `*.all.ts` | All runtimes (Node, Chromium, Deno, Bun) | `test.universal.all.ts` |
|
||||||
| `*.node+chromium.ts` | Both Node.js and Chromium | `test.isomorphic.node+chromium.ts` |
|
| `*.node+chromium.ts` | Both Node.js and Chromium | `test.isomorphic.node+chromium.ts` |
|
||||||
| `*.node+deno.ts` | Both Node.js and Deno | `test.cross.node+deno.ts` |
|
| `*.node+deno.ts` | Both Node.js and Deno | `test.cross.node+deno.ts` |
|
||||||
| `*.deno+bun.ts` | Both Deno and Bun | `test.modern.deno+bun.ts` |
|
| `*.deno+bun.ts` | Both Deno and Bun | `test.modern.deno+bun.ts` |
|
||||||
| `*.chromium.nonci.ts` | Chromium, skip in CI | `test.visual.chromium.nonci.ts` |
|
| `*.chromium.nonci.ts` | Chromium, skip in CI | `test.visual.chromium.nonci.ts` |
|
||||||
|
| `*.all.nonci.ts` | All runtimes, skip in CI | `test.comprehensive.all.nonci.ts` |
|
||||||
|
|
||||||
**Multi-Runtime Examples:**
|
**Multi-Runtime Examples:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// test.api.node+deno+bun.ts - runs in Node.js, Deno, and Bun
|
// test.api.all.ts - runs in all runtimes (Node, Chromium, Deno, Bun)
|
||||||
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
tap.test('universal HTTP test', async () => {
|
||||||
|
const response = await fetch('https://api.example.com/test');
|
||||||
|
expect(response.status).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// test.api.node+deno+bun.ts - runs in specific runtimes
|
||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
tap.test('cross-runtime HTTP test', async () => {
|
tap.test('cross-runtime HTTP test', async () => {
|
||||||
@@ -915,6 +929,7 @@ tstest test/api/endpoints.test.ts --verbose --timeout 60
|
|||||||
### Version 2.4.0
|
### Version 2.4.0
|
||||||
- 🚀 **Multi-Runtime Architecture** - Support for Deno, Bun, Node.js, and Chromium
|
- 🚀 **Multi-Runtime Architecture** - Support for Deno, Bun, Node.js, and Chromium
|
||||||
- 🔀 **New Naming Convention** - Flexible `.runtime1+runtime2.ts` pattern
|
- 🔀 **New Naming Convention** - Flexible `.runtime1+runtime2.ts` pattern
|
||||||
|
- 🌐 **Universal Testing** - `.all.ts` pattern runs tests on all supported runtimes
|
||||||
- 🔄 **Migration Tool** - Easy migration from legacy naming (`.browser.ts`, `.both.ts`)
|
- 🔄 **Migration Tool** - Easy migration from legacy naming (`.browser.ts`, `.both.ts`)
|
||||||
- 🦕 **Deno Support** - Full Deno runtime with Node.js compatibility
|
- 🦕 **Deno Support** - Full Deno runtime with Node.js compatibility
|
||||||
- 🐰 **Bun Support** - Ultra-fast Bun runtime integration
|
- 🐰 **Bun Support** - Ultra-fast Bun runtime integration
|
||||||
|
@@ -164,4 +164,40 @@ tap.test('parseTestFilename - handles full paths', async () => {
|
|||||||
expect(parsed.original).toEqual('test.node+chromium.ts');
|
expect(parsed.original).toEqual('test.node+chromium.ts');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tap.test('parseTestFilename - all keyword expands to all runtimes', async () => {
|
||||||
|
const parsed = parseTestFilename('test.all.ts');
|
||||||
|
expect(parsed.baseName).toEqual('test');
|
||||||
|
expect(parsed.runtimes).toEqual(['node', 'chromium', 'deno', 'bun']);
|
||||||
|
expect(parsed.modifiers).toEqual([]);
|
||||||
|
expect(parsed.extension).toEqual('ts');
|
||||||
|
expect(parsed.isLegacy).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('parseTestFilename - all keyword with nonci modifier', async () => {
|
||||||
|
const parsed = parseTestFilename('test.all.nonci.ts');
|
||||||
|
expect(parsed.baseName).toEqual('test');
|
||||||
|
expect(parsed.runtimes).toEqual(['node', 'chromium', 'deno', 'bun']);
|
||||||
|
expect(parsed.modifiers).toEqual(['nonci']);
|
||||||
|
expect(parsed.extension).toEqual('ts');
|
||||||
|
expect(parsed.isLegacy).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('parseTestFilename - all keyword with complex basename', async () => {
|
||||||
|
const parsed = parseTestFilename('test.some.feature.all.ts');
|
||||||
|
expect(parsed.baseName).toEqual('test.some.feature');
|
||||||
|
expect(parsed.runtimes).toEqual(['node', 'chromium', 'deno', 'bun']);
|
||||||
|
expect(parsed.modifiers).toEqual([]);
|
||||||
|
expect(parsed.extension).toEqual('ts');
|
||||||
|
expect(parsed.isLegacy).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('parseTestFilename - all keyword in chain expands to all runtimes', async () => {
|
||||||
|
const parsed = parseTestFilename('test.node+all.ts');
|
||||||
|
expect(parsed.baseName).toEqual('test');
|
||||||
|
expect(parsed.runtimes).toEqual(['node', 'chromium', 'deno', 'bun']);
|
||||||
|
expect(parsed.modifiers).toEqual([]);
|
||||||
|
expect(parsed.extension).toEqual('ts');
|
||||||
|
expect(parsed.isLegacy).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
export default tap.start();
|
export default tap.start();
|
||||||
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tstest',
|
name: '@git.zone/tstest',
|
||||||
version: '2.4.3',
|
version: '2.6.0',
|
||||||
description: 'a test utility to run tests that match test/**/*.ts'
|
description: 'a test utility to run tests that match test/**/*.ts'
|
||||||
}
|
}
|
||||||
|
@@ -47,11 +47,11 @@ export class BunRuntimeAdapter extends RuntimeAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Bun version is just the version number
|
// Bun version is just the version number
|
||||||
const version = result.stdout.trim();
|
const version = `v${result.stdout.trim()}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
available: true,
|
available: true,
|
||||||
version: `Bun ${version}`,
|
version: version,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
@@ -37,7 +37,7 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
|
|||||||
// The browser binary is usually handled by @push.rocks/smartbrowser
|
// The browser binary is usually handled by @push.rocks/smartbrowser
|
||||||
return {
|
return {
|
||||||
available: true,
|
available: true,
|
||||||
version: 'Chromium (via smartbrowser)',
|
version: 'via smartbrowser',
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
@@ -67,11 +67,11 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
|
|||||||
|
|
||||||
// Parse Deno version from output (first line is "deno X.Y.Z")
|
// Parse Deno version from output (first line is "deno X.Y.Z")
|
||||||
const versionMatch = result.stdout.match(/deno (\d+\.\d+\.\d+)/);
|
const versionMatch = result.stdout.match(/deno (\d+\.\d+\.\d+)/);
|
||||||
const version = versionMatch ? versionMatch[1] : 'unknown';
|
const version = versionMatch ? `v${versionMatch[1]}` : 'unknown';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
available: true,
|
available: true,
|
||||||
version: `Deno ${version}`,
|
version: version,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
@@ -35,18 +35,11 @@ export class NodeRuntimeAdapter extends RuntimeAdapter {
|
|||||||
// Check Node.js version
|
// Check Node.js version
|
||||||
const nodeVersion = process.version;
|
const nodeVersion = process.version;
|
||||||
|
|
||||||
// Check if tsrun is available
|
// Check if tsrun module is available (imported as dependency)
|
||||||
const result = await this.smartshellInstance.exec('tsrun --version', {
|
if (!plugins.tsrun || !plugins.tsrun.spawnPath) {
|
||||||
cwd: process.cwd(),
|
|
||||||
onError: () => {
|
|
||||||
// Ignore error
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.exitCode !== 0) {
|
|
||||||
return {
|
return {
|
||||||
available: false,
|
available: false,
|
||||||
error: 'tsrun not found. Install with: pnpm install --save-dev @git.zone/tsrun',
|
error: 'tsrun module not found or outdated (requires version 1.6.0+)',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +89,7 @@ export class NodeRuntimeAdapter extends RuntimeAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a test file in Node.js
|
* Execute a test file in Node.js using tsrun's spawnPath API
|
||||||
*/
|
*/
|
||||||
async run(
|
async run(
|
||||||
testFile: string,
|
testFile: string,
|
||||||
@@ -109,28 +102,35 @@ export class NodeRuntimeAdapter extends RuntimeAdapter {
|
|||||||
|
|
||||||
const mergedOptions = this.mergeOptions(options);
|
const mergedOptions = this.mergeOptions(options);
|
||||||
|
|
||||||
// Build tsrun command
|
// Build spawn options
|
||||||
let tsrunOptions = '';
|
const spawnOptions: any = {
|
||||||
|
cwd: mergedOptions.cwd || process.cwd(),
|
||||||
|
env: { ...mergedOptions.env },
|
||||||
|
args: [] as string[],
|
||||||
|
stdio: 'pipe' as const,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add --web flag if needed
|
||||||
if (process.argv.includes('--web')) {
|
if (process.argv.includes('--web')) {
|
||||||
tsrunOptions += ' --web';
|
spawnOptions.args.push('--web');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set filter tags as environment variable
|
// Set filter tags as environment variable
|
||||||
if (this.filterTags.length > 0) {
|
if (this.filterTags.length > 0) {
|
||||||
process.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
spawnOptions.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for 00init.ts file in test directory
|
// Check for 00init.ts file in test directory
|
||||||
const testDir = plugins.path.dirname(testFile);
|
const testDir = plugins.path.dirname(testFile);
|
||||||
const initFile = plugins.path.join(testDir, '00init.ts');
|
const initFile = plugins.path.join(testDir, '00init.ts');
|
||||||
let runCommand = `tsrun ${testFile}${tsrunOptions}`;
|
|
||||||
|
|
||||||
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
|
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
|
||||||
|
|
||||||
// If 00init.ts exists, run it first
|
// Determine which file to run
|
||||||
|
let fileToRun = testFile;
|
||||||
let loaderPath: string | null = null;
|
let loaderPath: string | null = null;
|
||||||
|
|
||||||
|
// If 00init.ts exists, create a loader file
|
||||||
if (initFileExists) {
|
if (initFileExists) {
|
||||||
// Create a temporary loader file that imports both 00init.ts and the test file
|
|
||||||
const absoluteInitFile = plugins.path.resolve(initFile);
|
const absoluteInitFile = plugins.path.resolve(initFile);
|
||||||
const absoluteTestFile = plugins.path.resolve(testFile);
|
const absoluteTestFile = plugins.path.resolve(testFile);
|
||||||
const loaderContent = `
|
const loaderContent = `
|
||||||
@@ -139,10 +139,12 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
`;
|
`;
|
||||||
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
||||||
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
|
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
|
||||||
runCommand = `tsrun ${loaderPath}${tsrunOptions}`;
|
fileToRun = loaderPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
const execResultStreaming = await this.smartshellInstance.execStreamingSilent(runCommand);
|
// Spawn the test process using tsrun's spawnPath API
|
||||||
|
// Pass undefined for fromFileUrl since fileToRun is already an absolute path
|
||||||
|
const tsrunProcess = plugins.tsrun.spawnPath(fileToRun, undefined, spawnOptions);
|
||||||
|
|
||||||
// If we created a loader file, clean it up after test execution
|
// If we created a loader file, clean it up after test execution
|
||||||
if (loaderPath) {
|
if (loaderPath) {
|
||||||
@@ -156,8 +158,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
execResultStreaming.childProcess.on('exit', cleanup);
|
tsrunProcess.childProcess.on('exit', cleanup);
|
||||||
execResultStreaming.childProcess.on('error', cleanup);
|
tsrunProcess.childProcess.on('error', cleanup);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start warning timer if no timeout was specified
|
// Start warning timer if no timeout was specified
|
||||||
@@ -180,15 +182,15 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
|
|
||||||
const timeoutPromise = new Promise<void>((_resolve, reject) => {
|
const timeoutPromise = new Promise<void>((_resolve, reject) => {
|
||||||
timeoutId = setTimeout(async () => {
|
timeoutId = setTimeout(async () => {
|
||||||
// Use smartshell's terminate() to kill entire process tree
|
// Use tsrun's terminate() to gracefully kill the process
|
||||||
await execResultStreaming.terminate();
|
await tsrunProcess.terminate();
|
||||||
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
|
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
|
||||||
}, timeoutMs);
|
}, timeoutMs);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.race([
|
await Promise.race([
|
||||||
tapParser.handleTapProcess(execResultStreaming.childProcess),
|
tapParser.handleTapProcess(tsrunProcess.childProcess),
|
||||||
timeoutPromise
|
timeoutPromise
|
||||||
]);
|
]);
|
||||||
// Clear timeout if test completed successfully
|
// Clear timeout if test completed successfully
|
||||||
@@ -200,16 +202,16 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
}
|
}
|
||||||
// Handle timeout error
|
// Handle timeout error
|
||||||
tapParser.handleTimeout(this.timeoutSeconds);
|
tapParser.handleTimeout(this.timeoutSeconds);
|
||||||
// Ensure entire process tree is killed if still running
|
// Ensure process is killed if still running
|
||||||
try {
|
try {
|
||||||
await execResultStreaming.kill(); // This kills the entire process tree with SIGKILL
|
tsrunProcess.kill('SIGKILL');
|
||||||
} catch (killError) {
|
} catch (killError) {
|
||||||
// Process tree might already be dead
|
// Process might already be dead
|
||||||
}
|
}
|
||||||
await tapParser.evaluateFinalResult();
|
await tapParser.evaluateFinalResult();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await tapParser.handleTapProcess(execResultStreaming.childProcess);
|
await tapParser.handleTapProcess(tsrunProcess.childProcess);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear warning timer if it was set
|
// Clear warning timer if it was set
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
* - test.chromium.ts
|
* - test.chromium.ts
|
||||||
* - test.node+chromium.ts
|
* - test.node+chromium.ts
|
||||||
* - test.deno+bun.ts
|
* - test.deno+bun.ts
|
||||||
|
* - test.all.ts (runs on all runtimes)
|
||||||
* - test.chromium.nonci.ts
|
* - test.chromium.nonci.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ export interface ParserConfig {
|
|||||||
const KNOWN_RUNTIMES: Set<string> = new Set(['node', 'chromium', 'deno', 'bun']);
|
const KNOWN_RUNTIMES: Set<string> = new Set(['node', 'chromium', 'deno', 'bun']);
|
||||||
const KNOWN_MODIFIERS: Set<string> = new Set(['nonci']);
|
const KNOWN_MODIFIERS: Set<string> = new Set(['nonci']);
|
||||||
const VALID_EXTENSIONS: Set<string> = new Set(['ts', 'tsx', 'mts', 'cts']);
|
const VALID_EXTENSIONS: Set<string> = new Set(['ts', 'tsx', 'mts', 'cts']);
|
||||||
|
const ALL_RUNTIMES: Runtime[] = ['node', 'chromium', 'deno', 'bun'];
|
||||||
|
|
||||||
// Legacy mappings for backwards compatibility
|
// Legacy mappings for backwards compatibility
|
||||||
const LEGACY_RUNTIME_MAP: Record<string, Runtime[]> = {
|
const LEGACY_RUNTIME_MAP: Record<string, Runtime[]> = {
|
||||||
@@ -102,9 +104,12 @@ export function parseTestFilename(
|
|||||||
const runtimeCandidates = token.split('+').map(r => r.trim()).filter(Boolean);
|
const runtimeCandidates = token.split('+').map(r => r.trim()).filter(Boolean);
|
||||||
const validRuntimes: Runtime[] = [];
|
const validRuntimes: Runtime[] = [];
|
||||||
const invalidRuntimes: string[] = [];
|
const invalidRuntimes: string[] = [];
|
||||||
|
let hasAllKeyword = false;
|
||||||
|
|
||||||
for (const candidate of runtimeCandidates) {
|
for (const candidate of runtimeCandidates) {
|
||||||
if (KNOWN_RUNTIMES.has(candidate)) {
|
if (candidate === 'all') {
|
||||||
|
hasAllKeyword = true;
|
||||||
|
} else if (KNOWN_RUNTIMES.has(candidate)) {
|
||||||
// Dedupe: only add if not already in list
|
// Dedupe: only add if not already in list
|
||||||
if (!validRuntimes.includes(candidate as Runtime)) {
|
if (!validRuntimes.includes(candidate as Runtime)) {
|
||||||
validRuntimes.push(candidate as Runtime);
|
validRuntimes.push(candidate as Runtime);
|
||||||
@@ -114,11 +119,18 @@ export function parseTestFilename(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If 'all' keyword is present, expand to all runtimes
|
||||||
|
if (hasAllKeyword) {
|
||||||
|
runtimes = [...ALL_RUNTIMES];
|
||||||
|
runtimeTokenIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (invalidRuntimes.length > 0) {
|
if (invalidRuntimes.length > 0) {
|
||||||
if (strictUnknownRuntime) {
|
if (strictUnknownRuntime) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Unknown runtime(s) in "${fileName}": ${invalidRuntimes.join(', ')}. ` +
|
`Unknown runtime(s) in "${fileName}": ${invalidRuntimes.join(', ')}. ` +
|
||||||
`Valid runtimes: ${Array.from(KNOWN_RUNTIMES).join(', ')}`
|
`Valid runtimes: ${Array.from(KNOWN_RUNTIMES).join(', ')}, all`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
@@ -138,6 +150,13 @@ export function parseTestFilename(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is the 'all' keyword (expands to all runtimes)
|
||||||
|
if (token === 'all') {
|
||||||
|
runtimes = [...ALL_RUNTIMES];
|
||||||
|
runtimeTokenIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is a single runtime token
|
// Check if this is a single runtime token
|
||||||
if (KNOWN_RUNTIMES.has(token)) {
|
if (KNOWN_RUNTIMES.has(token)) {
|
||||||
runtimes = [token as Runtime];
|
runtimes = [token as Runtime];
|
||||||
|
@@ -62,7 +62,19 @@ export class TsTest {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check and display available runtimes
|
||||||
|
*/
|
||||||
|
private async checkEnvironment() {
|
||||||
|
const availability = await this.runtimeRegistry.checkAvailability();
|
||||||
|
this.logger.environmentCheck(availability);
|
||||||
|
return availability;
|
||||||
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
|
// Check and display environment
|
||||||
|
await this.checkEnvironment();
|
||||||
|
|
||||||
// Move previous log files if --logfile option is used
|
// Move previous log files if --logfile option is used
|
||||||
if (this.logger.options.logFile) {
|
if (this.logger.options.logFile) {
|
||||||
await this.movePreviousLogFiles();
|
await this.movePreviousLogFiles();
|
||||||
|
@@ -137,6 +137,43 @@ export class TsTestLogger {
|
|||||||
this.log(this.format(` Found: ${count} test file(s)`, 'green'));
|
this.log(this.format(` Found: ${count} test file(s)`, 'green'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Environment check - display available runtimes
|
||||||
|
environmentCheck(availability: Map<string, { available: boolean; version?: string; error?: string }>) {
|
||||||
|
if (this.options.json) {
|
||||||
|
const runtimes: any = {};
|
||||||
|
for (const [runtime, info] of availability) {
|
||||||
|
runtimes[runtime] = info;
|
||||||
|
}
|
||||||
|
this.logJson({ event: 'environmentCheck', runtimes });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.quiet) return;
|
||||||
|
|
||||||
|
this.log(this.format('\n🌍 Test Environment', 'bold'));
|
||||||
|
|
||||||
|
// Define runtime display names
|
||||||
|
const runtimeNames: Record<string, string> = {
|
||||||
|
node: 'Node.js',
|
||||||
|
deno: 'Deno',
|
||||||
|
bun: 'Bun',
|
||||||
|
chromium: 'Chrome/Chromium'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Display each runtime
|
||||||
|
for (const [runtime, info] of availability) {
|
||||||
|
const displayName = runtimeNames[runtime] || runtime;
|
||||||
|
|
||||||
|
if (info.available) {
|
||||||
|
const versionStr = info.version ? ` ${info.version}` : '';
|
||||||
|
this.log(this.format(` ✓ ${displayName}${versionStr}`, 'green'));
|
||||||
|
} else {
|
||||||
|
const errorStr = info.error ? ` (${info.error})` : '';
|
||||||
|
this.log(this.format(` ✗ ${displayName}${errorStr}`, 'dim'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Test execution
|
// Test execution
|
||||||
testFileStart(filename: string, runtime: string, index: number, total: number) {
|
testFileStart(filename: string, runtime: string, index: number, total: number) {
|
||||||
|
@@ -37,8 +37,9 @@ export {
|
|||||||
|
|
||||||
// @git.zone scope
|
// @git.zone scope
|
||||||
import * as tsbundle from '@git.zone/tsbundle';
|
import * as tsbundle from '@git.zone/tsbundle';
|
||||||
|
import * as tsrun from '@git.zone/tsrun';
|
||||||
|
|
||||||
export { tsbundle };
|
export { tsbundle, tsrun };
|
||||||
|
|
||||||
// sindresorhus
|
// sindresorhus
|
||||||
import figures from 'figures';
|
import figures from 'figures';
|
||||||
|
Reference in New Issue
Block a user