@git.zone/tstest 2.3.8 → 2.4.1
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 +1 -1
- package/dist_ts/tstest.classes.migration.d.ts +104 -0
- package/dist_ts/tstest.classes.migration.js +200 -0
- package/dist_ts/tstest.classes.runtime.adapter.d.ts +172 -0
- package/dist_ts/tstest.classes.runtime.adapter.js +85 -0
- package/dist_ts/tstest.classes.runtime.bun.d.ts +30 -0
- package/dist_ts/tstest.classes.runtime.bun.js +184 -0
- package/dist_ts/tstest.classes.runtime.chromium.d.ts +36 -0
- package/dist_ts/tstest.classes.runtime.chromium.js +256 -0
- package/dist_ts/tstest.classes.runtime.deno.d.ts +34 -0
- package/dist_ts/tstest.classes.runtime.deno.js +217 -0
- package/dist_ts/tstest.classes.runtime.node.d.ts +30 -0
- package/dist_ts/tstest.classes.runtime.node.js +188 -0
- package/dist_ts/tstest.classes.runtime.parser.d.ts +37 -0
- package/dist_ts/tstest.classes.runtime.parser.js +159 -0
- package/dist_ts/tstest.classes.tstest.d.ts +2 -0
- package/dist_ts/tstest.classes.tstest.js +53 -22
- package/package.json +1 -1
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/tstest.classes.migration.ts +316 -0
- package/ts/tstest.classes.runtime.adapter.ts +245 -0
- package/ts/tstest.classes.runtime.bun.ts +219 -0
- package/ts/tstest.classes.runtime.chromium.ts +293 -0
- package/ts/tstest.classes.runtime.deno.ts +256 -0
- package/ts/tstest.classes.runtime.node.ts +222 -0
- package/ts/tstest.classes.runtime.parser.ts +211 -0
- package/ts/tstest.classes.tstest.ts +67 -22
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import * as plugins from './tstest.plugins.js';
|
|
2
|
+
import type { Runtime } from './tstest.classes.runtime.parser.js';
|
|
3
|
+
import { TapParser } from './tstest.classes.tap.parser.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Runtime-specific configuration options
|
|
7
|
+
*/
|
|
8
|
+
export interface RuntimeOptions {
|
|
9
|
+
/**
|
|
10
|
+
* Environment variables to pass to the runtime
|
|
11
|
+
*/
|
|
12
|
+
env?: Record<string, string>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Additional command-line arguments
|
|
16
|
+
*/
|
|
17
|
+
extraArgs?: string[];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Working directory for test execution
|
|
21
|
+
*/
|
|
22
|
+
cwd?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Timeout in milliseconds (0 = no timeout)
|
|
26
|
+
*/
|
|
27
|
+
timeout?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Deno-specific configuration options
|
|
32
|
+
*/
|
|
33
|
+
export interface DenoOptions extends RuntimeOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Permissions to grant to Deno
|
|
36
|
+
* Default: ['--allow-read', '--allow-env']
|
|
37
|
+
*/
|
|
38
|
+
permissions?: string[];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Path to deno.json config file
|
|
42
|
+
*/
|
|
43
|
+
configPath?: string;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Path to import map file
|
|
47
|
+
*/
|
|
48
|
+
importMap?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Chromium-specific configuration options
|
|
53
|
+
*/
|
|
54
|
+
export interface ChromiumOptions extends RuntimeOptions {
|
|
55
|
+
/**
|
|
56
|
+
* Chromium launch arguments
|
|
57
|
+
*/
|
|
58
|
+
launchArgs?: string[];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Headless mode (default: true)
|
|
62
|
+
*/
|
|
63
|
+
headless?: boolean;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Port range for HTTP server
|
|
67
|
+
*/
|
|
68
|
+
portRange?: { min: number; max: number };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Command configuration returned by createCommand()
|
|
73
|
+
*/
|
|
74
|
+
export interface RuntimeCommand {
|
|
75
|
+
/**
|
|
76
|
+
* The main command executable (e.g., 'node', 'deno', 'bun')
|
|
77
|
+
*/
|
|
78
|
+
command: string;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Command-line arguments
|
|
82
|
+
*/
|
|
83
|
+
args: string[];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Environment variables
|
|
87
|
+
*/
|
|
88
|
+
env?: Record<string, string>;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Working directory
|
|
92
|
+
*/
|
|
93
|
+
cwd?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Runtime availability check result
|
|
98
|
+
*/
|
|
99
|
+
export interface RuntimeAvailability {
|
|
100
|
+
/**
|
|
101
|
+
* Whether the runtime is available
|
|
102
|
+
*/
|
|
103
|
+
available: boolean;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Version string if available
|
|
107
|
+
*/
|
|
108
|
+
version?: string;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Error message if not available
|
|
112
|
+
*/
|
|
113
|
+
error?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Abstract base class for runtime adapters
|
|
118
|
+
* Each runtime (Node, Chromium, Deno, Bun) implements this interface
|
|
119
|
+
*/
|
|
120
|
+
export abstract class RuntimeAdapter {
|
|
121
|
+
/**
|
|
122
|
+
* Runtime identifier
|
|
123
|
+
*/
|
|
124
|
+
abstract readonly id: Runtime;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Human-readable display name
|
|
128
|
+
*/
|
|
129
|
+
abstract readonly displayName: string;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if this runtime is available on the system
|
|
133
|
+
* @returns Availability information including version
|
|
134
|
+
*/
|
|
135
|
+
abstract checkAvailable(): Promise<RuntimeAvailability>;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Create the command configuration for executing a test
|
|
139
|
+
* @param testFile - Absolute path to the test file
|
|
140
|
+
* @param options - Runtime-specific options
|
|
141
|
+
* @returns Command configuration
|
|
142
|
+
*/
|
|
143
|
+
abstract createCommand(testFile: string, options?: RuntimeOptions): RuntimeCommand;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Execute a test file and return a TAP parser
|
|
147
|
+
* @param testFile - Absolute path to the test file
|
|
148
|
+
* @param index - Test index (for display)
|
|
149
|
+
* @param total - Total number of tests (for display)
|
|
150
|
+
* @param options - Runtime-specific options
|
|
151
|
+
* @returns TAP parser with test results
|
|
152
|
+
*/
|
|
153
|
+
abstract run(
|
|
154
|
+
testFile: string,
|
|
155
|
+
index: number,
|
|
156
|
+
total: number,
|
|
157
|
+
options?: RuntimeOptions
|
|
158
|
+
): Promise<TapParser>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get the default options for this runtime
|
|
162
|
+
* Can be overridden by subclasses
|
|
163
|
+
*/
|
|
164
|
+
protected getDefaultOptions(): RuntimeOptions {
|
|
165
|
+
return {
|
|
166
|
+
timeout: 0,
|
|
167
|
+
extraArgs: [],
|
|
168
|
+
env: {},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Merge user options with defaults
|
|
174
|
+
*/
|
|
175
|
+
protected mergeOptions<T extends RuntimeOptions>(userOptions?: T): T {
|
|
176
|
+
const defaults = this.getDefaultOptions();
|
|
177
|
+
return {
|
|
178
|
+
...defaults,
|
|
179
|
+
...userOptions,
|
|
180
|
+
env: { ...defaults.env, ...userOptions?.env },
|
|
181
|
+
extraArgs: [...(defaults.extraArgs || []), ...(userOptions?.extraArgs || [])],
|
|
182
|
+
} as T;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Registry for runtime adapters
|
|
188
|
+
* Manages all available runtime implementations
|
|
189
|
+
*/
|
|
190
|
+
export class RuntimeAdapterRegistry {
|
|
191
|
+
private adapters: Map<Runtime, RuntimeAdapter> = new Map();
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Register a runtime adapter
|
|
195
|
+
*/
|
|
196
|
+
register(adapter: RuntimeAdapter): void {
|
|
197
|
+
this.adapters.set(adapter.id, adapter);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get an adapter by runtime ID
|
|
202
|
+
*/
|
|
203
|
+
get(runtime: Runtime): RuntimeAdapter | undefined {
|
|
204
|
+
return this.adapters.get(runtime);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get all registered adapters
|
|
209
|
+
*/
|
|
210
|
+
getAll(): RuntimeAdapter[] {
|
|
211
|
+
return Array.from(this.adapters.values());
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Check which runtimes are available on the system
|
|
216
|
+
*/
|
|
217
|
+
async checkAvailability(): Promise<Map<Runtime, RuntimeAvailability>> {
|
|
218
|
+
const results = new Map<Runtime, RuntimeAvailability>();
|
|
219
|
+
|
|
220
|
+
for (const [runtime, adapter] of this.adapters) {
|
|
221
|
+
const availability = await adapter.checkAvailable();
|
|
222
|
+
results.set(runtime, availability);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return results;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get adapters for a list of runtimes, in order
|
|
230
|
+
* @param runtimes - Ordered list of runtimes
|
|
231
|
+
* @returns Adapters in the same order, skipping any that aren't registered
|
|
232
|
+
*/
|
|
233
|
+
getAdaptersForRuntimes(runtimes: Runtime[]): RuntimeAdapter[] {
|
|
234
|
+
const adapters: RuntimeAdapter[] = [];
|
|
235
|
+
|
|
236
|
+
for (const runtime of runtimes) {
|
|
237
|
+
const adapter = this.get(runtime);
|
|
238
|
+
if (adapter) {
|
|
239
|
+
adapters.push(adapter);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return adapters;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import * as plugins from './tstest.plugins.js';
|
|
2
|
+
import { coloredString as cs } from '@push.rocks/consolecolor';
|
|
3
|
+
import {
|
|
4
|
+
RuntimeAdapter,
|
|
5
|
+
type RuntimeOptions,
|
|
6
|
+
type RuntimeCommand,
|
|
7
|
+
type RuntimeAvailability,
|
|
8
|
+
} from './tstest.classes.runtime.adapter.js';
|
|
9
|
+
import { TapParser } from './tstest.classes.tap.parser.js';
|
|
10
|
+
import { TsTestLogger } from './tstest.logging.js';
|
|
11
|
+
import type { Runtime } from './tstest.classes.runtime.parser.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Bun runtime adapter
|
|
15
|
+
* Executes tests using the Bun runtime with native TypeScript support
|
|
16
|
+
*/
|
|
17
|
+
export class BunRuntimeAdapter extends RuntimeAdapter {
|
|
18
|
+
readonly id: Runtime = 'bun';
|
|
19
|
+
readonly displayName: string = 'Bun';
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
private logger: TsTestLogger,
|
|
23
|
+
private smartshellInstance: any, // SmartShell instance from @push.rocks/smartshell
|
|
24
|
+
private timeoutSeconds: number | null,
|
|
25
|
+
private filterTags: string[]
|
|
26
|
+
) {
|
|
27
|
+
super();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if Bun is available
|
|
32
|
+
*/
|
|
33
|
+
async checkAvailable(): Promise<RuntimeAvailability> {
|
|
34
|
+
try {
|
|
35
|
+
const result = await this.smartshellInstance.exec('bun --version', {
|
|
36
|
+
cwd: process.cwd(),
|
|
37
|
+
onError: () => {
|
|
38
|
+
// Ignore error
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (result.exitCode !== 0) {
|
|
43
|
+
return {
|
|
44
|
+
available: false,
|
|
45
|
+
error: 'Bun not found. Install from: https://bun.sh/',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Bun version is just the version number
|
|
50
|
+
const version = result.stdout.trim();
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
available: true,
|
|
54
|
+
version: `Bun ${version}`,
|
|
55
|
+
};
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
available: false,
|
|
59
|
+
error: error.message,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Create command configuration for Bun test execution
|
|
66
|
+
*/
|
|
67
|
+
createCommand(testFile: string, options?: RuntimeOptions): RuntimeCommand {
|
|
68
|
+
const mergedOptions = this.mergeOptions(options);
|
|
69
|
+
|
|
70
|
+
const args: string[] = ['run'];
|
|
71
|
+
|
|
72
|
+
// Add extra args
|
|
73
|
+
if (mergedOptions.extraArgs && mergedOptions.extraArgs.length > 0) {
|
|
74
|
+
args.push(...mergedOptions.extraArgs);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Add test file
|
|
78
|
+
args.push(testFile);
|
|
79
|
+
|
|
80
|
+
// Set environment variables
|
|
81
|
+
const env = { ...mergedOptions.env };
|
|
82
|
+
|
|
83
|
+
if (this.filterTags.length > 0) {
|
|
84
|
+
env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
command: 'bun',
|
|
89
|
+
args,
|
|
90
|
+
env,
|
|
91
|
+
cwd: mergedOptions.cwd,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Execute a test file in Bun
|
|
97
|
+
*/
|
|
98
|
+
async run(
|
|
99
|
+
testFile: string,
|
|
100
|
+
index: number,
|
|
101
|
+
total: number,
|
|
102
|
+
options?: RuntimeOptions
|
|
103
|
+
): Promise<TapParser> {
|
|
104
|
+
this.logger.testFileStart(testFile, this.displayName, index, total);
|
|
105
|
+
const tapParser = new TapParser(testFile + ':bun', this.logger);
|
|
106
|
+
|
|
107
|
+
const mergedOptions = this.mergeOptions(options);
|
|
108
|
+
|
|
109
|
+
// Build Bun command
|
|
110
|
+
const command = this.createCommand(testFile, mergedOptions);
|
|
111
|
+
const fullCommand = `${command.command} ${command.args.join(' ')}`;
|
|
112
|
+
|
|
113
|
+
// Set filter tags as environment variable
|
|
114
|
+
if (this.filterTags.length > 0) {
|
|
115
|
+
process.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check for 00init.ts file in test directory
|
|
119
|
+
const testDir = plugins.path.dirname(testFile);
|
|
120
|
+
const initFile = plugins.path.join(testDir, '00init.ts');
|
|
121
|
+
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
|
|
122
|
+
|
|
123
|
+
let runCommand = fullCommand;
|
|
124
|
+
let loaderPath: string | null = null;
|
|
125
|
+
|
|
126
|
+
// If 00init.ts exists, create a loader file
|
|
127
|
+
if (initFileExists) {
|
|
128
|
+
const absoluteInitFile = plugins.path.resolve(initFile);
|
|
129
|
+
const absoluteTestFile = plugins.path.resolve(testFile);
|
|
130
|
+
const loaderContent = `
|
|
131
|
+
import '${absoluteInitFile.replace(/\\/g, '/')}';
|
|
132
|
+
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|
133
|
+
`;
|
|
134
|
+
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
|
135
|
+
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
|
|
136
|
+
|
|
137
|
+
// Rebuild command with loader file
|
|
138
|
+
const loaderCommand = this.createCommand(loaderPath, mergedOptions);
|
|
139
|
+
runCommand = `${loaderCommand.command} ${loaderCommand.args.join(' ')}`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const execResultStreaming = await this.smartshellInstance.execStreamingSilent(runCommand);
|
|
143
|
+
|
|
144
|
+
// If we created a loader file, clean it up after test execution
|
|
145
|
+
if (loaderPath) {
|
|
146
|
+
const cleanup = () => {
|
|
147
|
+
try {
|
|
148
|
+
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
|
|
149
|
+
plugins.smartfile.fs.removeSync(loaderPath);
|
|
150
|
+
}
|
|
151
|
+
} catch (e) {
|
|
152
|
+
// Ignore cleanup errors
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
execResultStreaming.childProcess.on('exit', cleanup);
|
|
157
|
+
execResultStreaming.childProcess.on('error', cleanup);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Start warning timer if no timeout was specified
|
|
161
|
+
let warningTimer: NodeJS.Timeout | null = null;
|
|
162
|
+
if (this.timeoutSeconds === null) {
|
|
163
|
+
warningTimer = setTimeout(() => {
|
|
164
|
+
console.error('');
|
|
165
|
+
console.error(cs('⚠️ WARNING: Test file is running for more than 1 minute', 'orange'));
|
|
166
|
+
console.error(cs(` File: ${testFile}`, 'orange'));
|
|
167
|
+
console.error(cs(' Consider using --timeout option to set a timeout for test files.', 'orange'));
|
|
168
|
+
console.error(cs(' Example: tstest test --timeout=300 (for 5 minutes)', 'orange'));
|
|
169
|
+
console.error('');
|
|
170
|
+
}, 60000); // 1 minute
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Handle timeout if specified
|
|
174
|
+
if (this.timeoutSeconds !== null) {
|
|
175
|
+
const timeoutMs = this.timeoutSeconds * 1000;
|
|
176
|
+
let timeoutId: NodeJS.Timeout;
|
|
177
|
+
|
|
178
|
+
const timeoutPromise = new Promise<void>((_resolve, reject) => {
|
|
179
|
+
timeoutId = setTimeout(async () => {
|
|
180
|
+
// Use smartshell's terminate() to kill entire process tree
|
|
181
|
+
await execResultStreaming.terminate();
|
|
182
|
+
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
|
|
183
|
+
}, timeoutMs);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
await Promise.race([
|
|
188
|
+
tapParser.handleTapProcess(execResultStreaming.childProcess),
|
|
189
|
+
timeoutPromise
|
|
190
|
+
]);
|
|
191
|
+
// Clear timeout if test completed successfully
|
|
192
|
+
clearTimeout(timeoutId);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
// Clear warning timer if it was set
|
|
195
|
+
if (warningTimer) {
|
|
196
|
+
clearTimeout(warningTimer);
|
|
197
|
+
}
|
|
198
|
+
// Handle timeout error
|
|
199
|
+
tapParser.handleTimeout(this.timeoutSeconds);
|
|
200
|
+
// Ensure entire process tree is killed if still running
|
|
201
|
+
try {
|
|
202
|
+
await execResultStreaming.kill(); // This kills the entire process tree with SIGKILL
|
|
203
|
+
} catch (killError) {
|
|
204
|
+
// Process tree might already be dead
|
|
205
|
+
}
|
|
206
|
+
await tapParser.evaluateFinalResult();
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
await tapParser.handleTapProcess(execResultStreaming.childProcess);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Clear warning timer if it was set
|
|
213
|
+
if (warningTimer) {
|
|
214
|
+
clearTimeout(warningTimer);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return tapParser;
|
|
218
|
+
}
|
|
219
|
+
}
|