From cf2f7fdee0fc31bed6747c1db07b7a132a9fb023 Mon Sep 17 00:00:00 2001 From: Arun Krishnamurthy Date: Tue, 14 Apr 2026 09:26:38 -0500 Subject: [PATCH] fix: use @argfile for classpath, return java.exe on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.2.0 failed silently because: 1. resolveJavaExecutable() returned bare 'java' — cross-spawn wrapped in cmd.exe which has an 8K char limit, exceeded by the classpath 2. Even without cmd.exe, backslashes in Windows paths are treated as escapes by Java's argument parser Fix: write classpath to an argfile with forward slashes (same as IntelliJ's @idea_arg_file), return java.exe on Windows to avoid cmd.exe wrapping, and catch execution errors so they appear on test items instead of silently disappearing. --- package.json | 2 +- src/execution/mavenRunner.ts | 22 +++++++++++++++------- src/execution/testExecutor.ts | 30 +++++++++++++++++++----------- src/test/unit/mavenRunner.test.ts | 19 +++++++++++++------ 4 files changed, 48 insertions(+), 25 deletions(-) diff --git a/package.json b/package.json index ed7caf8..799b25d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "cucumber-java-test-runner", "displayName": "Cucumber Test Runner for Java", "description": "Run and debug Cucumber BDD scenarios from VS Code's native Test Explorer. Integrates with Maven and cucumber-junit-platform-engine.", - "version": "0.2.0", + "version": "0.2.1", "publisher": "arunkris", "license": "MIT", "repository": { diff --git a/src/execution/mavenRunner.ts b/src/execution/mavenRunner.ts index 5f78265..cd580bd 100644 --- a/src/execution/mavenRunner.ts +++ b/src/execution/mavenRunner.ts @@ -6,6 +6,7 @@ import * as config from '../config/configuration'; const RESULTS_FILENAME = 'cucumber-vscode-results.json'; const CLASSPATH_FILENAME = 'cp.txt'; +const CLI_ARGFILE = 'cucumber-cli-cp.txt'; const JUNIT_PLATFORM_PROPERTIES = 'junit-platform.properties'; export class MavenRunner implements BuildToolRunner { @@ -127,10 +128,12 @@ export class MavenRunner implements BuildToolRunner { assembleCucumberCliCommand(options: RunOptions): CommandSpec { const java = this.resolveJavaExecutable(); - const classpath = this.resolveTestClasspath(options.projectRoot); const resultsPath = this.getResultsFilePath(options.projectRoot); - const args: string[] = ['-cp', classpath, 'io.cucumber.core.cli.Main']; + // Write classpath to an argfile to avoid Windows' 32K command line limit. + // Use forward slashes — Java's argfile parser treats backslashes as escapes. + const argFilePath = this.writeClasspathArgFile(options.projectRoot); + const args: string[] = [`@${argFilePath.replace(/\\/g, '/')}`, 'io.cucumber.core.cli.Main']; args.push('--plugin', `json:${resultsPath.replace(/\\/g, '/')}`); @@ -189,21 +192,26 @@ export class MavenRunner implements BuildToolRunner { private resolveJavaExecutable(): string { const javaHome = process.env.JAVA_HOME; + const exe = process.platform === 'win32' ? 'java.exe' : 'java'; if (javaHome) { - const javaBin = path.join(javaHome, 'bin', 'java'); - if (fs.existsSync(javaBin) || fs.existsSync(javaBin + '.exe')) { + const javaBin = path.join(javaHome, 'bin', exe); + if (fs.existsSync(javaBin)) { return javaBin; } } - return 'java'; + return exe; } - private resolveTestClasspath(projectRoot: string): string { + private writeClasspathArgFile(projectRoot: string): string { const cpFile = path.join(projectRoot, 'target', CLASSPATH_FILENAME); const deps = fs.readFileSync(cpFile, 'utf-8').trim(); const sep = process.platform === 'win32' ? ';' : ':'; const testClasses = path.join(projectRoot, 'target', 'test-classes'); const classes = path.join(projectRoot, 'target', 'classes'); - return [testClasses, classes, deps].filter(Boolean).join(sep); + const classpath = [testClasses, classes, deps].filter(Boolean).join(sep); + + const argFile = path.join(projectRoot, 'target', CLI_ARGFILE); + fs.writeFileSync(argFile, `-cp\n${classpath.replace(/\\/g, '/')}\n`); + return argFile; } } diff --git a/src/execution/testExecutor.ts b/src/execution/testExecutor.ts index ed9bea0..5e8ea30 100644 --- a/src/execution/testExecutor.ts +++ b/src/execution/testExecutor.ts @@ -97,20 +97,28 @@ export class TestExecutor { const resultsPath = this.buildToolRunner.getResultsFilePath(projectRoot); this.deleteFileIfExists(resultsPath); - if (featureTargets.length > 0) { - // Specific scenarios: compile then run Cucumber CLI directly - if (debug) { - await this.executeCucumberCliDebug(runOptions, run, cancellation); + try { + if (featureTargets.length > 0) { + if (debug) { + await this.executeCucumberCliDebug(runOptions, run, cancellation); + } else { + await this.executeCucumberCli(runOptions, run, cancellation); + } } else { - await this.executeCucumberCli(runOptions, run, cancellation); + if (debug) { + await this.executeDebug(runOptions, projectRoot, run, cancellation); + } else { + await this.executeRun(runOptions, run, cancellation); + } } - } else { - // Run All: Maven test with runner class - if (debug) { - await this.executeDebug(runOptions, projectRoot, run, cancellation); - } else { - await this.executeRun(runOptions, run, cancellation); + } catch (execErr) { + const msg = execErr instanceof Error ? execErr.message : String(execErr); + this.logger.error('Execution failed', execErr); + for (const item of projectItems) { + run.errored(item, new vscode.TestMessage(`Execution failed: ${msg}`)); + reportedItems.add(item.id); } + continue; } if (!cancellation.isCancellationRequested) { diff --git a/src/test/unit/mavenRunner.test.ts b/src/test/unit/mavenRunner.test.ts index 2b71688..af3a791 100644 --- a/src/test/unit/mavenRunner.test.ts +++ b/src/test/unit/mavenRunner.test.ts @@ -407,7 +407,7 @@ describe('MavenRunner', () => { assert.equal(cmd.args[tagsIdx + 1], 'not @wip'); }); - it('builds classpath from cp.txt + target dirs', () => { + it('writes classpath argfile with target dirs and dependencies', () => { mkdirp(path.join(tmpDir, 'target')); fs.writeFileSync(path.join(tmpDir, 'target', 'cp.txt'), '/dep1.jar:/dep2.jar'); @@ -415,11 +415,18 @@ describe('MavenRunner', () => { projectRoot: tmpDir, featureTargets: ['f.feature:1'], }); - const cpIdx = cmd.args.indexOf('-cp'); - const cp = cmd.args[cpIdx + 1]; - assert.ok(cp.includes('test-classes'), 'Classpath should include target/test-classes'); - assert.ok(cp.includes('classes'), 'Classpath should include target/classes'); - assert.ok(cp.includes('dep1.jar'), 'Classpath should include dependencies'); + // First arg should be @argfile reference + assert.ok(cmd.args[0].startsWith('@'), 'First arg should be @argfile'); + assert.ok(cmd.args[0].includes('cucumber-cli-cp.txt')); + + // Read the argfile and verify classpath content + const argFilePath = cmd.args[0].substring(1); // strip @ + const argContent = fs.readFileSync(argFilePath, 'utf-8'); + assert.ok(argContent.startsWith('-cp\n'), 'Argfile should start with -cp'); + assert.ok(argContent.includes('test-classes'), 'Classpath should include target/test-classes'); + assert.ok(argContent.includes('classes'), 'Classpath should include target/classes'); + assert.ok(argContent.includes('dep1.jar'), 'Classpath should include dependencies'); + assert.ok(!argContent.includes('\\'), 'Argfile should use forward slashes'); }); });