@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,222 @@
|
|
|
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
|
+
* Node.js runtime adapter
|
|
15
|
+
* Executes tests using tsrun (TypeScript runner for Node.js)
|
|
16
|
+
*/
|
|
17
|
+
export class NodeRuntimeAdapter extends RuntimeAdapter {
|
|
18
|
+
readonly id: Runtime = 'node';
|
|
19
|
+
readonly displayName: string = 'Node.js';
|
|
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 Node.js and tsrun are available
|
|
32
|
+
*/
|
|
33
|
+
async checkAvailable(): Promise<RuntimeAvailability> {
|
|
34
|
+
try {
|
|
35
|
+
// Check Node.js version
|
|
36
|
+
const nodeVersion = process.version;
|
|
37
|
+
|
|
38
|
+
// Check if tsrun is available
|
|
39
|
+
const result = await this.smartshellInstance.exec('tsrun --version', {
|
|
40
|
+
cwd: process.cwd(),
|
|
41
|
+
onError: () => {
|
|
42
|
+
// Ignore error
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (result.exitCode !== 0) {
|
|
47
|
+
return {
|
|
48
|
+
available: false,
|
|
49
|
+
error: 'tsrun not found. Install with: pnpm install --save-dev @git.zone/tsrun',
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
available: true,
|
|
55
|
+
version: nodeVersion,
|
|
56
|
+
};
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return {
|
|
59
|
+
available: false,
|
|
60
|
+
error: error.message,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Create command configuration for Node.js test execution
|
|
67
|
+
*/
|
|
68
|
+
createCommand(testFile: string, options?: RuntimeOptions): RuntimeCommand {
|
|
69
|
+
const mergedOptions = this.mergeOptions(options);
|
|
70
|
+
|
|
71
|
+
// Build tsrun options
|
|
72
|
+
const args: string[] = [];
|
|
73
|
+
|
|
74
|
+
if (process.argv.includes('--web')) {
|
|
75
|
+
args.push('--web');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Add any extra args
|
|
79
|
+
if (mergedOptions.extraArgs) {
|
|
80
|
+
args.push(...mergedOptions.extraArgs);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Set environment variables
|
|
84
|
+
const env = { ...mergedOptions.env };
|
|
85
|
+
|
|
86
|
+
if (this.filterTags.length > 0) {
|
|
87
|
+
env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
command: 'tsrun',
|
|
92
|
+
args: [testFile, ...args],
|
|
93
|
+
env,
|
|
94
|
+
cwd: mergedOptions.cwd,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Execute a test file in Node.js
|
|
100
|
+
*/
|
|
101
|
+
async run(
|
|
102
|
+
testFile: string,
|
|
103
|
+
index: number,
|
|
104
|
+
total: number,
|
|
105
|
+
options?: RuntimeOptions
|
|
106
|
+
): Promise<TapParser> {
|
|
107
|
+
this.logger.testFileStart(testFile, this.displayName, index, total);
|
|
108
|
+
const tapParser = new TapParser(testFile + ':node', this.logger);
|
|
109
|
+
|
|
110
|
+
const mergedOptions = this.mergeOptions(options);
|
|
111
|
+
|
|
112
|
+
// Build tsrun command
|
|
113
|
+
let tsrunOptions = '';
|
|
114
|
+
if (process.argv.includes('--web')) {
|
|
115
|
+
tsrunOptions += ' --web';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Set filter tags as environment variable
|
|
119
|
+
if (this.filterTags.length > 0) {
|
|
120
|
+
process.env.TSTEST_FILTER_TAGS = this.filterTags.join(',');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check for 00init.ts file in test directory
|
|
124
|
+
const testDir = plugins.path.dirname(testFile);
|
|
125
|
+
const initFile = plugins.path.join(testDir, '00init.ts');
|
|
126
|
+
let runCommand = `tsrun ${testFile}${tsrunOptions}`;
|
|
127
|
+
|
|
128
|
+
const initFileExists = await plugins.smartfile.fs.fileExists(initFile);
|
|
129
|
+
|
|
130
|
+
// If 00init.ts exists, run it first
|
|
131
|
+
let loaderPath: string | null = null;
|
|
132
|
+
if (initFileExists) {
|
|
133
|
+
// Create a temporary loader file that imports both 00init.ts and the test file
|
|
134
|
+
const absoluteInitFile = plugins.path.resolve(initFile);
|
|
135
|
+
const absoluteTestFile = plugins.path.resolve(testFile);
|
|
136
|
+
const loaderContent = `
|
|
137
|
+
import '${absoluteInitFile.replace(/\\/g, '/')}';
|
|
138
|
+
import '${absoluteTestFile.replace(/\\/g, '/')}';
|
|
139
|
+
`;
|
|
140
|
+
loaderPath = plugins.path.join(testDir, `.loader_${plugins.path.basename(testFile)}`);
|
|
141
|
+
await plugins.smartfile.memory.toFs(loaderContent, loaderPath);
|
|
142
|
+
runCommand = `tsrun ${loaderPath}${tsrunOptions}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const execResultStreaming = await this.smartshellInstance.execStreamingSilent(runCommand);
|
|
146
|
+
|
|
147
|
+
// If we created a loader file, clean it up after test execution
|
|
148
|
+
if (loaderPath) {
|
|
149
|
+
const cleanup = () => {
|
|
150
|
+
try {
|
|
151
|
+
if (plugins.smartfile.fs.fileExistsSync(loaderPath)) {
|
|
152
|
+
plugins.smartfile.fs.removeSync(loaderPath);
|
|
153
|
+
}
|
|
154
|
+
} catch (e) {
|
|
155
|
+
// Ignore cleanup errors
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
execResultStreaming.childProcess.on('exit', cleanup);
|
|
160
|
+
execResultStreaming.childProcess.on('error', cleanup);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Start warning timer if no timeout was specified
|
|
164
|
+
let warningTimer: NodeJS.Timeout | null = null;
|
|
165
|
+
if (this.timeoutSeconds === null) {
|
|
166
|
+
warningTimer = setTimeout(() => {
|
|
167
|
+
console.error('');
|
|
168
|
+
console.error(cs('⚠️ WARNING: Test file is running for more than 1 minute', 'orange'));
|
|
169
|
+
console.error(cs(` File: ${testFile}`, 'orange'));
|
|
170
|
+
console.error(cs(' Consider using --timeout option to set a timeout for test files.', 'orange'));
|
|
171
|
+
console.error(cs(' Example: tstest test --timeout=300 (for 5 minutes)', 'orange'));
|
|
172
|
+
console.error('');
|
|
173
|
+
}, 60000); // 1 minute
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Handle timeout if specified
|
|
177
|
+
if (this.timeoutSeconds !== null) {
|
|
178
|
+
const timeoutMs = this.timeoutSeconds * 1000;
|
|
179
|
+
let timeoutId: NodeJS.Timeout;
|
|
180
|
+
|
|
181
|
+
const timeoutPromise = new Promise<void>((_resolve, reject) => {
|
|
182
|
+
timeoutId = setTimeout(async () => {
|
|
183
|
+
// Use smartshell's terminate() to kill entire process tree
|
|
184
|
+
await execResultStreaming.terminate();
|
|
185
|
+
reject(new Error(`Test file timed out after ${this.timeoutSeconds} seconds`));
|
|
186
|
+
}, timeoutMs);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
await Promise.race([
|
|
191
|
+
tapParser.handleTapProcess(execResultStreaming.childProcess),
|
|
192
|
+
timeoutPromise
|
|
193
|
+
]);
|
|
194
|
+
// Clear timeout if test completed successfully
|
|
195
|
+
clearTimeout(timeoutId);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
// Clear warning timer if it was set
|
|
198
|
+
if (warningTimer) {
|
|
199
|
+
clearTimeout(warningTimer);
|
|
200
|
+
}
|
|
201
|
+
// Handle timeout error
|
|
202
|
+
tapParser.handleTimeout(this.timeoutSeconds);
|
|
203
|
+
// Ensure entire process tree is killed if still running
|
|
204
|
+
try {
|
|
205
|
+
await execResultStreaming.kill(); // This kills the entire process tree with SIGKILL
|
|
206
|
+
} catch (killError) {
|
|
207
|
+
// Process tree might already be dead
|
|
208
|
+
}
|
|
209
|
+
await tapParser.evaluateFinalResult();
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
await tapParser.handleTapProcess(execResultStreaming.childProcess);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Clear warning timer if it was set
|
|
216
|
+
if (warningTimer) {
|
|
217
|
+
clearTimeout(warningTimer);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return tapParser;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime parser for test file naming convention
|
|
3
|
+
* Supports: test.runtime1+runtime2.modifier.ts
|
|
4
|
+
* Examples:
|
|
5
|
+
* - test.node.ts
|
|
6
|
+
* - test.chromium.ts
|
|
7
|
+
* - test.node+chromium.ts
|
|
8
|
+
* - test.deno+bun.ts
|
|
9
|
+
* - test.chromium.nonci.ts
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type Runtime = 'node' | 'chromium' | 'deno' | 'bun';
|
|
13
|
+
export type Modifier = 'nonci';
|
|
14
|
+
|
|
15
|
+
export interface ParsedFilename {
|
|
16
|
+
baseName: string;
|
|
17
|
+
runtimes: Runtime[];
|
|
18
|
+
modifiers: Modifier[];
|
|
19
|
+
extension: string;
|
|
20
|
+
isLegacy: boolean;
|
|
21
|
+
original: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ParserConfig {
|
|
25
|
+
strictUnknownRuntime?: boolean; // default: true
|
|
26
|
+
defaultRuntimes?: Runtime[]; // default: ['node']
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const KNOWN_RUNTIMES: Set<string> = new Set(['node', 'chromium', 'deno', 'bun']);
|
|
30
|
+
const KNOWN_MODIFIERS: Set<string> = new Set(['nonci']);
|
|
31
|
+
const VALID_EXTENSIONS: Set<string> = new Set(['ts', 'tsx', 'mts', 'cts']);
|
|
32
|
+
|
|
33
|
+
// Legacy mappings for backwards compatibility
|
|
34
|
+
const LEGACY_RUNTIME_MAP: Record<string, Runtime[]> = {
|
|
35
|
+
browser: ['chromium'],
|
|
36
|
+
both: ['node', 'chromium'],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse a test filename to extract runtimes, modifiers, and detect legacy patterns
|
|
41
|
+
* Algorithm: Right-to-left token analysis from the extension
|
|
42
|
+
*/
|
|
43
|
+
export function parseTestFilename(
|
|
44
|
+
filePath: string,
|
|
45
|
+
config: ParserConfig = {}
|
|
46
|
+
): ParsedFilename {
|
|
47
|
+
const strictUnknownRuntime = config.strictUnknownRuntime ?? true;
|
|
48
|
+
const defaultRuntimes = config.defaultRuntimes ?? ['node'];
|
|
49
|
+
|
|
50
|
+
// Extract just the filename from the path
|
|
51
|
+
const fileName = filePath.split('/').pop() || filePath;
|
|
52
|
+
const original = fileName;
|
|
53
|
+
|
|
54
|
+
// Step 1: Extract and validate extension
|
|
55
|
+
const lastDot = fileName.lastIndexOf('.');
|
|
56
|
+
if (lastDot === -1) {
|
|
57
|
+
throw new Error(`Invalid test file: no extension found in "${fileName}"`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const extension = fileName.substring(lastDot + 1);
|
|
61
|
+
if (!VALID_EXTENSIONS.has(extension)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`Invalid test file extension ".${extension}" in "${fileName}". ` +
|
|
64
|
+
`Valid extensions: ${Array.from(VALID_EXTENSIONS).join(', ')}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Step 2: Split remaining basename by dots
|
|
69
|
+
const withoutExtension = fileName.substring(0, lastDot);
|
|
70
|
+
const tokens = withoutExtension.split('.');
|
|
71
|
+
|
|
72
|
+
if (tokens.length === 0) {
|
|
73
|
+
throw new Error(`Invalid test file: empty basename in "${fileName}"`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Step 3: Parse from right to left
|
|
77
|
+
let isLegacy = false;
|
|
78
|
+
const modifiers: Modifier[] = [];
|
|
79
|
+
let runtimes: Runtime[] = [];
|
|
80
|
+
let runtimeTokenIndex = -1;
|
|
81
|
+
|
|
82
|
+
// Scan from right to left
|
|
83
|
+
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
84
|
+
const token = tokens[i];
|
|
85
|
+
|
|
86
|
+
// Check if this is a known modifier
|
|
87
|
+
if (KNOWN_MODIFIERS.has(token)) {
|
|
88
|
+
modifiers.unshift(token as Modifier);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check if this is a legacy runtime token
|
|
93
|
+
if (LEGACY_RUNTIME_MAP[token]) {
|
|
94
|
+
isLegacy = true;
|
|
95
|
+
runtimes = LEGACY_RUNTIME_MAP[token];
|
|
96
|
+
runtimeTokenIndex = i;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Check if this is a runtime chain (may contain + separators)
|
|
101
|
+
if (token.includes('+')) {
|
|
102
|
+
const runtimeCandidates = token.split('+').map(r => r.trim()).filter(Boolean);
|
|
103
|
+
const validRuntimes: Runtime[] = [];
|
|
104
|
+
const invalidRuntimes: string[] = [];
|
|
105
|
+
|
|
106
|
+
for (const candidate of runtimeCandidates) {
|
|
107
|
+
if (KNOWN_RUNTIMES.has(candidate)) {
|
|
108
|
+
// Dedupe: only add if not already in list
|
|
109
|
+
if (!validRuntimes.includes(candidate as Runtime)) {
|
|
110
|
+
validRuntimes.push(candidate as Runtime);
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
invalidRuntimes.push(candidate);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (invalidRuntimes.length > 0) {
|
|
118
|
+
if (strictUnknownRuntime) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Unknown runtime(s) in "${fileName}": ${invalidRuntimes.join(', ')}. ` +
|
|
121
|
+
`Valid runtimes: ${Array.from(KNOWN_RUNTIMES).join(', ')}`
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
console.warn(
|
|
125
|
+
`⚠️ Warning: Unknown runtime(s) in "${fileName}": ${invalidRuntimes.join(', ')}. ` +
|
|
126
|
+
`Defaulting to: ${defaultRuntimes.join('+')}`
|
|
127
|
+
);
|
|
128
|
+
runtimes = [...defaultRuntimes];
|
|
129
|
+
runtimeTokenIndex = i;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (validRuntimes.length > 0) {
|
|
135
|
+
runtimes = validRuntimes;
|
|
136
|
+
runtimeTokenIndex = i;
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check if this is a single runtime token
|
|
142
|
+
if (KNOWN_RUNTIMES.has(token)) {
|
|
143
|
+
runtimes = [token as Runtime];
|
|
144
|
+
runtimeTokenIndex = i;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// If we've scanned past modifiers and haven't found a runtime, stop looking
|
|
149
|
+
if (modifiers.length > 0) {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Step 4: Determine base name
|
|
155
|
+
// Everything before the runtime token (if found) is the base name
|
|
156
|
+
const baseNameTokens = runtimeTokenIndex >= 0 ? tokens.slice(0, runtimeTokenIndex) : tokens;
|
|
157
|
+
const baseName = baseNameTokens.join('.');
|
|
158
|
+
|
|
159
|
+
// Step 5: Apply defaults if no runtime was detected
|
|
160
|
+
if (runtimes.length === 0) {
|
|
161
|
+
runtimes = [...defaultRuntimes];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
baseName: baseName || 'test',
|
|
166
|
+
runtimes,
|
|
167
|
+
modifiers,
|
|
168
|
+
extension,
|
|
169
|
+
isLegacy,
|
|
170
|
+
original,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Check if a filename uses legacy naming convention
|
|
176
|
+
*/
|
|
177
|
+
export function isLegacyFilename(fileName: string): boolean {
|
|
178
|
+
const tokens = fileName.split('.');
|
|
179
|
+
for (const token of tokens) {
|
|
180
|
+
if (LEGACY_RUNTIME_MAP[token]) {
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get the suggested new filename for a legacy filename
|
|
189
|
+
*/
|
|
190
|
+
export function getLegacyMigrationTarget(fileName: string): string | null {
|
|
191
|
+
const parsed = parseTestFilename(fileName, { strictUnknownRuntime: false });
|
|
192
|
+
|
|
193
|
+
if (!parsed.isLegacy) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Reconstruct filename with new naming
|
|
198
|
+
const parts = [parsed.baseName];
|
|
199
|
+
|
|
200
|
+
if (parsed.runtimes.length > 0) {
|
|
201
|
+
parts.push(parsed.runtimes.join('+'));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (parsed.modifiers.length > 0) {
|
|
205
|
+
parts.push(...parsed.modifiers);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
parts.push(parsed.extension);
|
|
209
|
+
|
|
210
|
+
return parts.join('.');
|
|
211
|
+
}
|
|
@@ -10,6 +10,14 @@ import { TestExecutionMode } from './index.js';
|
|
|
10
10
|
import { TsTestLogger } from './tstest.logging.js';
|
|
11
11
|
import type { LogOptions } from './tstest.logging.js';
|
|
12
12
|
|
|
13
|
+
// Runtime adapters
|
|
14
|
+
import { parseTestFilename } from './tstest.classes.runtime.parser.js';
|
|
15
|
+
import { RuntimeAdapterRegistry } from './tstest.classes.runtime.adapter.js';
|
|
16
|
+
import { NodeRuntimeAdapter } from './tstest.classes.runtime.node.js';
|
|
17
|
+
import { ChromiumRuntimeAdapter } from './tstest.classes.runtime.chromium.js';
|
|
18
|
+
import { DenoRuntimeAdapter } from './tstest.classes.runtime.deno.js';
|
|
19
|
+
import { BunRuntimeAdapter } from './tstest.classes.runtime.bun.js';
|
|
20
|
+
|
|
13
21
|
export class TsTest {
|
|
14
22
|
public testDir: TestDirectory;
|
|
15
23
|
public executionMode: TestExecutionMode;
|
|
@@ -28,6 +36,8 @@ export class TsTest {
|
|
|
28
36
|
|
|
29
37
|
public tsbundleInstance = new plugins.tsbundle.TsBundle();
|
|
30
38
|
|
|
39
|
+
public runtimeRegistry = new RuntimeAdapterRegistry();
|
|
40
|
+
|
|
31
41
|
constructor(cwdArg: string, testPathArg: string, executionModeArg: TestExecutionMode, logOptions: LogOptions = {}, tags: string[] = [], startFromFile: number | null = null, stopAtFile: number | null = null, timeoutSeconds: number | null = null) {
|
|
32
42
|
this.executionMode = executionModeArg;
|
|
33
43
|
this.testDir = new TestDirectory(cwdArg, testPathArg, executionModeArg);
|
|
@@ -36,6 +46,20 @@ export class TsTest {
|
|
|
36
46
|
this.startFromFile = startFromFile;
|
|
37
47
|
this.stopAtFile = stopAtFile;
|
|
38
48
|
this.timeoutSeconds = timeoutSeconds;
|
|
49
|
+
|
|
50
|
+
// Register runtime adapters
|
|
51
|
+
this.runtimeRegistry.register(
|
|
52
|
+
new NodeRuntimeAdapter(this.logger, this.smartshellInstance, this.timeoutSeconds, this.filterTags)
|
|
53
|
+
);
|
|
54
|
+
this.runtimeRegistry.register(
|
|
55
|
+
new ChromiumRuntimeAdapter(this.logger, this.tsbundleInstance, this.smartbrowserInstance, this.timeoutSeconds)
|
|
56
|
+
);
|
|
57
|
+
this.runtimeRegistry.register(
|
|
58
|
+
new DenoRuntimeAdapter(this.logger, this.smartshellInstance, this.timeoutSeconds, this.filterTags)
|
|
59
|
+
);
|
|
60
|
+
this.runtimeRegistry.register(
|
|
61
|
+
new BunRuntimeAdapter(this.logger, this.smartshellInstance, this.timeoutSeconds, this.filterTags)
|
|
62
|
+
);
|
|
39
63
|
}
|
|
40
64
|
|
|
41
65
|
async run() {
|
|
@@ -175,29 +199,50 @@ export class TsTest {
|
|
|
175
199
|
}
|
|
176
200
|
|
|
177
201
|
private async runSingleTest(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
202
|
+
// Parse the filename to determine runtimes and modifiers
|
|
203
|
+
const fileName = plugins.path.basename(fileNameArg);
|
|
204
|
+
const parsed = parseTestFilename(fileName, { strictUnknownRuntime: false });
|
|
205
|
+
|
|
206
|
+
// Check for nonci modifier in CI environment
|
|
207
|
+
if (process.env.CI && parsed.modifiers.includes('nonci')) {
|
|
208
|
+
this.logger.tapOutput(`Skipping ${fileNameArg} - marked as non-CI`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Show deprecation warning for legacy naming
|
|
213
|
+
if (parsed.isLegacy) {
|
|
214
|
+
console.warn('');
|
|
215
|
+
console.warn(cs('⚠️ DEPRECATION WARNING', 'orange'));
|
|
216
|
+
console.warn(cs(` File: ${fileName}`, 'orange'));
|
|
217
|
+
console.warn(cs(` Legacy naming detected. Please migrate to new naming convention.`, 'orange'));
|
|
218
|
+
console.warn(cs(` Suggested: ${fileName.replace('.browser.', '.chromium.').replace('.both.', '.node+chromium.')}`, 'green'));
|
|
219
|
+
console.warn(cs(` Run: tstest migrate --dry-run`, 'cyan'));
|
|
220
|
+
console.warn('');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Get adapters for the specified runtimes
|
|
224
|
+
const adapters = this.runtimeRegistry.getAdaptersForRuntimes(parsed.runtimes);
|
|
225
|
+
|
|
226
|
+
if (adapters.length === 0) {
|
|
227
|
+
this.logger.tapOutput(`Skipping ${fileNameArg} - no runtime adapters available`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Execute tests for each runtime
|
|
232
|
+
if (adapters.length === 1) {
|
|
233
|
+
// Single runtime - no sections needed
|
|
234
|
+
const adapter = adapters[0];
|
|
235
|
+
const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles);
|
|
236
|
+
tapCombinator.addTapParser(tapParser);
|
|
237
|
+
} else {
|
|
238
|
+
// Multiple runtimes - use sections
|
|
239
|
+
for (let i = 0; i < adapters.length; i++) {
|
|
240
|
+
const adapter = adapters[i];
|
|
241
|
+
this.logger.sectionStart(`Part ${i + 1}: ${adapter.displayName}`);
|
|
242
|
+
const tapParser = await adapter.run(fileNameArg, fileIndex, totalFiles);
|
|
243
|
+
tapCombinator.addTapParser(tapParser);
|
|
195
244
|
this.logger.sectionEnd();
|
|
196
|
-
|
|
197
|
-
default:
|
|
198
|
-
const tapParserNode = await this.runInNode(fileNameArg, fileIndex, totalFiles);
|
|
199
|
-
tapCombinator.addTapParser(tapParserNode);
|
|
200
|
-
break;
|
|
245
|
+
}
|
|
201
246
|
}
|
|
202
247
|
}
|
|
203
248
|
|