diff --git a/package.json b/package.json index f8d3300..41eef28 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.1.1", + "version": "0.1.2", "publisher": "arunkris", "license": "MIT", "repository": { @@ -67,6 +67,11 @@ "type": "string", "default": "", "description": "Default tag expression applied to all test runs (e.g., \"not @wip\")." + }, + "cucumberTestRunner.glue": { + "type": "string", + "default": "", + "description": "Cucumber glue (step definitions) package. Auto-detected from junit-platform.properties if empty. (e.g., \"com.example.steps\")" } } } diff --git a/src/config/configuration.ts b/src/config/configuration.ts index 160a6d2..6524c86 100644 --- a/src/config/configuration.ts +++ b/src/config/configuration.ts @@ -24,3 +24,8 @@ export function getDefaultTags(): string | undefined { const value = vscode.workspace.getConfiguration(SECTION).get('defaultTags', ''); return value || undefined; } + +export function getGlue(): string | undefined { + const value = vscode.workspace.getConfiguration(SECTION).get('glue', ''); + return value || undefined; +} diff --git a/src/execution/mavenRunner.ts b/src/execution/mavenRunner.ts index 1c6f96a..1d0584d 100644 --- a/src/execution/mavenRunner.ts +++ b/src/execution/mavenRunner.ts @@ -79,16 +79,18 @@ export class MavenRunner implements BuildToolRunner { const args: string[] = ['test']; if (options.featureTargets.length > 0) { - // Target specific features. The Cucumber JUnit Platform Engine discovers - // these directly via ServiceLoader using the cucumber.features property. args.push(`-Dcucumber.features=${options.featureTargets.join(',')}`); - // Exclude the @Suite runner class (e.g., CucumberTest) from Surefire's - // class scanning to prevent double execution. Without this, Surefire - // discovers the engine via ServiceLoader AND discovers the Suite class - // via classpath scanning, causing the same scenarios to run twice. - if (options.runnerClass) { - args.push(`-Dtest=!${options.runnerClass}`); + // Only run the Cucumber engine — prevents non-Cucumber tests from + // executing and avoids double execution through the Suite engine. + args.push('-Dincludejunit5engines=cucumber'); + + // Pass glue so the Cucumber engine finds step definitions without + // scanning the entire classpath. + const glue = config.getGlue() + ?? this.readJunitPlatformProperty(options.projectRoot, 'cucumber.glue'); + if (glue) { + args.push(`-Dcucumber.glue=${glue}`); } } else if (options.runnerClass) { // Running ALL tests — use the runner class to scope to Cucumber only. @@ -132,6 +134,11 @@ export class MavenRunner implements BuildToolRunner { } async readExistingPlugins(projectRoot: string): Promise { + const value = this.readJunitPlatformProperty(projectRoot, 'cucumber.plugin'); + return value ? value.split(/\s*,\s*/).filter(p => p.length > 0) : []; + } + + private readJunitPlatformProperty(projectRoot: string, key: string): string | undefined { const propsPath = path.join( projectRoot, 'src', 'test', 'resources', @@ -140,14 +147,14 @@ export class MavenRunner implements BuildToolRunner { try { const content = fs.readFileSync(propsPath, 'utf-8'); - const match = content.match(/^cucumber\.plugin\s*=\s*(.+)$/m); - if (match) { - return match[1].trim().split(/\s*,\s*/).filter(p => p.length > 0); - } + const escapedKey = key.replace(/\./g, '\\.'); + const regex = new RegExp(`^${escapedKey}\\s*=\\s*(.+)$`, 'm'); + const match = content.match(regex); + return match ? match[1].trim() : undefined; } catch { // File doesn't exist or can't be read } - return []; + return undefined; } } diff --git a/src/test/unit/mavenRunner.test.ts b/src/test/unit/mavenRunner.test.ts index 53df25b..01a9e0e 100644 --- a/src/test/unit/mavenRunner.test.ts +++ b/src/test/unit/mavenRunner.test.ts @@ -209,13 +209,64 @@ describe('MavenRunner', () => { assert.ok(cmd.args.includes('-Dtest=com.example.RunCucumber')); }); - it('excludes runner class when feature targets are provided (avoids double execution)', async () => { + it('uses engine filtering when feature targets are provided (avoids double execution)', async () => { const cmd = await runner.assembleCommand({ projectRoot: tmpDir, featureTargets: ['src/test/resources/login.feature:10'], runnerClass: 'CucumberTest', }); - assert.ok(cmd.args.includes('-Dtest=!CucumberTest')); + assert.ok(cmd.args.includes('-Dincludejunit5engines=cucumber'), + 'Should include engine filter'); + assert.ok(!cmd.args.some(a => a.startsWith('-Dtest=!')), + 'Should NOT use -Dtest=! exclusion'); + }); + + it('uses engine filtering even without runnerClass when feature targets are provided', async () => { + const cmd = await runner.assembleCommand({ + projectRoot: tmpDir, + featureTargets: ['src/test/resources/login.feature:10'], + }); + assert.ok(cmd.args.includes('-Dincludejunit5engines=cucumber'), + 'Should include engine filter even without runnerClass'); + assert.ok(!cmd.args.some(a => a.startsWith('-Dtest=')), + 'Should not have any -Dtest arg'); + }); + + it('reads cucumber.glue from junit-platform.properties when feature targets are provided', async () => { + const propsDir = path.join(tmpDir, 'src', 'test', 'resources'); + mkdirp(propsDir); + fs.writeFileSync( + path.join(propsDir, 'junit-platform.properties'), + 'cucumber.glue = com.example.steps\n', + ); + + const cmd = await runner.assembleCommand({ + projectRoot: tmpDir, + featureTargets: ['src/test/resources/login.feature:10'], + }); + assert.ok(cmd.args.includes('-Dcucumber.glue=com.example.steps'), + 'Should pass glue from properties file'); + }); + + it('omits -Dcucumber.glue when not configured anywhere', async () => { + const cmd = await runner.assembleCommand({ + projectRoot: tmpDir, + featureTargets: ['src/test/resources/login.feature:10'], + }); + assert.ok(!cmd.args.some(a => a.startsWith('-Dcucumber.glue=')), + 'Should not have glue arg when not configured'); + }); + + it('does not use engine filtering when featureTargets is empty (Run All)', async () => { + const cmd = await runner.assembleCommand({ + projectRoot: tmpDir, + featureTargets: [], + runnerClass: 'com.example.RunCucumber', + }); + assert.ok(!cmd.args.includes('-Dincludejunit5engines=cucumber'), + 'Should not include engine filter for Run All'); + assert.ok(cmd.args.includes('-Dtest=com.example.RunCucumber'), + 'Should use -Dtest for Run All'); }); it('omits -Dtest when runnerClass is not provided', async () => {