Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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\")"
}
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/config/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,8 @@ export function getDefaultTags(): string | undefined {
const value = vscode.workspace.getConfiguration(SECTION).get<string>('defaultTags', '');
return value || undefined;
}

export function getGlue(): string | undefined {
const value = vscode.workspace.getConfiguration(SECTION).get<string>('glue', '');
return value || undefined;
}
33 changes: 20 additions & 13 deletions src/execution/mavenRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -132,6 +134,11 @@ export class MavenRunner implements BuildToolRunner {
}

async readExistingPlugins(projectRoot: string): Promise<string[]> {
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',
Expand All @@ -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;
}
}
55 changes: 53 additions & 2 deletions src/test/unit/mavenRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down