@git.zone/tstest 3.1.8 → 3.3.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/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/tstest.classes.migration.js +4 -4
- package/dist_ts/tstest.classes.runtime.bun.js +5 -5
- package/dist_ts/tstest.classes.runtime.chromium.js +12 -10
- package/dist_ts/tstest.classes.runtime.deno.d.ts +4 -0
- package/dist_ts/tstest.classes.runtime.deno.js +22 -27
- package/dist_ts/tstest.classes.runtime.docker.js +2 -2
- package/dist_ts/tstest.classes.runtime.node.js +5 -5
- package/dist_ts/tstest.classes.tap.parser.js +6 -7
- package/dist_ts/tstest.classes.testdirectory.d.ts +1 -1
- package/dist_ts/tstest.classes.testdirectory.js +11 -12
- package/dist_ts/tstest.classes.testfile.directives.d.ts +38 -0
- package/dist_ts/tstest.classes.testfile.directives.js +191 -0
- package/dist_ts/tstest.classes.tstest.js +48 -33
- package/dist_ts/tstest.plugins.d.ts +6 -3
- package/dist_ts/tstest.plugins.js +7 -4
- package/dist_ts_tapbundle_serverside/classes.tapnodetools.d.ts +30 -0
- package/dist_ts_tapbundle_serverside/classes.tapnodetools.js +100 -3
- package/dist_ts_tapbundle_serverside/classes.testfileprovider.js +3 -3
- package/dist_ts_tapbundle_serverside/plugins.d.ts +4 -1
- package/dist_ts_tapbundle_serverside/plugins.js +5 -2
- package/npmextra.json +1 -1
- package/package.json +17 -16
- package/readme.hints.md +1 -1
- package/readme.md +329 -860
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/tstest.classes.migration.ts +3 -6
- package/ts/tstest.classes.runtime.bun.ts +4 -4
- package/ts/tstest.classes.runtime.chromium.ts +8 -12
- package/ts/tstest.classes.runtime.deno.ts +22 -26
- package/ts/tstest.classes.runtime.docker.ts +1 -1
- package/ts/tstest.classes.runtime.node.ts +4 -4
- package/ts/tstest.classes.tap.parser.ts +5 -7
- package/ts/tstest.classes.testdirectory.ts +19 -20
- package/ts/tstest.classes.testfile.directives.ts +226 -0
- package/ts/tstest.classes.tstest.ts +60 -43
- package/ts/tstest.plugins.ts +8 -3
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@git.zone/tstest',
|
|
6
|
-
version: '3.
|
|
7
|
-
description: '
|
|
6
|
+
version: '3.3.0',
|
|
7
|
+
description: 'A powerful, modern test runner for TypeScript with multi-runtime support (Node.js, Deno, Bun, Chromium) and a batteries-included test framework.'
|
|
8
8
|
}
|
|
@@ -111,10 +111,7 @@ export class Migration {
|
|
|
111
111
|
* Find all legacy test files in the base directory
|
|
112
112
|
*/
|
|
113
113
|
async findLegacyFiles(): Promise<string[]> {
|
|
114
|
-
const files =
|
|
115
|
-
this.options.baseDir,
|
|
116
|
-
this.options.pattern
|
|
117
|
-
);
|
|
114
|
+
const files = plugins.fs.globSync(this.options.pattern, { cwd: this.options.baseDir }) as string[];
|
|
118
115
|
|
|
119
116
|
const legacyFiles: string[] = [];
|
|
120
117
|
|
|
@@ -154,7 +151,7 @@ export class Migration {
|
|
|
154
151
|
const newPath = plugins.path.join(dirName, newFileName);
|
|
155
152
|
|
|
156
153
|
// Check if target file already exists
|
|
157
|
-
if (await plugins.
|
|
154
|
+
if (await plugins.smartfsInstance.file(newPath).exists()) {
|
|
158
155
|
return {
|
|
159
156
|
oldPath: filePath,
|
|
160
157
|
newPath,
|
|
@@ -206,7 +203,7 @@ export class Migration {
|
|
|
206
203
|
private async isGitRepository(dir: string): Promise<boolean> {
|
|
207
204
|
try {
|
|
208
205
|
const gitDir = plugins.path.join(dir, '.git');
|
|
209
|
-
return await plugins.
|
|
206
|
+
return await plugins.smartfsInstance.directory(gitDir).exists();
|
|
210
207
|
} catch {
|
|
211
208
|
return false;
|
|
212
209
|
}
|
|
@@ -121,7 +121,7 @@ export class BunRuntimeAdapter extends RuntimeAdapter {
|
|
|
121
121
|
// Check for 00init.ts file in test directory
|
|
122
122
|
const testDir = plugins.path.dirname(testFile);
|
|
123
123
|
const initFile = plugins.path.join(testDir, '00init.ts');
|
|
124
|
-
const initFileExists = await plugins.
|
|
124
|
+
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
|
|
125
125
|
|
|
126
126
|
let runCommand = fullCommand;
|
|
127
127
|
let loaderPath: string | null = null;
|
|
@@ -135,7 +135,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
|
|
|
135
135
|
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|
136
136
|
`;
|
|
137
137
|
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
|
138
|
-
await plugins.
|
|
138
|
+
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
|
|
139
139
|
|
|
140
140
|
// Rebuild command with loader file
|
|
141
141
|
const loaderCommand = this.createCommand(loaderPath, mergedOptions);
|
|
@@ -148,8 +148,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|
|
148
148
|
if (loaderPath) {
|
|
149
149
|
const cleanup = () => {
|
|
150
150
|
try {
|
|
151
|
-
if (plugins.
|
|
152
|
-
plugins.
|
|
151
|
+
if (plugins.fs.existsSync(loaderPath)) {
|
|
152
|
+
plugins.fs.rmSync(loaderPath, { force: true });
|
|
153
153
|
}
|
|
154
154
|
} catch (e) {
|
|
155
155
|
// Ignore cleanup errors
|
|
@@ -107,7 +107,8 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
|
|
|
107
107
|
const bundleFilePath = plugins.path.join(tsbundleCacheDirPath, bundleFileName);
|
|
108
108
|
|
|
109
109
|
// lets bundle the test
|
|
110
|
-
await plugins.
|
|
110
|
+
try { await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().delete(); } catch (e) { /* may not exist */ }
|
|
111
|
+
await plugins.smartfsInstance.directory(tsbundleCacheDirPath).recursive().create();
|
|
111
112
|
await this.tsbundleInstance.build(process.cwd(), testFile, bundleFilePath, {
|
|
112
113
|
bundler: 'esbuild',
|
|
113
114
|
});
|
|
@@ -116,15 +117,13 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
|
|
|
116
117
|
const { httpPort, wsPort } = await this.findFreePorts();
|
|
117
118
|
|
|
118
119
|
// lets create a server
|
|
119
|
-
const server = new plugins.typedserver.
|
|
120
|
+
const server = new plugins.typedserver.TypedServer({
|
|
120
121
|
cors: true,
|
|
121
122
|
port: httpPort,
|
|
123
|
+
serveDir: tsbundleCacheDirPath,
|
|
122
124
|
});
|
|
123
|
-
server.addRoute(
|
|
124
|
-
|
|
125
|
-
new plugins.typedserver.servertools.Handler('GET', async (_req, res) => {
|
|
126
|
-
res.type('.html');
|
|
127
|
-
res.write(`
|
|
125
|
+
server.addRoute('/test', 'GET', async () => {
|
|
126
|
+
return new Response(`
|
|
128
127
|
<html>
|
|
129
128
|
<head>
|
|
130
129
|
<script>
|
|
@@ -134,11 +133,8 @@ export class ChromiumRuntimeAdapter extends RuntimeAdapter {
|
|
|
134
133
|
</head>
|
|
135
134
|
<body></body>
|
|
136
135
|
</html>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
})
|
|
140
|
-
);
|
|
141
|
-
server.addRoute('/*splat', new plugins.typedserver.servertools.HandlerStatic(tsbundleCacheDirPath));
|
|
136
|
+
`, { headers: { 'Content-Type': 'text/html' } });
|
|
137
|
+
});
|
|
142
138
|
await server.start();
|
|
143
139
|
|
|
144
140
|
// lets handle realtime comms
|
|
@@ -10,6 +10,20 @@ import { TapParser } from './tstest.classes.tap.parser.js';
|
|
|
10
10
|
import { TsTestLogger } from './tstest.logging.js';
|
|
11
11
|
import type { Runtime } from './tstest.classes.runtime.parser.js';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Default Deno permissions used when no directives override them.
|
|
15
|
+
*/
|
|
16
|
+
export const DENO_DEFAULT_PERMISSIONS = [
|
|
17
|
+
'--allow-read',
|
|
18
|
+
'--allow-env',
|
|
19
|
+
'--allow-net',
|
|
20
|
+
'--allow-write',
|
|
21
|
+
'--allow-sys',
|
|
22
|
+
'--allow-import',
|
|
23
|
+
'--node-modules-dir',
|
|
24
|
+
'--sloppy-imports',
|
|
25
|
+
];
|
|
26
|
+
|
|
13
27
|
/**
|
|
14
28
|
* Deno runtime adapter
|
|
15
29
|
* Executes tests using the Deno runtime
|
|
@@ -36,25 +50,16 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
|
|
|
36
50
|
const denoJsonPath = plugins.path.join(process.cwd(), 'deno.json');
|
|
37
51
|
const denoJsoncPath = plugins.path.join(process.cwd(), 'deno.jsonc');
|
|
38
52
|
|
|
39
|
-
if (plugins.
|
|
53
|
+
if (plugins.fs.existsSync(denoJsonPath)) {
|
|
40
54
|
configPath = denoJsonPath;
|
|
41
|
-
} else if (plugins.
|
|
55
|
+
} else if (plugins.fs.existsSync(denoJsoncPath)) {
|
|
42
56
|
configPath = denoJsoncPath;
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
return {
|
|
46
60
|
...super.getDefaultOptions(),
|
|
47
61
|
configPath,
|
|
48
|
-
permissions: [
|
|
49
|
-
'--allow-read',
|
|
50
|
-
'--allow-env',
|
|
51
|
-
'--allow-net',
|
|
52
|
-
'--allow-write',
|
|
53
|
-
'--allow-sys', // Allow system info access
|
|
54
|
-
'--allow-import', // Allow npm/node imports
|
|
55
|
-
'--node-modules-dir', // Enable Node.js compatibility mode
|
|
56
|
-
'--sloppy-imports', // Allow .js imports to resolve to .ts files
|
|
57
|
-
],
|
|
62
|
+
permissions: [...DENO_DEFAULT_PERMISSIONS],
|
|
58
63
|
};
|
|
59
64
|
}
|
|
60
65
|
|
|
@@ -102,16 +107,7 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
|
|
|
102
107
|
const args: string[] = ['run'];
|
|
103
108
|
|
|
104
109
|
// Add permissions
|
|
105
|
-
const permissions = mergedOptions.permissions || [
|
|
106
|
-
'--allow-read',
|
|
107
|
-
'--allow-env',
|
|
108
|
-
'--allow-net',
|
|
109
|
-
'--allow-write',
|
|
110
|
-
'--allow-sys',
|
|
111
|
-
'--allow-import',
|
|
112
|
-
'--node-modules-dir',
|
|
113
|
-
'--sloppy-imports',
|
|
114
|
-
];
|
|
110
|
+
const permissions = mergedOptions.permissions || [...DENO_DEFAULT_PERMISSIONS];
|
|
115
111
|
args.push(...permissions);
|
|
116
112
|
|
|
117
113
|
// Add config file if specified
|
|
@@ -173,7 +169,7 @@ export class DenoRuntimeAdapter extends RuntimeAdapter {
|
|
|
173
169
|
// Check for 00init.ts file in test directory
|
|
174
170
|
const testDir = plugins.path.dirname(testFile);
|
|
175
171
|
const initFile = plugins.path.join(testDir, '00init.ts');
|
|
176
|
-
const initFileExists = await plugins.
|
|
172
|
+
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
|
|
177
173
|
|
|
178
174
|
let runCommand = fullCommand;
|
|
179
175
|
let loaderPath: string | null = null;
|
|
@@ -187,7 +183,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
|
|
|
187
183
|
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|
188
184
|
`;
|
|
189
185
|
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
|
190
|
-
await plugins.
|
|
186
|
+
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
|
|
191
187
|
|
|
192
188
|
// Rebuild command with loader file
|
|
193
189
|
const loaderCommand = this.createCommand(loaderPath, mergedOptions);
|
|
@@ -200,8 +196,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|
|
200
196
|
if (loaderPath) {
|
|
201
197
|
const cleanup = () => {
|
|
202
198
|
try {
|
|
203
|
-
if (plugins.
|
|
204
|
-
plugins.
|
|
199
|
+
if (plugins.fs.existsSync(loaderPath)) {
|
|
200
|
+
plugins.fs.rmSync(loaderPath, { force: true });
|
|
205
201
|
}
|
|
206
202
|
} catch (e) {
|
|
207
203
|
// Ignore cleanup errors
|
|
@@ -102,7 +102,7 @@ export class DockerRuntimeAdapter extends RuntimeAdapter {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
// Check if Dockerfile exists
|
|
105
|
-
if (!await plugins.
|
|
105
|
+
if (!await plugins.smartfsInstance.file(dockerfilePath).exists()) {
|
|
106
106
|
throw new Error(
|
|
107
107
|
`Dockerfile not found: ${dockerfilePath}\n` +
|
|
108
108
|
`Expected Dockerfile for Docker test variant.`
|
|
@@ -123,7 +123,7 @@ export class NodeRuntimeAdapter extends RuntimeAdapter {
|
|
|
123
123
|
// Check for 00init.ts file in test directory
|
|
124
124
|
const testDir = plugins.path.dirname(testFile);
|
|
125
125
|
const initFile = plugins.path.join(testDir, '00init.ts');
|
|
126
|
-
const initFileExists = await plugins.
|
|
126
|
+
const initFileExists = await plugins.smartfsInstance.file(initFile).exists();
|
|
127
127
|
|
|
128
128
|
// Determine which file to run
|
|
129
129
|
let fileToRun = testFile;
|
|
@@ -138,7 +138,7 @@ import '${absoluteInitFile.replace(/\\/g, '/')}';
|
|
|
138
138
|
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|
139
139
|
`;
|
|
140
140
|
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
|
141
|
-
await plugins.
|
|
141
|
+
await plugins.smartfsInstance.file(loaderPath).write(loaderContent);
|
|
142
142
|
fileToRun = loaderPath;
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -150,8 +150,8 @@ import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|
|
150
150
|
if (loaderPath) {
|
|
151
151
|
const cleanup = () => {
|
|
152
152
|
try {
|
|
153
|
-
if (plugins.
|
|
154
|
-
plugins.
|
|
153
|
+
if (plugins.fs.existsSync(loaderPath)) {
|
|
154
|
+
plugins.fs.rmSync(loaderPath, { force: true });
|
|
155
155
|
}
|
|
156
156
|
} catch (e) {
|
|
157
157
|
// Ignore cleanup errors
|
|
@@ -497,12 +497,10 @@ export class TapParser {
|
|
|
497
497
|
*/
|
|
498
498
|
private async handleSnapshot(snapshotData: { path: string; content: string; action: string }) {
|
|
499
499
|
try {
|
|
500
|
-
const smartfile = await import('@push.rocks/smartfile');
|
|
501
|
-
|
|
502
500
|
if (snapshotData.action === 'compare') {
|
|
503
501
|
// Try to read existing snapshot
|
|
504
502
|
try {
|
|
505
|
-
const existingSnapshot = await
|
|
503
|
+
const existingSnapshot = await plugins.smartfsInstance.file(snapshotData.path).encoding('utf8').read() as string;
|
|
506
504
|
if (existingSnapshot !== snapshotData.content) {
|
|
507
505
|
// Snapshot mismatch
|
|
508
506
|
if (this.logger) {
|
|
@@ -520,8 +518,8 @@ export class TapParser {
|
|
|
520
518
|
if (error.code === 'ENOENT') {
|
|
521
519
|
// Snapshot doesn't exist, create it
|
|
522
520
|
const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
|
|
523
|
-
await
|
|
524
|
-
await
|
|
521
|
+
await plugins.smartfsInstance.directory(dirPath).recursive().create();
|
|
522
|
+
await plugins.smartfsInstance.file(snapshotData.path).write(snapshotData.content);
|
|
525
523
|
if (this.logger) {
|
|
526
524
|
this.logger.testConsoleOutput(`Snapshot created: ${snapshotData.path}`);
|
|
527
525
|
}
|
|
@@ -532,8 +530,8 @@ export class TapParser {
|
|
|
532
530
|
} else if (snapshotData.action === 'update') {
|
|
533
531
|
// Update snapshot
|
|
534
532
|
const dirPath = snapshotData.path.substring(0, snapshotData.path.lastIndexOf('/'));
|
|
535
|
-
await
|
|
536
|
-
await
|
|
533
|
+
await plugins.smartfsInstance.directory(dirPath).recursive().create();
|
|
534
|
+
await plugins.smartfsInstance.file(snapshotData.path).write(snapshotData.content);
|
|
537
535
|
if (this.logger) {
|
|
538
536
|
this.logger.testConsoleOutput(`Snapshot updated: ${snapshotData.path}`);
|
|
539
537
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import * as plugins from './tstest.plugins.js';
|
|
2
2
|
import * as paths from './tstest.paths.js';
|
|
3
|
-
import { SmartFile } from '@push.rocks/smartfile';
|
|
3
|
+
import { type SmartFile, SmartFileFactory } from '@push.rocks/smartfile';
|
|
4
4
|
import { TestExecutionMode } from './index.js';
|
|
5
5
|
|
|
6
|
+
const smartFileFactory = SmartFileFactory.nodeFs();
|
|
7
|
+
|
|
6
8
|
// tap related stuff
|
|
7
9
|
import { TapCombinator } from './tstest.classes.tap.combinator.js';
|
|
8
10
|
import { TapParser } from './tstest.classes.tap.parser.js';
|
|
@@ -45,28 +47,28 @@ export class TestDirectory {
|
|
|
45
47
|
switch (this.executionMode) {
|
|
46
48
|
case TestExecutionMode.FILE:
|
|
47
49
|
// Single file mode
|
|
48
|
-
const filePath = plugins.path.isAbsolute(this.testPath)
|
|
49
|
-
? this.testPath
|
|
50
|
+
const filePath = plugins.path.isAbsolute(this.testPath)
|
|
51
|
+
? this.testPath
|
|
50
52
|
: plugins.path.join(this.cwd, this.testPath);
|
|
51
|
-
|
|
52
|
-
if (await plugins.
|
|
53
|
-
this.testfileArray = [await
|
|
53
|
+
|
|
54
|
+
if (await plugins.smartfsInstance.file(filePath).exists()) {
|
|
55
|
+
this.testfileArray = [await smartFileFactory.fromFilePath(filePath)];
|
|
54
56
|
} else {
|
|
55
57
|
throw new Error(`Test file not found: ${filePath}`);
|
|
56
58
|
}
|
|
57
59
|
break;
|
|
58
60
|
|
|
59
61
|
case TestExecutionMode.GLOB:
|
|
60
|
-
// Glob pattern mode - use
|
|
62
|
+
// Glob pattern mode - use Node.js fs.globSync for full glob support
|
|
61
63
|
const globPattern = this.testPath;
|
|
62
|
-
const matchedFiles =
|
|
63
|
-
|
|
64
|
+
const matchedFiles = plugins.fs.globSync(globPattern, { cwd: this.cwd });
|
|
65
|
+
|
|
64
66
|
this.testfileArray = await Promise.all(
|
|
65
|
-
matchedFiles.map(async (filePath) => {
|
|
66
|
-
const absolutePath = plugins.path.isAbsolute(filePath)
|
|
67
|
-
? filePath
|
|
67
|
+
matchedFiles.map(async (filePath: string) => {
|
|
68
|
+
const absolutePath = plugins.path.isAbsolute(filePath)
|
|
69
|
+
? filePath
|
|
68
70
|
: plugins.path.join(this.cwd, filePath);
|
|
69
|
-
return await
|
|
71
|
+
return await smartFileFactory.fromFilePath(absolutePath);
|
|
70
72
|
})
|
|
71
73
|
);
|
|
72
74
|
break;
|
|
@@ -79,19 +81,16 @@ export class TestDirectory {
|
|
|
79
81
|
const tsPattern = '**/test*.ts';
|
|
80
82
|
const dockerPattern = '**/*.docker.sh';
|
|
81
83
|
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
]);
|
|
86
|
-
|
|
87
|
-
const allTestFiles = [...tsFiles, ...dockerFiles];
|
|
84
|
+
const tsFiles = plugins.fs.globSync(tsPattern, { cwd: dirPath });
|
|
85
|
+
const dockerFiles = plugins.fs.globSync(dockerPattern, { cwd: dirPath });
|
|
86
|
+
const allTestFiles = [...tsFiles, ...dockerFiles] as string[];
|
|
88
87
|
|
|
89
88
|
this.testfileArray = await Promise.all(
|
|
90
89
|
allTestFiles.map(async (filePath) => {
|
|
91
90
|
const absolutePath = plugins.path.isAbsolute(filePath)
|
|
92
91
|
? filePath
|
|
93
92
|
: plugins.path.join(dirPath, filePath);
|
|
94
|
-
return await
|
|
93
|
+
return await smartFileFactory.fromFilePath(absolutePath);
|
|
95
94
|
})
|
|
96
95
|
);
|
|
97
96
|
break;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import * as plugins from './tstest.plugins.js';
|
|
2
|
+
import type { DenoOptions, RuntimeOptions } from './tstest.classes.runtime.adapter.js';
|
|
3
|
+
import type { Runtime } from './tstest.classes.runtime.parser.js';
|
|
4
|
+
import { DENO_DEFAULT_PERMISSIONS } from './tstest.classes.runtime.deno.js';
|
|
5
|
+
|
|
6
|
+
type DirectiveScope = Runtime | 'global';
|
|
7
|
+
|
|
8
|
+
export interface ITestFileDirective {
|
|
9
|
+
scope: DirectiveScope;
|
|
10
|
+
key: string;
|
|
11
|
+
value?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface IParsedDirectives {
|
|
15
|
+
deno: ITestFileDirective[];
|
|
16
|
+
node: ITestFileDirective[];
|
|
17
|
+
bun: ITestFileDirective[];
|
|
18
|
+
chromium: ITestFileDirective[];
|
|
19
|
+
global: ITestFileDirective[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const VALID_SCOPES = new Set<string>(['deno', 'node', 'bun', 'chromium']);
|
|
23
|
+
|
|
24
|
+
const DENO_PERMISSION_MAP: Record<string, string> = {
|
|
25
|
+
allowAll: '--allow-all',
|
|
26
|
+
allowRun: '--allow-run',
|
|
27
|
+
allowFfi: '--allow-ffi',
|
|
28
|
+
allowHrtime: '--allow-hrtime',
|
|
29
|
+
allowRead: '--allow-read',
|
|
30
|
+
allowWrite: '--allow-write',
|
|
31
|
+
allowNet: '--allow-net',
|
|
32
|
+
allowEnv: '--allow-env',
|
|
33
|
+
allowSys: '--allow-sys',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function createEmptyDirectives(): IParsedDirectives {
|
|
37
|
+
return { deno: [], node: [], bun: [], chromium: [], global: [] };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse tstest directives from file content.
|
|
42
|
+
* Scans comments at the top of the file (before any code).
|
|
43
|
+
*/
|
|
44
|
+
export function parseDirectivesFromContent(content: string): IParsedDirectives {
|
|
45
|
+
const result = createEmptyDirectives();
|
|
46
|
+
const lines = content.split('\n');
|
|
47
|
+
const maxLines = Math.min(lines.length, 30);
|
|
48
|
+
|
|
49
|
+
for (let i = 0; i < maxLines; i++) {
|
|
50
|
+
const line = lines[i].trim();
|
|
51
|
+
|
|
52
|
+
// Skip empty lines
|
|
53
|
+
if (line === '') continue;
|
|
54
|
+
|
|
55
|
+
// Stop at first non-comment line
|
|
56
|
+
if (!line.startsWith('//')) break;
|
|
57
|
+
|
|
58
|
+
// Match tstest directive: // tstest:<rest>
|
|
59
|
+
const match = line.match(/^\/\/\s*tstest:(.+)$/);
|
|
60
|
+
if (!match) continue;
|
|
61
|
+
|
|
62
|
+
const parts = match[1].split(':');
|
|
63
|
+
if (parts.length < 2) {
|
|
64
|
+
console.warn(`Warning: malformed tstest directive: "${line}"`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const scopeStr = parts[0].trim();
|
|
69
|
+
const key = parts[1].trim();
|
|
70
|
+
const value = parts.length > 2 ? parts.slice(2).join(':').trim() : undefined;
|
|
71
|
+
|
|
72
|
+
// Handle global directives (env, timeout)
|
|
73
|
+
if (scopeStr === 'env' || scopeStr === 'timeout') {
|
|
74
|
+
result.global.push({
|
|
75
|
+
scope: 'global',
|
|
76
|
+
key: scopeStr,
|
|
77
|
+
value: key + (value !== undefined ? ':' + value : ''),
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!VALID_SCOPES.has(scopeStr)) {
|
|
83
|
+
console.warn(`Warning: unknown tstest directive scope "${scopeStr}" in: "${line}"`);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const scope = scopeStr as Runtime;
|
|
88
|
+
result[scope].push({ scope, key, value });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Parse directives from a test file on disk.
|
|
96
|
+
*/
|
|
97
|
+
export async function parseDirectivesFromFile(filePath: string): Promise<IParsedDirectives> {
|
|
98
|
+
try {
|
|
99
|
+
const content = plugins.fs.readFileSync(filePath, 'utf8');
|
|
100
|
+
return parseDirectivesFromContent(content);
|
|
101
|
+
} catch {
|
|
102
|
+
return createEmptyDirectives();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Merge directives from 00init.ts and the test file.
|
|
108
|
+
* Test file directives are appended (take effect after init directives).
|
|
109
|
+
*/
|
|
110
|
+
export function mergeDirectives(init: IParsedDirectives, testFile: IParsedDirectives): IParsedDirectives {
|
|
111
|
+
return {
|
|
112
|
+
deno: [...init.deno, ...testFile.deno],
|
|
113
|
+
node: [...init.node, ...testFile.node],
|
|
114
|
+
bun: [...init.bun, ...testFile.bun],
|
|
115
|
+
chromium: [...init.chromium, ...testFile.chromium],
|
|
116
|
+
global: [...init.global, ...testFile.global],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if any directives exist for any scope.
|
|
122
|
+
*/
|
|
123
|
+
export function hasDirectives(directives: IParsedDirectives): boolean {
|
|
124
|
+
return (
|
|
125
|
+
directives.deno.length > 0 ||
|
|
126
|
+
directives.node.length > 0 ||
|
|
127
|
+
directives.bun.length > 0 ||
|
|
128
|
+
directives.chromium.length > 0 ||
|
|
129
|
+
directives.global.length > 0
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Convert parsed directives into DenoOptions.
|
|
135
|
+
*/
|
|
136
|
+
function directivesToDenoOptions(directives: IParsedDirectives): DenoOptions | undefined {
|
|
137
|
+
const denoDirectives = directives.deno;
|
|
138
|
+
if (denoDirectives.length === 0 && directives.global.length === 0) return undefined;
|
|
139
|
+
|
|
140
|
+
const options: DenoOptions = {};
|
|
141
|
+
const extraPermissions: string[] = [];
|
|
142
|
+
const extraArgs: string[] = [];
|
|
143
|
+
const env: Record<string, string> = {};
|
|
144
|
+
let useAllowAll = false;
|
|
145
|
+
|
|
146
|
+
for (const d of denoDirectives) {
|
|
147
|
+
if (d.key === 'allowAll') {
|
|
148
|
+
useAllowAll = true;
|
|
149
|
+
} else if (DENO_PERMISSION_MAP[d.key]) {
|
|
150
|
+
extraPermissions.push(DENO_PERMISSION_MAP[d.key]);
|
|
151
|
+
} else if (d.key === 'flag' && d.value) {
|
|
152
|
+
extraArgs.push(d.value);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Process global directives
|
|
157
|
+
for (const d of directives.global) {
|
|
158
|
+
if (d.key === 'env' && d.value) {
|
|
159
|
+
const eqIndex = d.value.indexOf('=');
|
|
160
|
+
if (eqIndex > 0) {
|
|
161
|
+
env[d.value.substring(0, eqIndex)] = d.value.substring(eqIndex + 1);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (useAllowAll) {
|
|
167
|
+
// --allow-all replaces individual permissions, but keep compatibility flags
|
|
168
|
+
options.permissions = ['--allow-all', '--node-modules-dir', '--sloppy-imports'];
|
|
169
|
+
} else if (extraPermissions.length > 0) {
|
|
170
|
+
// Start with defaults and add extra permissions (deduplicated)
|
|
171
|
+
const allPermissions = [...DENO_DEFAULT_PERMISSIONS];
|
|
172
|
+
for (const p of extraPermissions) {
|
|
173
|
+
if (!allPermissions.includes(p)) {
|
|
174
|
+
allPermissions.push(p);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
options.permissions = allPermissions;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (extraArgs.length > 0) options.extraArgs = extraArgs;
|
|
181
|
+
if (Object.keys(env).length > 0) options.env = env;
|
|
182
|
+
|
|
183
|
+
// Return undefined if nothing was set
|
|
184
|
+
if (!options.permissions && !options.extraArgs && !options.env) return undefined;
|
|
185
|
+
return options;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Convert parsed directives into RuntimeOptions for Node/Bun (flag directives only).
|
|
190
|
+
*/
|
|
191
|
+
function directivesToGenericOptions(directives: ITestFileDirective[], globalDirectives: ITestFileDirective[]): RuntimeOptions | undefined {
|
|
192
|
+
const extraArgs: string[] = [];
|
|
193
|
+
const env: Record<string, string> = {};
|
|
194
|
+
|
|
195
|
+
for (const d of directives) {
|
|
196
|
+
if (d.key === 'flag' && d.value) {
|
|
197
|
+
extraArgs.push(d.value);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const d of globalDirectives) {
|
|
202
|
+
if (d.key === 'env' && d.value) {
|
|
203
|
+
const eqIndex = d.value.indexOf('=');
|
|
204
|
+
if (eqIndex > 0) {
|
|
205
|
+
env[d.value.substring(0, eqIndex)] = d.value.substring(eqIndex + 1);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (extraArgs.length === 0 && Object.keys(env).length === 0) return undefined;
|
|
211
|
+
|
|
212
|
+
const options: RuntimeOptions = {};
|
|
213
|
+
if (extraArgs.length > 0) options.extraArgs = extraArgs;
|
|
214
|
+
if (Object.keys(env).length > 0) options.env = env;
|
|
215
|
+
return options;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Convert parsed directives into RuntimeOptions for a specific runtime.
|
|
220
|
+
*/
|
|
221
|
+
export function directivesToRuntimeOptions(directives: IParsedDirectives, runtime: Runtime): RuntimeOptions | undefined {
|
|
222
|
+
if (runtime === 'deno') {
|
|
223
|
+
return directivesToDenoOptions(directives);
|
|
224
|
+
}
|
|
225
|
+
return directivesToGenericOptions(directives[runtime] || [], directives.global);
|
|
226
|
+
}
|