feat(docker): add Docker test file support and runtime adapter
This commit is contained in:
		
							
								
								
									
										9
									
								
								test/test.example.latest.docker.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										9
									
								
								test/test.example.latest.docker.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | # Sample Docker test file | ||||||
|  | # This file demonstrates the naming pattern: test.{baseName}.{variant}.docker.sh | ||||||
|  | # The variant "latest" maps to the Dockerfile in the project root | ||||||
|  |  | ||||||
|  | echo "TAP version 13" | ||||||
|  | echo "1..2" | ||||||
|  | echo "ok 1 - Sample Docker test passes" | ||||||
|  | echo "ok 2 - Docker environment is working" | ||||||
							
								
								
									
										251
									
								
								ts/tstest.classes.runtime.docker.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								ts/tstest.classes.runtime.docker.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,251 @@ | |||||||
|  | import * as plugins from './tstest.plugins.js'; | ||||||
|  | import { coloredString as cs } from '@push.rocks/consolecolor'; | ||||||
|  | import { | ||||||
|  |   RuntimeAdapter, | ||||||
|  |   type RuntimeOptions, | ||||||
|  |   type RuntimeCommand, | ||||||
|  |   type RuntimeAvailability, | ||||||
|  | } from './tstest.classes.runtime.adapter.js'; | ||||||
|  | import { TapParser } from './tstest.classes.tap.parser.js'; | ||||||
|  | import { TsTestLogger } from './tstest.logging.js'; | ||||||
|  | import type { Runtime } from './tstest.classes.runtime.parser.js'; | ||||||
|  | import { | ||||||
|  |   parseDockerTestFilename, | ||||||
|  |   mapVariantToDockerfile, | ||||||
|  |   isDockerTestFile | ||||||
|  | } from './tstest.classes.runtime.parser.js'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Docker runtime adapter | ||||||
|  |  * Executes shell test files inside Docker containers | ||||||
|  |  * Pattern: test.{variant}.docker.sh | ||||||
|  |  * Variants map to Dockerfiles: latest -> Dockerfile, others -> Dockerfile_{variant} | ||||||
|  |  */ | ||||||
|  | export class DockerRuntimeAdapter extends RuntimeAdapter { | ||||||
|  |   readonly id: Runtime = 'node'; // Using 'node' temporarily as Runtime type doesn't include 'docker' | ||||||
|  |   readonly displayName: string = 'Docker'; | ||||||
|  |  | ||||||
|  |   private builtImages: Set<string> = new Set(); // Track built images to avoid rebuilding | ||||||
|  |  | ||||||
|  |   constructor( | ||||||
|  |     private logger: TsTestLogger, | ||||||
|  |     private smartshellInstance: any, // SmartShell instance from @push.rocks/smartshell | ||||||
|  |     private timeoutSeconds: number | null, | ||||||
|  |     private cwd: string | ||||||
|  |   ) { | ||||||
|  |     super(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Check if Docker CLI is available | ||||||
|  |    */ | ||||||
|  |   async checkAvailable(): Promise<RuntimeAvailability> { | ||||||
|  |     try { | ||||||
|  |       const result = await this.smartshellInstance.exec('docker --version'); | ||||||
|  |  | ||||||
|  |       if (result.exitCode !== 0) { | ||||||
|  |         return { | ||||||
|  |           available: false, | ||||||
|  |           error: 'Docker command failed', | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Extract version from output like "Docker version 24.0.5, build ced0996" | ||||||
|  |       const versionMatch = result.stdout.match(/Docker version ([^,]+)/); | ||||||
|  |       const version = versionMatch ? versionMatch[1] : 'unknown'; | ||||||
|  |  | ||||||
|  |       return { | ||||||
|  |         available: true, | ||||||
|  |         version, | ||||||
|  |       }; | ||||||
|  |     } catch (error) { | ||||||
|  |       return { | ||||||
|  |         available: false, | ||||||
|  |         error: `Docker not found: ${error.message}`, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Create command configuration for Docker test execution | ||||||
|  |    * This is used for informational purposes | ||||||
|  |    */ | ||||||
|  |   createCommand(testFile: string, options?: RuntimeOptions): RuntimeCommand { | ||||||
|  |     const parsed = parseDockerTestFilename(testFile); | ||||||
|  |     const dockerfilePath = mapVariantToDockerfile(parsed.variant, this.cwd); | ||||||
|  |     const imageName = `tstest-${parsed.variant}`; | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       command: 'docker', | ||||||
|  |       args: [ | ||||||
|  |         'run', | ||||||
|  |         '--rm', | ||||||
|  |         '-v', | ||||||
|  |         `${this.cwd}/test:/test`, | ||||||
|  |         imageName, | ||||||
|  |         'taprun', | ||||||
|  |         `/test/${plugins.path.basename(testFile)}` | ||||||
|  |       ], | ||||||
|  |       env: {}, | ||||||
|  |       cwd: this.cwd, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Build a Docker image from the specified Dockerfile | ||||||
|  |    */ | ||||||
|  |   private async buildDockerImage(dockerfilePath: string, imageName: string): Promise<void> { | ||||||
|  |     // Check if image is already built | ||||||
|  |     if (this.builtImages.has(imageName)) { | ||||||
|  |       this.logger.tapOutput(`Using cached Docker image: ${imageName}`); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Check if Dockerfile exists | ||||||
|  |     if (!await plugins.smartfile.fs.fileExists(dockerfilePath)) { | ||||||
|  |       throw new Error( | ||||||
|  |         `Dockerfile not found: ${dockerfilePath}\n` + | ||||||
|  |         `Expected Dockerfile for Docker test variant.` | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     this.logger.tapOutput(`Building Docker image: ${imageName} from ${dockerfilePath}`); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       const buildResult = await this.smartshellInstance.exec( | ||||||
|  |         `docker build -f ${dockerfilePath} -t ${imageName} ${this.cwd}`, | ||||||
|  |         { | ||||||
|  |           cwd: this.cwd, | ||||||
|  |         } | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       if (buildResult.exitCode !== 0) { | ||||||
|  |         throw new Error(`Docker build failed:\n${buildResult.stderr}`); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       this.builtImages.add(imageName); | ||||||
|  |       this.logger.tapOutput(`✅ Docker image built successfully: ${imageName}`); | ||||||
|  |     } catch (error) { | ||||||
|  |       throw new Error(`Failed to build Docker image: ${error.message}`); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Execute a Docker test file | ||||||
|  |    */ | ||||||
|  |   async run( | ||||||
|  |     testFile: string, | ||||||
|  |     index: number, | ||||||
|  |     total: number, | ||||||
|  |     options?: RuntimeOptions | ||||||
|  |   ): Promise<TapParser> { | ||||||
|  |     this.logger.testFileStart(testFile, this.displayName, index, total); | ||||||
|  |  | ||||||
|  |     // Parse the Docker test filename | ||||||
|  |     const parsed = parseDockerTestFilename(testFile); | ||||||
|  |     const dockerfilePath = mapVariantToDockerfile(parsed.variant, this.cwd); | ||||||
|  |     const imageName = `tstest-${parsed.variant}`; | ||||||
|  |  | ||||||
|  |     // Build the Docker image | ||||||
|  |     await this.buildDockerImage(dockerfilePath, imageName); | ||||||
|  |  | ||||||
|  |     // Prepare the test file path relative to the mounted directory | ||||||
|  |     // We need to get the path relative to cwd | ||||||
|  |     const absoluteTestPath = plugins.path.isAbsolute(testFile) | ||||||
|  |       ? testFile | ||||||
|  |       : plugins.path.join(this.cwd, testFile); | ||||||
|  |  | ||||||
|  |     const relativeTestPath = plugins.path.relative(this.cwd, absoluteTestPath); | ||||||
|  |  | ||||||
|  |     // Create TAP parser | ||||||
|  |     const tapParser = new TapParser(testFile + ':docker', this.logger); | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       // Build docker run command | ||||||
|  |       const dockerArgs = [ | ||||||
|  |         'run', | ||||||
|  |         '--rm', | ||||||
|  |         '-v', | ||||||
|  |         `${this.cwd}/test:/test`, | ||||||
|  |         imageName, | ||||||
|  |         'taprun', | ||||||
|  |         `/test/${plugins.path.basename(testFile)}` | ||||||
|  |       ]; | ||||||
|  |  | ||||||
|  |       this.logger.tapOutput(`Executing: docker ${dockerArgs.join(' ')}`); | ||||||
|  |  | ||||||
|  |       // Execute the Docker container | ||||||
|  |       const execPromise = this.smartshellInstance.execStreaming( | ||||||
|  |         `docker ${dockerArgs.join(' ')}`, | ||||||
|  |         { | ||||||
|  |           cwd: this.cwd, | ||||||
|  |         } | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       // Set up timeout if configured | ||||||
|  |       let timeoutHandle: NodeJS.Timeout | null = null; | ||||||
|  |       if (this.timeoutSeconds) { | ||||||
|  |         timeoutHandle = setTimeout(() => { | ||||||
|  |           this.logger.tapOutput(`⏱️  Test timeout (${this.timeoutSeconds}s) - killing container`); | ||||||
|  |           // Try to kill any running containers with this image | ||||||
|  |           this.smartshellInstance.exec(`docker ps -q --filter ancestor=${imageName} | xargs -r docker kill`); | ||||||
|  |         }, this.timeoutSeconds * 1000); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Stream output to TAP parser line by line | ||||||
|  |       execPromise.childProcess.stdout.on('data', (data: Buffer) => { | ||||||
|  |         const output = data.toString(); | ||||||
|  |         const lines = output.split('\n'); | ||||||
|  |         for (const line of lines) { | ||||||
|  |           if (line.trim()) { | ||||||
|  |             tapParser.handleTapLog(line); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       execPromise.childProcess.stderr.on('data', (data: Buffer) => { | ||||||
|  |         const output = data.toString(); | ||||||
|  |         this.logger.tapOutput(cs(`[stderr] ${output}`, 'orange')); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       // Wait for completion | ||||||
|  |       const result = await execPromise; | ||||||
|  |  | ||||||
|  |       // Clear timeout | ||||||
|  |       if (timeoutHandle) { | ||||||
|  |         clearTimeout(timeoutHandle); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (result.exitCode !== 0) { | ||||||
|  |         this.logger.tapOutput(cs(`❌ Docker test failed with exit code ${result.exitCode}`, 'red')); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       // Evaluate final result | ||||||
|  |       await tapParser.evaluateFinalResult(); | ||||||
|  |  | ||||||
|  |     } catch (error) { | ||||||
|  |       this.logger.tapOutput(cs(`❌ Error running Docker test: ${error.message}`, 'red')); | ||||||
|  |       // Add a failing test result to the parser | ||||||
|  |       tapParser.handleTapLog('not ok 1 - Docker test execution failed'); | ||||||
|  |       await tapParser.evaluateFinalResult(); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return tapParser; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Clean up built Docker images (optional, can be called at end of test suite) | ||||||
|  |    */ | ||||||
|  |   async cleanup(): Promise<void> { | ||||||
|  |     for (const imageName of this.builtImages) { | ||||||
|  |       try { | ||||||
|  |         this.logger.tapOutput(`Removing Docker image: ${imageName}`); | ||||||
|  |         await this.smartshellInstance.exec(`docker rmi ${imageName}`); | ||||||
|  |       } catch (error) { | ||||||
|  |         // Ignore cleanup errors | ||||||
|  |         this.logger.tapOutput(cs(`Warning: Failed to remove image ${imageName}: ${error.message}`, 'orange')); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     this.builtImages.clear(); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -29,7 +29,7 @@ export interface ParserConfig { | |||||||
|  |  | ||||||
| const KNOWN_RUNTIMES: Set<string> = new Set(['node', 'chromium', 'deno', 'bun']); | const KNOWN_RUNTIMES: Set<string> = new Set(['node', 'chromium', 'deno', 'bun']); | ||||||
| const KNOWN_MODIFIERS: Set<string> = new Set(['nonci']); | const KNOWN_MODIFIERS: Set<string> = new Set(['nonci']); | ||||||
| const VALID_EXTENSIONS: Set<string> = new Set(['ts', 'tsx', 'mts', 'cts']); | const VALID_EXTENSIONS: Set<string> = new Set(['ts', 'tsx', 'mts', 'cts', 'sh']); | ||||||
| const ALL_RUNTIMES: Runtime[] = ['node', 'chromium', 'deno', 'bun']; | const ALL_RUNTIMES: Runtime[] = ['node', 'chromium', 'deno', 'bun']; | ||||||
|  |  | ||||||
| // Legacy mappings for backwards compatibility | // Legacy mappings for backwards compatibility | ||||||
| @@ -228,3 +228,81 @@ export function getLegacyMigrationTarget(fileName: string): string | null { | |||||||
|  |  | ||||||
|   return parts.join('.'); |   return parts.join('.'); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Docker test file information | ||||||
|  |  */ | ||||||
|  | export interface DockerTestFileInfo { | ||||||
|  |   baseName: string; | ||||||
|  |   variant: string; | ||||||
|  |   isDockerTest: true; | ||||||
|  |   original: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Check if a filename matches the Docker test pattern: *.{variant}.docker.sh | ||||||
|  |  * Examples: test.latest.docker.sh, test.integration.npmci.docker.sh | ||||||
|  |  */ | ||||||
|  | export function isDockerTestFile(fileName: string): boolean { | ||||||
|  |   // Must end with .docker.sh | ||||||
|  |   if (!fileName.endsWith('.docker.sh')) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Extract filename from path if needed | ||||||
|  |   const name = fileName.split('/').pop() || fileName; | ||||||
|  |  | ||||||
|  |   // Must have at least 3 parts: [baseName, variant, docker, sh] | ||||||
|  |   const parts = name.split('.'); | ||||||
|  |   return parts.length >= 4 && parts[parts.length - 2] === 'docker' && parts[parts.length - 1] === 'sh'; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Parse a Docker test filename to extract variant and base name | ||||||
|  |  * Pattern: test.{baseName}.{variant}.docker.sh | ||||||
|  |  * Examples: | ||||||
|  |  *   - test.latest.docker.sh -> { baseName: 'test', variant: 'latest' } | ||||||
|  |  *   - test.integration.npmci.docker.sh -> { baseName: 'test.integration', variant: 'npmci' } | ||||||
|  |  */ | ||||||
|  | export function parseDockerTestFilename(filePath: string): DockerTestFileInfo { | ||||||
|  |   // Extract just the filename from the path | ||||||
|  |   const fileName = filePath.split('/').pop() || filePath; | ||||||
|  |   const original = fileName; | ||||||
|  |  | ||||||
|  |   if (!isDockerTestFile(fileName)) { | ||||||
|  |     throw new Error(`Not a valid Docker test file: "${fileName}". Expected pattern: *.{variant}.docker.sh`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Remove .docker.sh suffix | ||||||
|  |   const withoutSuffix = fileName.slice(0, -10); // Remove '.docker.sh' | ||||||
|  |   const tokens = withoutSuffix.split('.'); | ||||||
|  |  | ||||||
|  |   if (tokens.length === 0) { | ||||||
|  |     throw new Error(`Invalid Docker test file: empty basename in "${fileName}"`); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Last token before .docker.sh is the variant | ||||||
|  |   const variant = tokens[tokens.length - 1]; | ||||||
|  |  | ||||||
|  |   // Everything else is the base name | ||||||
|  |   const baseName = tokens.slice(0, -1).join('.'); | ||||||
|  |  | ||||||
|  |   return { | ||||||
|  |     baseName: baseName || 'test', | ||||||
|  |     variant, | ||||||
|  |     isDockerTest: true, | ||||||
|  |     original, | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Map a Docker variant to its corresponding Dockerfile path | ||||||
|  |  * "latest" -> "Dockerfile" | ||||||
|  |  * Other variants -> "Dockerfile_{variant}" | ||||||
|  |  */ | ||||||
|  | export function mapVariantToDockerfile(variant: string, baseDir: string): string { | ||||||
|  |   if (variant === 'latest') { | ||||||
|  |     return `${baseDir}/Dockerfile`; | ||||||
|  |   } | ||||||
|  |   return `${baseDir}/Dockerfile_${variant}`; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -74,12 +74,20 @@ export class TestDirectory { | |||||||
|       case TestExecutionMode.DIRECTORY: |       case TestExecutionMode.DIRECTORY: | ||||||
|         // Directory mode - now recursive with ** pattern |         // Directory mode - now recursive with ** pattern | ||||||
|         const dirPath = plugins.path.join(this.cwd, this.testPath); |         const dirPath = plugins.path.join(this.cwd, this.testPath); | ||||||
|         const testPattern = '**/test*.ts'; |  | ||||||
|          |         // Search for both TypeScript test files and Docker shell test files | ||||||
|         const testFiles = await plugins.smartfile.fs.listFileTree(dirPath, testPattern); |         const tsPattern = '**/test*.ts'; | ||||||
|          |         const dockerPattern = '**/*.docker.sh'; | ||||||
|  |  | ||||||
|  |         const [tsFiles, dockerFiles] = await Promise.all([ | ||||||
|  |           plugins.smartfile.fs.listFileTree(dirPath, tsPattern), | ||||||
|  |           plugins.smartfile.fs.listFileTree(dirPath, dockerPattern), | ||||||
|  |         ]); | ||||||
|  |  | ||||||
|  |         const allTestFiles = [...tsFiles, ...dockerFiles]; | ||||||
|  |  | ||||||
|         this.testfileArray = await Promise.all( |         this.testfileArray = await Promise.all( | ||||||
|           testFiles.map(async (filePath) => { |           allTestFiles.map(async (filePath) => { | ||||||
|             const absolutePath = plugins.path.isAbsolute(filePath) |             const absolutePath = plugins.path.isAbsolute(filePath) | ||||||
|               ? filePath |               ? filePath | ||||||
|               : plugins.path.join(dirPath, filePath); |               : plugins.path.join(dirPath, filePath); | ||||||
|   | |||||||
| @@ -11,12 +11,13 @@ import { TsTestLogger } from './tstest.logging.js'; | |||||||
| import type { LogOptions } from './tstest.logging.js'; | import type { LogOptions } from './tstest.logging.js'; | ||||||
|  |  | ||||||
| // Runtime adapters | // Runtime adapters | ||||||
| import { parseTestFilename } from './tstest.classes.runtime.parser.js'; | import { parseTestFilename, isDockerTestFile, parseDockerTestFilename } from './tstest.classes.runtime.parser.js'; | ||||||
| import { RuntimeAdapterRegistry } from './tstest.classes.runtime.adapter.js'; | import { RuntimeAdapterRegistry } from './tstest.classes.runtime.adapter.js'; | ||||||
| import { NodeRuntimeAdapter } from './tstest.classes.runtime.node.js'; | import { NodeRuntimeAdapter } from './tstest.classes.runtime.node.js'; | ||||||
| import { ChromiumRuntimeAdapter } from './tstest.classes.runtime.chromium.js'; | import { ChromiumRuntimeAdapter } from './tstest.classes.runtime.chromium.js'; | ||||||
| import { DenoRuntimeAdapter } from './tstest.classes.runtime.deno.js'; | 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'; | ||||||
|  |  | ||||||
| export class TsTest { | export class TsTest { | ||||||
|   public testDir: TestDirectory; |   public testDir: TestDirectory; | ||||||
| @@ -37,6 +38,7 @@ export class TsTest { | |||||||
|   public tsbundleInstance = new plugins.tsbundle.TsBundle(); |   public tsbundleInstance = new plugins.tsbundle.TsBundle(); | ||||||
|  |  | ||||||
|   public runtimeRegistry = new RuntimeAdapterRegistry(); |   public runtimeRegistry = new RuntimeAdapterRegistry(); | ||||||
|  |   public dockerAdapter: DockerRuntimeAdapter | 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; | ||||||
| @@ -60,6 +62,14 @@ export class TsTest { | |||||||
|     this.runtimeRegistry.register( |     this.runtimeRegistry.register( | ||||||
|       new BunRuntimeAdapter(this.logger, this.smartshellInstance, this.timeoutSeconds, this.filterTags) |       new BunRuntimeAdapter(this.logger, this.smartshellInstance, this.timeoutSeconds, this.filterTags) | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|  |     // Initialize Docker adapter | ||||||
|  |     this.dockerAdapter = new DockerRuntimeAdapter( | ||||||
|  |       this.logger, | ||||||
|  |       this.smartshellInstance, | ||||||
|  |       this.timeoutSeconds, | ||||||
|  |       cwdArg | ||||||
|  |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
| @@ -211,8 +221,14 @@ export class TsTest { | |||||||
|   } |   } | ||||||
|    |    | ||||||
|   private async runSingleTest(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) { |   private async runSingleTest(fileNameArg: string, fileIndex: number, totalFiles: number, tapCombinator: TapCombinator) { | ||||||
|     // Parse the filename to determine runtimes and modifiers |  | ||||||
|     const fileName = plugins.path.basename(fileNameArg); |     const fileName = plugins.path.basename(fileNameArg); | ||||||
|  |  | ||||||
|  |     // Check if this is a Docker test file | ||||||
|  |     if (isDockerTestFile(fileName)) { | ||||||
|  |       return await this.runDockerTest(fileNameArg, fileIndex, totalFiles, tapCombinator); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Parse the filename to determine runtimes and modifiers (for TypeScript tests) | ||||||
|     const parsed = parseTestFilename(fileName, { strictUnknownRuntime: false }); |     const parsed = parseTestFilename(fileName, { strictUnknownRuntime: false }); | ||||||
|  |  | ||||||
|     // Check for nonci modifier in CI environment |     // Check for nonci modifier in CI environment | ||||||
| @@ -258,6 +274,28 @@ export class TsTest { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Execute a Docker test file | ||||||
|  |    */ | ||||||
|  |   private async runDockerTest( | ||||||
|  |     fileNameArg: string, | ||||||
|  |     fileIndex: number, | ||||||
|  |     totalFiles: number, | ||||||
|  |     tapCombinator: TapCombinator | ||||||
|  |   ): Promise<void> { | ||||||
|  |     if (!this.dockerAdapter) { | ||||||
|  |       this.logger.tapOutput(cs('❌ Docker adapter not initialized', 'red')); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     try { | ||||||
|  |       const tapParser = await this.dockerAdapter.run(fileNameArg, fileIndex, totalFiles); | ||||||
|  |       tapCombinator.addTapParser(tapParser); | ||||||
|  |     } catch (error) { | ||||||
|  |       this.logger.tapOutput(cs(`❌ Docker test failed: ${error.message}`, 'red')); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public async runInNode(fileNameArg: string, index: number, total: number): Promise<TapParser> { |   public async runInNode(fileNameArg: string, index: number, total: number): Promise<TapParser> { | ||||||
|     this.logger.testFileStart(fileNameArg, 'node.js', index, total); |     this.logger.testFileStart(fileNameArg, 'node.js', index, total); | ||||||
|     const tapParser = new TapParser(fileNameArg + ':node', this.logger); |     const tapParser = new TapParser(fileNameArg + ':node', this.logger); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user