Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e53e5143e | |||
| cf4e5c33e8 | |||
| bc39793682 | |||
| edce15b20a | |||
| 9d34a3511a | |||
| 300e03628c | |||
| 893d6e40dc | |||
| 8b16ba1d9a | |||
| 8c41d18f84 | |||
| 69263b3efc |
39
changelog.md
39
changelog.md
@@ -1,5 +1,44 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-19 - 3.5.0 - feat(tstest)
|
||||||
|
add support for package.json before scripts during test execution
|
||||||
|
|
||||||
|
- load test:before or test:before:once once per test run from package.json scripts
|
||||||
|
- run test:before:testfile before each test file execution and pass TSTEST_FILE and TSTEST_RUNTIME environment variables
|
||||||
|
- log before-script lifecycle events and abort or skip execution when setup scripts fail
|
||||||
|
|
||||||
|
## 2026-03-18 - 3.4.0 - feat(tapbundle,deno)
|
||||||
|
replace smarts3 test tooling with smartstorage and pre-resolve Deno test dependencies
|
||||||
|
|
||||||
|
- switch TapNodeTools storage helper from @push.rocks/smarts3 to @push.rocks/smartstorage and rename createSmarts3() to createSmartStorage()
|
||||||
|
- update node tests and tapbundle server-side documentation to use the new smartstorage helper
|
||||||
|
- run `deno install --entrypoint` before executing Deno tests to resolve dependencies up front
|
||||||
|
- bump supporting development dependencies including @types/node and @push.rocks/smartshell
|
||||||
|
|
||||||
|
## 2026-03-09 - 3.3.2 - fix(deps)
|
||||||
|
bump dependency versions and reorder smartserve in package.json
|
||||||
|
|
||||||
|
- bump @push.rocks/smartbrowser from ^2.0.10 to ^2.0.11
|
||||||
|
- bump @push.rocks/smartfs from ^1.4.0 to ^1.5.0
|
||||||
|
- move @push.rocks/smartserve to later position in dependencies (version unchanged: ^2.0.1)
|
||||||
|
|
||||||
|
## 2026-03-09 - 3.3.1 - fix(serve)
|
||||||
|
migrate test HTTP server to @push.rocks/smartserve and update related dependencies
|
||||||
|
|
||||||
|
- Replace @api.global/typedserver with @push.rocks/smartserve and FileServer; use SmartServe.setHandler to serve static assets and a custom /test response.
|
||||||
|
- Export smartserve from ts/tstest.plugins.ts and remove typedserver import/export.
|
||||||
|
- Update package.json dependencies: add @push.rocks/smartserve@^2.0.1 and bump @push.rocks/smartbrowser to ^2.0.10.
|
||||||
|
|
||||||
|
## 2026-03-06 - 3.3.0 - feat(testfile-directives)
|
||||||
|
Add per-test file directives to control runtime permissions and flags (Deno, Node, Bun, Chromium)
|
||||||
|
|
||||||
|
- Introduce test file directive parser (ts/tstest.classes.testfile.directives.ts) to parse comments like // tstest:deno:allowAll and map them to runtime options.
|
||||||
|
- Add DENO_DEFAULT_PERMISSIONS constant and centralize Deno default flags (ts/tstest.classes.runtime.deno.ts) to avoid repeating the list.
|
||||||
|
- Integrate directives into the test runner (ts/tstest.classes.tstest.ts): read directives from test files and optional 00init.ts, merge them, and pass runtime-specific options to adapters.
|
||||||
|
- Documentation: add a "Test File Directives" section to readme.md with examples and available directives.
|
||||||
|
- Add automated tests for directives behavior (test/test.directives.node.ts).
|
||||||
|
- Bump package metadata and minor dependency updates; update package description and npmextra.json to reflect new functionality.
|
||||||
|
|
||||||
## 2026-03-03 - 3.2.0 - feat(tapbundle_serverside)
|
## 2026-03-03 - 3.2.0 - feat(tapbundle_serverside)
|
||||||
add network port discovery utilities and migrate file I/O to smartfs; refactor runtimes to use Node fs and SmartFs, update server APIs and bump dependencies
|
add network port discovery utilities and migrate file I/O to smartfs; refactor runtimes to use Node fs and SmartFs, update server APIs and bump dependencies
|
||||||
|
|
||||||
|
|||||||
@@ -9,5 +9,5 @@
|
|||||||
"target": "ES2022"
|
"target": "ES2022"
|
||||||
},
|
},
|
||||||
"nodeModulesDir": true,
|
"nodeModulesDir": true,
|
||||||
"version": "3.2.0"
|
"version": "3.5.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"githost": "code.foss.global",
|
"githost": "code.foss.global",
|
||||||
"gitscope": "git.zone",
|
"gitscope": "git.zone",
|
||||||
"gitrepo": "tstest",
|
"gitrepo": "tstest",
|
||||||
"description": "a test utility to run tests that match test/**/*.ts",
|
"description": "A powerful, modern test runner for TypeScript with multi-runtime support (Node.js, Deno, Bun, Chromium) and a batteries-included test framework.",
|
||||||
"npmPackagename": "@git.zone/tstest",
|
"npmPackagename": "@git.zone/tstest",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
|||||||
22
package.json
22
package.json
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tstest",
|
"name": "@git.zone/tstest",
|
||||||
"version": "3.2.0",
|
"version": "3.5.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "a test utility to run tests that match test/**/*.ts",
|
"description": "A powerful, modern test runner for TypeScript with multi-runtime support (Node.js, Deno, Bun, Chromium) and a batteries-included test framework.",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./dist_ts/index.js",
|
".": "./dist_ts/index.js",
|
||||||
"./tapbundle": "./dist_ts_tapbundle/index.js",
|
"./tapbundle": "./dist_ts_tapbundle/index.js",
|
||||||
@@ -25,22 +25,21 @@
|
|||||||
"buildDocs": "tsdoc"
|
"buildDocs": "tsdoc"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^4.1.2",
|
"@git.zone/tsbuild": "^4.3.0",
|
||||||
"@types/node": "^22.15.21"
|
"@types/node": "^25.5.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@api.global/typedserver": "^8.4.0",
|
"@git.zone/tsbundle": "^2.9.1",
|
||||||
"@git.zone/tsbundle": "^2.9.0",
|
|
||||||
"@git.zone/tsrun": "^2.0.1",
|
"@git.zone/tsrun": "^2.0.1",
|
||||||
"@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.11",
|
||||||
"@push.rocks/smartcrypto": "^2.0.4",
|
"@push.rocks/smartcrypto": "^2.0.4",
|
||||||
"@push.rocks/smartdelay": "^3.0.5",
|
"@push.rocks/smartdelay": "^3.0.5",
|
||||||
"@push.rocks/smartenv": "^6.0.0",
|
"@push.rocks/smartenv": "^6.0.0",
|
||||||
"@push.rocks/smartexpect": "^2.5.0",
|
"@push.rocks/smartexpect": "^2.5.0",
|
||||||
"@push.rocks/smartfile": "^13.1.2",
|
"@push.rocks/smartfile": "^13.1.2",
|
||||||
"@push.rocks/smartfs": "^1.3.1",
|
"@push.rocks/smartfs": "^1.5.0",
|
||||||
"@push.rocks/smartjson": "^6.0.0",
|
"@push.rocks/smartjson": "^6.0.0",
|
||||||
"@push.rocks/smartlog": "^3.2.1",
|
"@push.rocks/smartlog": "^3.2.1",
|
||||||
"@push.rocks/smartmongo": "^5.1.0",
|
"@push.rocks/smartmongo": "^5.1.0",
|
||||||
@@ -48,10 +47,11 @@
|
|||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"@push.rocks/smartpromise": "^4.2.3",
|
"@push.rocks/smartpromise": "^4.2.3",
|
||||||
"@push.rocks/smartrequest": "^5.0.1",
|
"@push.rocks/smartrequest": "^5.0.1",
|
||||||
"@push.rocks/smarts3": "^5.3.0",
|
"@push.rocks/smartserve": "^2.0.1",
|
||||||
"@push.rocks/smartshell": "^3.3.0",
|
"@push.rocks/smartshell": "^3.3.8",
|
||||||
"@push.rocks/smartwatch": "^6.3.0",
|
"@push.rocks/smartstorage": "^6.0.1",
|
||||||
"@push.rocks/smarttime": "^4.2.3",
|
"@push.rocks/smarttime": "^4.2.3",
|
||||||
|
"@push.rocks/smartwatch": "^6.3.0",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"figures": "^6.1.0",
|
"figures": "^6.1.0",
|
||||||
"ws": "^8.19.0"
|
"ws": "^8.19.0"
|
||||||
|
|||||||
3141
pnpm-lock.yaml
generated
3141
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
64
readme.md
64
readme.md
@@ -442,6 +442,70 @@ const s3 = await tapNodeTools.createSmarts3();
|
|||||||
await s3.stop();
|
await s3.stop();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Test File Directives
|
||||||
|
|
||||||
|
Control runtime behavior directly from your test files using special comment directives at the top of the file. Directives must appear before any `import` statements.
|
||||||
|
|
||||||
|
### Deno Permissions
|
||||||
|
|
||||||
|
By default, Deno tests run with `--allow-read`, `--allow-env`, `--allow-net`, `--allow-write`, `--allow-sys`, and `--allow-import`. Add directives to request additional permissions:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tstest:deno:allowAll
|
||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
tap.test('test with full Deno permissions', async () => {
|
||||||
|
// Runs with --allow-all (e.g., for FFI, subprocess spawning, etc.)
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Directives
|
||||||
|
|
||||||
|
| Directive | Effect |
|
||||||
|
|---|---|
|
||||||
|
| `// tstest:deno:allowAll` | Grants all Deno permissions (`--allow-all`) |
|
||||||
|
| `// tstest:deno:allowRun` | Adds `--allow-run` for subprocess spawning |
|
||||||
|
| `// tstest:deno:allowFfi` | Adds `--allow-ffi` for native library calls |
|
||||||
|
| `// tstest:deno:allowHrtime` | Adds `--allow-hrtime` for high-res timers |
|
||||||
|
| `// tstest:deno:flag:--unstable-ffi` | Passes any arbitrary Deno flag |
|
||||||
|
| `// tstest:node:flag:--max-old-space-size=4096` | Passes flags to Node.js |
|
||||||
|
| `// tstest:bun:flag:--smol` | Passes flags to Bun |
|
||||||
|
|
||||||
|
### Multiple Directives
|
||||||
|
|
||||||
|
Combine as many directives as needed:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// tstest:deno:allowRun
|
||||||
|
// tstest:deno:allowFfi
|
||||||
|
// tstest:deno:flag:--unstable-ffi
|
||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
|
||||||
|
tap.test('test with Rust FFI', async () => {
|
||||||
|
// Has --allow-run, --allow-ffi, and --unstable-ffi in addition to defaults
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shared Directives via 00init.ts
|
||||||
|
|
||||||
|
Directives in a `00init.ts` file apply to all test files in that directory. Test file directives are merged with (and extend) init file directives.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// test/00init.ts
|
||||||
|
// tstest:deno:allowRun
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// test/test.mytest.deno.ts
|
||||||
|
// tstest:deno:allowFfi
|
||||||
|
// Both --allow-run (from 00init.ts) and --allow-ffi are active
|
||||||
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||||
|
```
|
||||||
|
|
||||||
## Advanced Features
|
## Advanced Features
|
||||||
|
|
||||||
### Watch Mode
|
### Watch Mode
|
||||||
|
|||||||
@@ -20,9 +20,9 @@ tap.test('should create a smartmongo instance', async () => {
|
|||||||
await smartmongo.stop();
|
await smartmongo.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.test('should create a smarts3 instance', async () => {
|
tap.test('should create a smartstorage instance', async () => {
|
||||||
const smarts3 = await tapNodeTools.createSmarts3();
|
const smartstorage = await tapNodeTools.createSmartStorage();
|
||||||
await smarts3.stop();
|
await smartstorage.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
tap.start();
|
||||||
|
|||||||
153
test/test.directives.node.ts
Normal file
153
test/test.directives.node.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { expect, tap } from '../ts_tapbundle/index.js';
|
||||||
|
import {
|
||||||
|
parseDirectivesFromContent,
|
||||||
|
mergeDirectives,
|
||||||
|
directivesToRuntimeOptions,
|
||||||
|
hasDirectives,
|
||||||
|
} from '../ts/tstest.classes.testfile.directives.js';
|
||||||
|
|
||||||
|
tap.test('parseDirectivesFromContent - deno allowAll', async () => {
|
||||||
|
const content = `// tstest:deno:allowAll
|
||||||
|
import { tap } from '../tapbundle/index.js';
|
||||||
|
`;
|
||||||
|
const directives = parseDirectivesFromContent(content);
|
||||||
|
expect(directives.deno.length).toEqual(1);
|
||||||
|
expect(directives.deno[0].key).toEqual('allowAll');
|
||||||
|
expect(directives.deno[0].scope).toEqual('deno');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('parseDirectivesFromContent - multiple deno directives', async () => {
|
||||||
|
const content = `// tstest:deno:allowRun
|
||||||
|
// tstest:deno:allowFfi
|
||||||
|
import { tap } from '../tapbundle/index.js';
|
||||||
|
`;
|
||||||
|
const directives = parseDirectivesFromContent(content);
|
||||||
|
expect(directives.deno.length).toEqual(2);
|
||||||
|
expect(directives.deno[0].key).toEqual('allowRun');
|
||||||
|
expect(directives.deno[1].key).toEqual('allowFfi');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('parseDirectivesFromContent - flag directive with value', async () => {
|
||||||
|
const content = `// tstest:deno:flag:--unstable-ffi
|
||||||
|
import { tap } from '../tapbundle/index.js';
|
||||||
|
`;
|
||||||
|
const directives = parseDirectivesFromContent(content);
|
||||||
|
expect(directives.deno.length).toEqual(1);
|
||||||
|
expect(directives.deno[0].key).toEqual('flag');
|
||||||
|
expect(directives.deno[0].value).toEqual('--unstable-ffi');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('parseDirectivesFromContent - node flag directive', async () => {
|
||||||
|
const content = `// tstest:node:flag:--max-old-space-size=4096
|
||||||
|
import { tap } from '../tapbundle/index.js';
|
||||||
|
`;
|
||||||
|
const directives = parseDirectivesFromContent(content);
|
||||||
|
expect(directives.node.length).toEqual(1);
|
||||||
|
expect(directives.node[0].key).toEqual('flag');
|
||||||
|
expect(directives.node[0].value).toEqual('--max-old-space-size=4096');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('parseDirectivesFromContent - empty lines before directives', async () => {
|
||||||
|
const content = `
|
||||||
|
// tstest:deno:allowAll
|
||||||
|
import { tap } from '../tapbundle/index.js';
|
||||||
|
`;
|
||||||
|
const directives = parseDirectivesFromContent(content);
|
||||||
|
expect(directives.deno.length).toEqual(1);
|
||||||
|
expect(directives.deno[0].key).toEqual('allowAll');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('parseDirectivesFromContent - stops at first non-comment line', async () => {
|
||||||
|
const content = `// tstest:deno:allowRun
|
||||||
|
import { tap } from '../tapbundle/index.js';
|
||||||
|
// tstest:deno:allowFfi
|
||||||
|
`;
|
||||||
|
const directives = parseDirectivesFromContent(content);
|
||||||
|
// Should only find allowRun, not allowFfi (after import)
|
||||||
|
expect(directives.deno.length).toEqual(1);
|
||||||
|
expect(directives.deno[0].key).toEqual('allowRun');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('parseDirectivesFromContent - no directives returns empty', async () => {
|
||||||
|
const content = `import { tap } from '../tapbundle/index.js';
|
||||||
|
tap.test('foo', async () => {});
|
||||||
|
`;
|
||||||
|
const directives = parseDirectivesFromContent(content);
|
||||||
|
expect(hasDirectives(directives)).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('parseDirectivesFromContent - regular comments are skipped', async () => {
|
||||||
|
const content = `// This is a regular comment
|
||||||
|
// tstest:deno:allowAll
|
||||||
|
import { tap } from '../tapbundle/index.js';
|
||||||
|
`;
|
||||||
|
const directives = parseDirectivesFromContent(content);
|
||||||
|
expect(directives.deno.length).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('parseDirectivesFromContent - mixed runtime directives', async () => {
|
||||||
|
const content = `// tstest:deno:allowRun
|
||||||
|
// tstest:bun:flag:--smol
|
||||||
|
import { tap } from '../tapbundle/index.js';
|
||||||
|
`;
|
||||||
|
const directives = parseDirectivesFromContent(content);
|
||||||
|
expect(directives.deno.length).toEqual(1);
|
||||||
|
expect(directives.bun.length).toEqual(1);
|
||||||
|
expect(directives.bun[0].key).toEqual('flag');
|
||||||
|
expect(directives.bun[0].value).toEqual('--smol');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('directivesToRuntimeOptions - deno allowAll', async () => {
|
||||||
|
const content = `// tstest:deno:allowAll
|
||||||
|
import { tap } from '../tapbundle/index.js';
|
||||||
|
`;
|
||||||
|
const directives = parseDirectivesFromContent(content);
|
||||||
|
const options = directivesToRuntimeOptions(directives, 'deno') as any;
|
||||||
|
expect(options).toBeTruthy();
|
||||||
|
expect(options.permissions).toContain('--allow-all');
|
||||||
|
expect(options.permissions).not.toContain('--allow-read');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('directivesToRuntimeOptions - deno extra permissions', async () => {
|
||||||
|
const content = `// tstest:deno:allowRun
|
||||||
|
// tstest:deno:allowFfi
|
||||||
|
import { tap } from '../tapbundle/index.js';
|
||||||
|
`;
|
||||||
|
const directives = parseDirectivesFromContent(content);
|
||||||
|
const options = directivesToRuntimeOptions(directives, 'deno') as any;
|
||||||
|
expect(options).toBeTruthy();
|
||||||
|
expect(options.permissions).toContain('--allow-run');
|
||||||
|
expect(options.permissions).toContain('--allow-ffi');
|
||||||
|
// Should still contain defaults
|
||||||
|
expect(options.permissions).toContain('--allow-read');
|
||||||
|
expect(options.permissions).toContain('--allow-env');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('directivesToRuntimeOptions - no directives returns undefined', async () => {
|
||||||
|
const directives = parseDirectivesFromContent('import { tap } from "tapbundle";');
|
||||||
|
const options = directivesToRuntimeOptions(directives, 'deno');
|
||||||
|
expect(options).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('directivesToRuntimeOptions - node flag directive', async () => {
|
||||||
|
const content = `// tstest:node:flag:--max-old-space-size=4096
|
||||||
|
import { tap } from '../tapbundle/index.js';
|
||||||
|
`;
|
||||||
|
const directives = parseDirectivesFromContent(content);
|
||||||
|
const options = directivesToRuntimeOptions(directives, 'node');
|
||||||
|
expect(options).toBeTruthy();
|
||||||
|
expect(options.extraArgs).toContain('--max-old-space-size=4096');
|
||||||
|
});
|
||||||
|
|
||||||
|
tap.test('mergeDirectives - combines directives from init and test file', async () => {
|
||||||
|
const init = parseDirectivesFromContent(`// tstest:deno:allowRun
|
||||||
|
`);
|
||||||
|
const testFile = parseDirectivesFromContent(`// tstest:deno:allowFfi
|
||||||
|
`);
|
||||||
|
const merged = mergeDirectives(init, testFile);
|
||||||
|
expect(merged.deno.length).toEqual(2);
|
||||||
|
expect(merged.deno[0].key).toEqual('allowRun');
|
||||||
|
expect(merged.deno[1].key).toEqual('allowFfi');
|
||||||
|
});
|
||||||
|
|
||||||
|
export default tap.start();
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tstest',
|
name: '@git.zone/tstest',
|
||||||
version: '3.2.0',
|
version: '3.5.0',
|
||||||
description: 'a test utility to run tests that match test/**/*.ts'
|
description: 'A powerful, modern test runner for TypeScript with multi-runtime support (Node.js, Deno, Bun, Chromium) and a batteries-included test framework.'
|
||||||
}
|
}
|
||||||
|
|||||||
86
ts/tstest.classes.beforescripts.ts
Normal file
86
ts/tstest.classes.beforescripts.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
import * as plugins from './tstest.plugins.js';
|
||||||
|
import type { TsTestLogger } from './tstest.logging.js';
|
||||||
|
import type { Smartshell } from '@push.rocks/smartshell';
|
||||||
|
|
||||||
|
export interface IBeforeScripts {
|
||||||
|
/** The "test:before" or "test:before:once" script command, or null if not defined */
|
||||||
|
beforeOnce: string | null;
|
||||||
|
/** The "test:before:testfile" script command, or null if not defined */
|
||||||
|
beforeTestfile: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load before-script commands from the project's package.json scripts section.
|
||||||
|
*/
|
||||||
|
export function loadBeforeScripts(cwd: string): IBeforeScripts {
|
||||||
|
const result: IBeforeScripts = { beforeOnce: null, beforeTestfile: null };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const packageJsonPath = plugins.path.join(cwd, 'package.json');
|
||||||
|
const packageJson = JSON.parse(plugins.fs.readFileSync(packageJsonPath, 'utf8'));
|
||||||
|
const scripts = packageJson?.scripts;
|
||||||
|
if (!scripts) return result;
|
||||||
|
|
||||||
|
// test:before takes precedence over test:before:once (they are aliases)
|
||||||
|
if (scripts['test:before']) {
|
||||||
|
result.beforeOnce = scripts['test:before'];
|
||||||
|
if (scripts['test:before:once']) {
|
||||||
|
console.warn('Warning: Both "test:before" and "test:before:once" are defined. Using "test:before".');
|
||||||
|
}
|
||||||
|
} else if (scripts['test:before:once']) {
|
||||||
|
result.beforeOnce = scripts['test:before:once'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scripts['test:before:testfile']) {
|
||||||
|
result.beforeTestfile = scripts['test:before:testfile'];
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// No package.json or parse error — return defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a before-script command and return whether it succeeded.
|
||||||
|
*/
|
||||||
|
export async function runBeforeScript(
|
||||||
|
smartshellInstance: Smartshell,
|
||||||
|
command: string,
|
||||||
|
label: string,
|
||||||
|
logger: TsTestLogger,
|
||||||
|
env?: { TSTEST_FILE?: string; TSTEST_RUNTIME?: string },
|
||||||
|
): Promise<boolean> {
|
||||||
|
logger.beforeScriptStart(label, command);
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// Set environment variables if provided
|
||||||
|
const envKeysSet: string[] = [];
|
||||||
|
if (env) {
|
||||||
|
for (const [key, value] of Object.entries(env)) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
process.env[key] = value;
|
||||||
|
envKeysSet.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const execResult = await smartshellInstance.execStreaming(command);
|
||||||
|
const result = await execResult.finalPromise;
|
||||||
|
const durationMs = Date.now() - startTime;
|
||||||
|
const success = result.exitCode === 0;
|
||||||
|
|
||||||
|
logger.beforeScriptEnd(label, success, durationMs);
|
||||||
|
return success;
|
||||||
|
} catch {
|
||||||
|
const durationMs = Date.now() - startTime;
|
||||||
|
logger.beforeScriptEnd(label, false, durationMs);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
// Clean up environment variables
|
||||||
|
for (const key of envKeysSet) {
|
||||||
|
delete process.env[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -116,24 +116,27 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
|
|||||||
// Find free ports for HTTP and WebSocket
|
// Find free ports for HTTP and WebSocket
|
||||||
const { httpPort, wsPort } = await this.findFreePorts();
|
const { httpPort, wsPort } = await this.findFreePorts();
|
||||||
|
|
||||||
// lets create a server
|
// Use SmartServe with setHandler() to bypass global ControllerRegistry
|
||||||
const server = new plugins.typedserver.TypedServer({
|
const fileServer = new plugins.smartserve.FileServer({ root: tsbundleCacheDirPath });
|
||||||
cors: true,
|
const server = new plugins.smartserve.SmartServe({ port: httpPort });
|
||||||
port: httpPort,
|
server.setHandler(async (request: Request) => {
|
||||||
serveDir: tsbundleCacheDirPath,
|
const url = new URL(request.url);
|
||||||
});
|
if (url.pathname === '/test') {
|
||||||
server.addRoute('/test', 'GET', async () => {
|
return new Response(`
|
||||||
return new Response(`
|
<html>
|
||||||
<html>
|
<head>
|
||||||
<head>
|
<script>
|
||||||
<script>
|
globalThis.testdom = true;
|
||||||
globalThis.testdom = true;
|
globalThis.wsPort = ${wsPort};
|
||||||
globalThis.wsPort = ${wsPort};
|
</script>
|
||||||
</script>
|
</head>
|
||||||
</head>
|
<body></body>
|
||||||
<body></body>
|
</html>
|
||||||
</html>
|
`, { headers: { 'Content-Type': 'text/html' } });
|
||||||
`, { headers: { 'Content-Type': 'text/html' } });
|
}
|
||||||
|
const staticResponse = await fileServer.serve(request);
|
||||||
|
if (staticResponse) return staticResponse;
|
||||||
|
return new Response('Not Found', { status: 404 });
|
||||||
});
|
});
|
||||||
await server.start();
|
await server.start();
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,20 @@ import { TapParser } from './tstest.classes.tap.parser.js';
|
|||||||
import { TsTestLogger } from './tstest.logging.js';
|
import { TsTestLogger } from './tstest.logging.js';
|
||||||
import type { Runtime } from './tstest.classes.runtime.parser.js';
|
import type { Runtime } from './tstest.classes.runtime.parser.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default Deno permissions used when no directives override them.
|
||||||
|
*/
|
||||||
|
export const DENO_DEFAULT_PERMISSIONS = [
|
||||||
|
'--allow-read',
|
||||||
|
'--allow-env',
|
||||||
|
'--allow-net',
|
||||||
|
'--allow-write',
|
||||||
|
'--allow-sys',
|
||||||
|
'--allow-import',
|
||||||
|
'--node-modules-dir',
|
||||||
|
'--sloppy-imports',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deno runtime adapter
|
* Deno runtime adapter
|
||||||
* Executes tests using the Deno runtime
|
* Executes tests using the Deno runtime
|
||||||
@@ -45,16 +59,7 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
|
|||||||
return {
|
return {
|
||||||
...super.getDefaultOptions(),
|
...super.getDefaultOptions(),
|
||||||
configPath,
|
configPath,
|
||||||
permissions: [
|
permissions: [...DENO_DEFAULT_PERMISSIONS],
|
||||||
'--allow-read',
|
|
||||||
'--allow-env',
|
|
||||||
'--allow-net',
|
|
||||||
'--allow-write',
|
|
||||||
'--allow-sys', // Allow system info access
|
|
||||||
'--allow-import', // Allow npm/node imports
|
|
||||||
'--node-modules-dir', // Enable Node.js compatibility mode
|
|
||||||
'--sloppy-imports', // Allow .js imports to resolve to .ts files
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,16 +107,7 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
|
|||||||
const args: string[] = ['run'];
|
const args: string[] = ['run'];
|
||||||
|
|
||||||
// Add permissions
|
// Add permissions
|
||||||
const permissions = mergedOptions.permissions || [
|
const permissions = mergedOptions.permissions || [...DENO_DEFAULT_PERMISSIONS];
|
||||||
'--allow-read',
|
|
||||||
'--allow-env',
|
|
||||||
'--allow-net',
|
|
||||||
'--allow-write',
|
|
||||||
'--allow-sys',
|
|
||||||
'--allow-import',
|
|
||||||
'--node-modules-dir',
|
|
||||||
'--sloppy-imports',
|
|
||||||
];
|
|
||||||
args.push(...permissions);
|
args.push(...permissions);
|
||||||
|
|
||||||
// Add config file if specified
|
// Add config file if specified
|
||||||
@@ -194,6 +190,17 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
runCommand = `${loaderCommand.command} ${loaderCommand.args.join(' ')}`;
|
runCommand = `${loaderCommand.command} ${loaderCommand.args.join(' ')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-resolve dependencies for the Deno test entrypoint
|
||||||
|
const installTarget = loaderPath || testFile;
|
||||||
|
const installArgs = ['install', '--entrypoint', installTarget];
|
||||||
|
if (mergedOptions.configPath) {
|
||||||
|
installArgs.push('--config', mergedOptions.configPath);
|
||||||
|
}
|
||||||
|
const installCommand = `deno ${installArgs.join(' ')}`;
|
||||||
|
console.log(cs(` ⏳ Resolving Deno dependencies for ${plugins.path.basename(testFile)}...`, 'blue'));
|
||||||
|
await this.smartshellInstance.execSilent(installCommand, { cwd: process.cwd() });
|
||||||
|
console.log(cs(` ✓ Deno dependencies resolved`, 'green'));
|
||||||
|
|
||||||
const execResultStreaming = await this.smartshellInstance.execStreamingSilent(runCommand);
|
const execResultStreaming = await this.smartshellInstance.execStreamingSilent(runCommand);
|
||||||
|
|
||||||
// If we created a loader file, clean it up after test execution
|
// If we created a loader file, clean it up after test execution
|
||||||
|
|||||||
226
ts/tstest.classes.testfile.directives.ts
Normal file
226
ts/tstest.classes.testfile.directives.ts
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import * as plugins from './tstest.plugins.js';
|
||||||
|
import type { DenoOptions, RuntimeOptions } from './tstest.classes.runtime.adapter.js';
|
||||||
|
import type { Runtime } from './tstest.classes.runtime.parser.js';
|
||||||
|
import { DENO_DEFAULT_PERMISSIONS } from './tstest.classes.runtime.deno.js';
|
||||||
|
|
||||||
|
type DirectiveScope = Runtime | 'global';
|
||||||
|
|
||||||
|
export interface ITestFileDirective {
|
||||||
|
scope: DirectiveScope;
|
||||||
|
key: string;
|
||||||
|
value?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IParsedDirectives {
|
||||||
|
deno: ITestFileDirective[];
|
||||||
|
node: ITestFileDirective[];
|
||||||
|
bun: ITestFileDirective[];
|
||||||
|
chromium: ITestFileDirective[];
|
||||||
|
global: ITestFileDirective[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const VALID_SCOPES = new Set<string>(['deno', 'node', 'bun', 'chromium']);
|
||||||
|
|
||||||
|
const DENO_PERMISSION_MAP: Record<string, string> = {
|
||||||
|
allowAll: '--allow-all',
|
||||||
|
allowRun: '--allow-run',
|
||||||
|
allowFfi: '--allow-ffi',
|
||||||
|
allowHrtime: '--allow-hrtime',
|
||||||
|
allowRead: '--allow-read',
|
||||||
|
allowWrite: '--allow-write',
|
||||||
|
allowNet: '--allow-net',
|
||||||
|
allowEnv: '--allow-env',
|
||||||
|
allowSys: '--allow-sys',
|
||||||
|
};
|
||||||
|
|
||||||
|
function createEmptyDirectives(): IParsedDirectives {
|
||||||
|
return { deno: [], node: [], bun: [], chromium: [], global: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse tstest directives from file content.
|
||||||
|
* Scans comments at the top of the file (before any code).
|
||||||
|
*/
|
||||||
|
export function parseDirectivesFromContent(content: string): IParsedDirectives {
|
||||||
|
const result = createEmptyDirectives();
|
||||||
|
const lines = content.split('\n');
|
||||||
|
const maxLines = Math.min(lines.length, 30);
|
||||||
|
|
||||||
|
for (let i = 0; i < maxLines; i++) {
|
||||||
|
const line = lines[i].trim();
|
||||||
|
|
||||||
|
// Skip empty lines
|
||||||
|
if (line === '') continue;
|
||||||
|
|
||||||
|
// Stop at first non-comment line
|
||||||
|
if (!line.startsWith('//')) break;
|
||||||
|
|
||||||
|
// Match tstest directive: // tstest:<rest>
|
||||||
|
const match = line.match(/^\/\/\s*tstest:(.+)$/);
|
||||||
|
if (!match) continue;
|
||||||
|
|
||||||
|
const parts = match[1].split(':');
|
||||||
|
if (parts.length < 2) {
|
||||||
|
console.warn(`Warning: malformed tstest directive: "${line}"`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scopeStr = parts[0].trim();
|
||||||
|
const key = parts[1].trim();
|
||||||
|
const value = parts.length > 2 ? parts.slice(2).join(':').trim() : undefined;
|
||||||
|
|
||||||
|
// Handle global directives (env, timeout)
|
||||||
|
if (scopeStr === 'env' || scopeStr === 'timeout') {
|
||||||
|
result.global.push({
|
||||||
|
scope: 'global',
|
||||||
|
key: scopeStr,
|
||||||
|
value: key + (value !== undefined ? ':' + value : ''),
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!VALID_SCOPES.has(scopeStr)) {
|
||||||
|
console.warn(`Warning: unknown tstest directive scope "${scopeStr}" in: "${line}"`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const scope = scopeStr as Runtime;
|
||||||
|
result[scope].push({ scope, key, value });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse directives from a test file on disk.
|
||||||
|
*/
|
||||||
|
export async function parseDirectivesFromFile(filePath: string): Promise<IParsedDirectives> {
|
||||||
|
try {
|
||||||
|
const content = plugins.fs.readFileSync(filePath, 'utf8');
|
||||||
|
return parseDirectivesFromContent(content);
|
||||||
|
} catch {
|
||||||
|
return createEmptyDirectives();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge directives from 00init.ts and the test file.
|
||||||
|
* Test file directives are appended (take effect after init directives).
|
||||||
|
*/
|
||||||
|
export function mergeDirectives(init: IParsedDirectives, testFile: IParsedDirectives): IParsedDirectives {
|
||||||
|
return {
|
||||||
|
deno: [...init.deno, ...testFile.deno],
|
||||||
|
node: [...init.node, ...testFile.node],
|
||||||
|
bun: [...init.bun, ...testFile.bun],
|
||||||
|
chromium: [...init.chromium, ...testFile.chromium],
|
||||||
|
global: [...init.global, ...testFile.global],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any directives exist for any scope.
|
||||||
|
*/
|
||||||
|
export function hasDirectives(directives: IParsedDirectives): boolean {
|
||||||
|
return (
|
||||||
|
directives.deno.length > 0 ||
|
||||||
|
directives.node.length > 0 ||
|
||||||
|
directives.bun.length > 0 ||
|
||||||
|
directives.chromium.length > 0 ||
|
||||||
|
directives.global.length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert parsed directives into DenoOptions.
|
||||||
|
*/
|
||||||
|
function directivesToDenoOptions(directives: IParsedDirectives): DenoOptions | undefined {
|
||||||
|
const denoDirectives = directives.deno;
|
||||||
|
if (denoDirectives.length === 0 && directives.global.length === 0) return undefined;
|
||||||
|
|
||||||
|
const options: DenoOptions = {};
|
||||||
|
const extraPermissions: string[] = [];
|
||||||
|
const extraArgs: string[] = [];
|
||||||
|
const env: Record<string, string> = {};
|
||||||
|
let useAllowAll = false;
|
||||||
|
|
||||||
|
for (const d of denoDirectives) {
|
||||||
|
if (d.key === 'allowAll') {
|
||||||
|
useAllowAll = true;
|
||||||
|
} else if (DENO_PERMISSION_MAP[d.key]) {
|
||||||
|
extraPermissions.push(DENO_PERMISSION_MAP[d.key]);
|
||||||
|
} else if (d.key === 'flag' && d.value) {
|
||||||
|
extraArgs.push(d.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process global directives
|
||||||
|
for (const d of directives.global) {
|
||||||
|
if (d.key === 'env' && d.value) {
|
||||||
|
const eqIndex = d.value.indexOf('=');
|
||||||
|
if (eqIndex > 0) {
|
||||||
|
env[d.value.substring(0, eqIndex)] = d.value.substring(eqIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useAllowAll) {
|
||||||
|
// --allow-all replaces individual permissions, but keep compatibility flags
|
||||||
|
options.permissions = ['--allow-all', '--node-modules-dir', '--sloppy-imports'];
|
||||||
|
} else if (extraPermissions.length > 0) {
|
||||||
|
// Start with defaults and add extra permissions (deduplicated)
|
||||||
|
const allPermissions = [...DENO_DEFAULT_PERMISSIONS];
|
||||||
|
for (const p of extraPermissions) {
|
||||||
|
if (!allPermissions.includes(p)) {
|
||||||
|
allPermissions.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
options.permissions = allPermissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraArgs.length > 0) options.extraArgs = extraArgs;
|
||||||
|
if (Object.keys(env).length > 0) options.env = env;
|
||||||
|
|
||||||
|
// Return undefined if nothing was set
|
||||||
|
if (!options.permissions && !options.extraArgs && !options.env) return undefined;
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert parsed directives into RuntimeOptions for Node/Bun (flag directives only).
|
||||||
|
*/
|
||||||
|
function directivesToGenericOptions(directives: ITestFileDirective[], globalDirectives: ITestFileDirective[]): RuntimeOptions | undefined {
|
||||||
|
const extraArgs: string[] = [];
|
||||||
|
const env: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const d of directives) {
|
||||||
|
if (d.key === 'flag' && d.value) {
|
||||||
|
extraArgs.push(d.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const d of globalDirectives) {
|
||||||
|
if (d.key === 'env' && d.value) {
|
||||||
|
const eqIndex = d.value.indexOf('=');
|
||||||
|
if (eqIndex > 0) {
|
||||||
|
env[d.value.substring(0, eqIndex)] = d.value.substring(eqIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extraArgs.length === 0 && Object.keys(env).length === 0) return undefined;
|
||||||
|
|
||||||
|
const options: RuntimeOptions = {};
|
||||||
|
if (extraArgs.length > 0) options.extraArgs = extraArgs;
|
||||||
|
if (Object.keys(env).length > 0) options.env = env;
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert parsed directives into RuntimeOptions for a specific runtime.
|
||||||
|
*/
|
||||||
|
export function directivesToRuntimeOptions(directives: IParsedDirectives, runtime: Runtime): RuntimeOptions | undefined {
|
||||||
|
if (runtime === 'deno') {
|
||||||
|
return directivesToDenoOptions(directives);
|
||||||
|
}
|
||||||
|
return directivesToGenericOptions(directives[runtime] || [], directives.global);
|
||||||
|
}
|
||||||
@@ -19,6 +19,17 @@ import { DenoRuntimeAdapter } from './tstest.classes.runtime.deno.js';
|
|||||||
import { BunRuntimeAdapter } from './tstest.classes.runtime.bun.js';
|
import { BunRuntimeAdapter } from './tstest.classes.runtime.bun.js';
|
||||||
import { DockerRuntimeAdapter } from './tstest.classes.runtime.docker.js';
|
import { DockerRuntimeAdapter } from './tstest.classes.runtime.docker.js';
|
||||||
|
|
||||||
|
// Test file directives
|
||||||
|
import {
|
||||||
|
parseDirectivesFromFile,
|
||||||
|
mergeDirectives,
|
||||||
|
directivesToRuntimeOptions,
|
||||||
|
hasDirectives,
|
||||||
|
} from './tstest.classes.testfile.directives.js';
|
||||||
|
|
||||||
|
// Before-script support
|
||||||
|
import { loadBeforeScripts, runBeforeScript, type IBeforeScripts } from './tstest.classes.beforescripts.js';
|
||||||
|
|
||||||
export class TsTest {
|
export class TsTest {
|
||||||
public testDir: TestDirectory;
|
public testDir: TestDirectory;
|
||||||
public executionMode: TestExecutionMode;
|
public executionMode: TestExecutionMode;
|
||||||
@@ -40,6 +51,8 @@ export class TsTest {
|
|||||||
public runtimeRegistry = new RuntimeAdapterRegistry();
|
public runtimeRegistry = new RuntimeAdapterRegistry();
|
||||||
public dockerAdapter: DockerRuntimeAdapter | null = null;
|
public dockerAdapter: DockerRuntimeAdapter | null = null;
|
||||||
|
|
||||||
|
private beforeScripts: IBeforeScripts | null = null;
|
||||||
|
|
||||||
constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}, tags: string[] = [], startFromFile: number | null = null, stopAtFile: number | null = null, timeoutSeconds: number | null = null) {
|
constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}, tags: string[] = [], startFromFile: number | null = null, stopAtFile: number | null = null, timeoutSeconds: number | null = null) {
|
||||||
this.executionMode = executionModeArg;
|
this.executionMode = executionModeArg;
|
||||||
this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg);
|
this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg);
|
||||||
@@ -89,7 +102,22 @@ export class TsTest {
|
|||||||
if (this.logger.options.logFile) {
|
if (this.logger.options.logFile) {
|
||||||
await this.movePreviousLogFiles();
|
await this.movePreviousLogFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load and execute test:before script (runs once per test run)
|
||||||
|
this.beforeScripts = loadBeforeScripts(this.testDir.cwd);
|
||||||
|
if (this.beforeScripts.beforeOnce) {
|
||||||
|
const success = await runBeforeScript(
|
||||||
|
this.smartshellInstance,
|
||||||
|
this.beforeScripts.beforeOnce,
|
||||||
|
'test:before',
|
||||||
|
this.logger,
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
this.logger.error('test:before script failed. Aborting test run.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const testGroups = await this.testDir.getTestFileGroups();
|
const testGroups = await this.testDir.getTestFileGroups();
|
||||||
const allFiles = [...testGroups.serial, ...Object.values(testGroups.parallelGroups).flat()];
|
const allFiles = [...testGroups.serial, ...Object.values(testGroups.parallelGroups).flat()];
|
||||||
|
|
||||||
@@ -256,18 +284,65 @@ export class TsTest {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse directives from the test file (e.g., // tstest:deno:allowAll)
|
||||||
|
let directives = await parseDirectivesFromFile(fileNameArg);
|
||||||
|
|
||||||
|
// Also check for directives in 00init.ts
|
||||||
|
const testDir = plugins.path.dirname(fileNameArg);
|
||||||
|
const initFile = plugins.path.join(testDir, '00init.ts');
|
||||||
|
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
|
||||||
|
if (initFileExists) {
|
||||||
|
const initDirectives = await parseDirectivesFromFile(initFile);
|
||||||
|
directives = mergeDirectives(initDirectives, directives);
|
||||||
|
}
|
||||||
|
|
||||||
// Execute tests for each runtime
|
// Execute tests for each runtime
|
||||||
if (adapters.length === 1) {
|
if (adapters.length === 1) {
|
||||||
// Single runtime - no sections needed
|
// Single runtime - no sections needed
|
||||||
const adapter = adapters[0];
|
const adapter = adapters[0];
|
||||||
const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles);
|
|
||||||
|
// Run test:before:testfile if defined
|
||||||
|
if (this.beforeScripts?.beforeTestfile) {
|
||||||
|
const success = await runBeforeScript(
|
||||||
|
this.smartshellInstance,
|
||||||
|
this.beforeScripts.beforeTestfile,
|
||||||
|
`test:before:testfile (${fileName})`,
|
||||||
|
this.logger,
|
||||||
|
{ TSTEST_FILE: fileNameArg, TSTEST_RUNTIME: adapter.id },
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
this.logger.error(`test:before:testfile failed for ${fileName}. Skipping test file.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = hasDirectives(directives) ? directivesToRuntimeOptions(directives, adapter.id) : undefined;
|
||||||
|
const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles, options);
|
||||||
tapCombinator.addTapParser(tapParser);
|
tapCombinator.addTapParser(tapParser);
|
||||||
} else {
|
} else {
|
||||||
// Multiple runtimes - use sections
|
// Multiple runtimes - use sections
|
||||||
for (let i = 0; i < adapters.length; i++) {
|
for (let i = 0; i < adapters.length; i++) {
|
||||||
const adapter = adapters[i];
|
const adapter = adapters[i];
|
||||||
this.logger.sectionStart(`Part ${i + 1}: ${adapter.displayName}`);
|
this.logger.sectionStart(`Part ${i + 1}: ${adapter.displayName}`);
|
||||||
const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles);
|
|
||||||
|
// Run test:before:testfile if defined (runs before each runtime)
|
||||||
|
if (this.beforeScripts?.beforeTestfile) {
|
||||||
|
const success = await runBeforeScript(
|
||||||
|
this.smartshellInstance,
|
||||||
|
this.beforeScripts.beforeTestfile,
|
||||||
|
`test:before:testfile (${fileName} on ${adapter.displayName})`,
|
||||||
|
this.logger,
|
||||||
|
{ TSTEST_FILE: fileNameArg, TSTEST_RUNTIME: adapter.id },
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
this.logger.error(`test:before:testfile failed for ${fileName} on ${adapter.displayName}. Skipping.`);
|
||||||
|
this.logger.sectionEnd();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = hasDirectives(directives) ? directivesToRuntimeOptions(directives, adapter.id) : undefined;
|
||||||
|
const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles, options);
|
||||||
tapCombinator.addTapParser(tapParser);
|
tapCombinator.addTapParser(tapParser);
|
||||||
this.logger.sectionEnd();
|
this.logger.sectionEnd();
|
||||||
}
|
}
|
||||||
@@ -288,6 +363,21 @@ export class TsTest {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run test:before:testfile if defined
|
||||||
|
if (this.beforeScripts?.beforeTestfile) {
|
||||||
|
const success = await runBeforeScript(
|
||||||
|
this.smartshellInstance,
|
||||||
|
this.beforeScripts.beforeTestfile,
|
||||||
|
`test:before:testfile (${fileNameArg} on Docker)`,
|
||||||
|
this.logger,
|
||||||
|
{ TSTEST_FILE: fileNameArg, TSTEST_RUNTIME: 'docker' },
|
||||||
|
);
|
||||||
|
if (!success) {
|
||||||
|
this.logger.error(`test:before:testfile failed for ${fileNameArg}. Skipping.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tapParser = await this.dockerAdapter.run(fileNameArg, fileIndex, totalFiles);
|
const tapParser = await this.dockerAdapter.run(fileNameArg, fileIndex, totalFiles);
|
||||||
tapCombinator.addTapParser(tapParser);
|
tapCombinator.addTapParser(tapParser);
|
||||||
@@ -454,24 +544,27 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|||||||
// Find free ports for HTTP and WebSocket
|
// Find free ports for HTTP and WebSocket
|
||||||
const { httpPort, wsPort } = await this.findFreePorts();
|
const { httpPort, wsPort } = await this.findFreePorts();
|
||||||
|
|
||||||
// lets create a server
|
// Use SmartServe with setHandler() to bypass global ControllerRegistry
|
||||||
const server = new plugins.typedserver.TypedServer({
|
const fileServer = new plugins.smartserve.FileServer({ root: tsbundleCacheDirPath });
|
||||||
cors: true,
|
const server = new plugins.smartserve.SmartServe({ port: httpPort });
|
||||||
port: httpPort,
|
server.setHandler(async (request: Request) => {
|
||||||
serveDir: tsbundleCacheDirPath,
|
const url = new URL(request.url);
|
||||||
});
|
if (url.pathname === '/test') {
|
||||||
server.addRoute('/test', 'GET', async () => {
|
return new Response(`
|
||||||
return new Response(`
|
<html>
|
||||||
<html>
|
<head>
|
||||||
<head>
|
<script>
|
||||||
<script>
|
globalThis.testdom = true;
|
||||||
globalThis.testdom = true;
|
globalThis.wsPort = ${wsPort};
|
||||||
globalThis.wsPort = ${wsPort};
|
</script>
|
||||||
</script>
|
</head>
|
||||||
</head>
|
<body></body>
|
||||||
<body></body>
|
</html>
|
||||||
</html>
|
`, { headers: { 'Content-Type': 'text/html' } });
|
||||||
`, { headers: { 'Content-Type': 'text/html' } });
|
}
|
||||||
|
const staticResponse = await fileServer.serve(request);
|
||||||
|
if (staticResponse) return staticResponse;
|
||||||
|
return new Response('Not Found', { status: 404 });
|
||||||
});
|
});
|
||||||
await server.start();
|
await server.start();
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,40 @@ export class TsTestLogger {
|
|||||||
this.log(this.format(`[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${message}`, 'dim'));
|
this.log(this.format(`[${'█'.repeat(filled)}${'░'.repeat(empty)}] ${message}`, 'dim'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Before-script lifecycle hooks
|
||||||
|
beforeScriptStart(label: string, command: string) {
|
||||||
|
if (this.options.json) {
|
||||||
|
this.logJson({ event: 'beforeScript', label, command });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.quiet) {
|
||||||
|
this.log(`Running ${label}...`);
|
||||||
|
} else {
|
||||||
|
this.log(this.format(`\n🔧 Running ${label}...`, 'cyan'));
|
||||||
|
this.log(this.format(` Command: ${command}`, 'dim'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeScriptEnd(label: string, success: boolean, durationMs: number) {
|
||||||
|
const durationStr = durationMs >= 1000 ? `${(durationMs / 1000).toFixed(1)}s` : `${durationMs}ms`;
|
||||||
|
|
||||||
|
if (this.options.json) {
|
||||||
|
this.logJson({ event: 'beforeScriptEnd', label, success, durationMs });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.quiet) {
|
||||||
|
this.log(success ? `${label} done (${durationStr})` : `${label} FAILED`);
|
||||||
|
} else {
|
||||||
|
if (success) {
|
||||||
|
this.log(this.format(` ✓ ${label} completed (${durationStr})`, 'green'));
|
||||||
|
} else {
|
||||||
|
this.log(this.format(` ✗ ${label} failed (${durationStr})`, 'red'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Test discovery
|
// Test discovery
|
||||||
testDiscovery(count: number, pattern: string, executionMode: string) {
|
testDiscovery(count: number, pattern: string, executionMode: string) {
|
||||||
if (this.options.json) {
|
if (this.options.json) {
|
||||||
|
|||||||
@@ -4,16 +4,10 @@ import * as path from 'path';
|
|||||||
|
|
||||||
export { fs, path };
|
export { fs, path };
|
||||||
|
|
||||||
// @apiglobal scope
|
|
||||||
import * as typedserver from '@api.global/typedserver';
|
|
||||||
|
|
||||||
export {
|
|
||||||
typedserver
|
|
||||||
}
|
|
||||||
|
|
||||||
// @push.rocks scope
|
// @push.rocks scope
|
||||||
import * as consolecolor from '@push.rocks/consolecolor';
|
import * as consolecolor from '@push.rocks/consolecolor';
|
||||||
import * as smartbrowser from '@push.rocks/smartbrowser';
|
import * as smartbrowser from '@push.rocks/smartbrowser';
|
||||||
|
import * as smartserve from '@push.rocks/smartserve';
|
||||||
import * as smartdelay from '@push.rocks/smartdelay';
|
import * as smartdelay from '@push.rocks/smartdelay';
|
||||||
import * as smartfile from '@push.rocks/smartfile';
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
import * as smartfs from '@push.rocks/smartfs';
|
import * as smartfs from '@push.rocks/smartfs';
|
||||||
@@ -28,6 +22,7 @@ import * as tapbundle from '../dist_ts_tapbundle/index.js';
|
|||||||
export {
|
export {
|
||||||
consolecolor,
|
consolecolor,
|
||||||
smartbrowser,
|
smartbrowser,
|
||||||
|
smartserve,
|
||||||
smartdelay,
|
smartdelay,
|
||||||
smartfile,
|
smartfile,
|
||||||
smartfs,
|
smartfs,
|
||||||
|
|||||||
@@ -83,16 +83,15 @@ class TapNodeTools {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* create and return a smarts3 instance
|
* create and return a smartstorage instance
|
||||||
*/
|
*/
|
||||||
public async createSmarts3() {
|
public async createSmartStorage() {
|
||||||
const smarts3Mod = await import('@push.rocks/smarts3');
|
const smartstorageMod = await import('@push.rocks/smartstorage');
|
||||||
const smarts3Instance = new smarts3Mod.Smarts3({
|
const smartstorageInstance = await smartstorageMod.SmartStorage.createAndStart({
|
||||||
server: { port: 3003 },
|
server: { port: 3003 },
|
||||||
storage: { cleanSlate: true },
|
storage: { cleanSlate: true },
|
||||||
});
|
});
|
||||||
await smarts3Instance.start();
|
return smartstorageInstance;
|
||||||
return smarts3Instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============
|
// ============
|
||||||
|
|||||||
@@ -207,12 +207,12 @@ Uses [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo).
|
|||||||
Create a local S3-compatible storage instance for testing.
|
Create a local S3-compatible storage instance for testing.
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const s3 = await tapNodeTools.createSmarts3();
|
const s3 = await tapNodeTools.createSmartStorage();
|
||||||
// ... run storage tests ...
|
// ... run storage tests ...
|
||||||
await s3.stop();
|
await s3.stop();
|
||||||
```
|
```
|
||||||
|
|
||||||
Default config: port 3003, clean slate enabled. Uses [@push.rocks/smarts3](https://code.foss.global/push.rocks/smarts3).
|
Default config: port 3003, clean slate enabled. Uses [@push.rocks/smartstorage](https://code.foss.global/push.rocks/smartstorage).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ test/mytest.all.ts ❌ Will fail in Deno/Bun/Chromium
|
|||||||
- [@push.rocks/smartshell](https://code.foss.global/push.rocks/smartshell) — Shell command execution
|
- [@push.rocks/smartshell](https://code.foss.global/push.rocks/smartshell) — Shell command execution
|
||||||
- [@push.rocks/smartcrypto](https://code.foss.global/push.rocks/smartcrypto) — Certificate generation
|
- [@push.rocks/smartcrypto](https://code.foss.global/push.rocks/smartcrypto) — Certificate generation
|
||||||
- [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo) — MongoDB testing
|
- [@push.rocks/smartmongo](https://code.foss.global/push.rocks/smartmongo) — MongoDB testing
|
||||||
- [@push.rocks/smarts3](https://code.foss.global/push.rocks/smarts3) — S3 storage testing
|
- [@push.rocks/smartstorage](https://code.foss.global/push.rocks/smartstorage) — S3 storage testing
|
||||||
- [@push.rocks/smartfile](https://code.foss.global/push.rocks/smartfile) — File operations
|
- [@push.rocks/smartfile](https://code.foss.global/push.rocks/smartfile) — File operations
|
||||||
- [@push.rocks/smartrequest](https://code.foss.global/push.rocks/smartrequest) — HTTP requests
|
- [@push.rocks/smartrequest](https://code.foss.global/push.rocks/smartrequest) — HTTP requests
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user