From c480f77a37242f74e5627c08e493e4371099ebb0 Mon Sep 17 00:00:00 2001 From: Arun Krishnamurthy Date: Mon, 13 Apr 2026 22:04:28 -0500 Subject: [PATCH 1/2] fix: use Surefire engine filtering instead of -Dtest=! for scenario runs Replace -Dtest=!RunnerClass with -Dincludejunit5engines=cucumber when running specific scenarios. The old approach excluded the runner class but ran every other test class in the project. Engine filtering ensures only the Cucumber engine executes. Also read cucumber.glue from junit-platform.properties (or a new VS Code setting) so the engine can find step definitions without relying on @ConfigurationParameter annotations on the excluded runner class. --- package.json | 5 +++ src/config/configuration.ts | 5 +++ src/execution/mavenRunner.ts | 33 +++++++++++-------- src/test/unit/mavenRunner.test.ts | 55 +++++++++++++++++++++++++++++-- 4 files changed, 83 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index f8d3300..e094d2d 100644 --- a/package.json +++ b/package.json @@ -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 () => { From 844b7cb6e9b9c9497f6b87a42018ccf09bf23593 Mon Sep 17 00:00:00 2001 From: Arun Krishnamurthy Date: Mon, 13 Apr 2026 22:08:44 -0500 Subject: [PATCH 2/2] release: v0.1.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e094d2d..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": {