@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.
@@ -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
+ }