Skip to content
Open
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
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@
{
"command": "phpunit.rerun",
"title": "PHPUnit: Repeat the last test run"
},
{
"command": "phpunit.run-by-group",
"title": "PHPUnit: Run tests by group"
}
],
"keybindings": [
Expand Down
24 changes: 23 additions & 1 deletion src/CommandHandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CancellationTokenSource, commands, TestItem, TestRunProfile, TestRunRequest, window } from 'vscode';
import { Handler } from './Handler';
import { TestCollection } from './TestCollection';
import { GroupRegistry, TestCollection } from './TestCollection';

export class CommandHandler {
constructor(private testCollection: TestCollection, private testRunProfile: TestRunProfile) {}
Expand Down Expand Up @@ -53,6 +53,28 @@ export class CommandHandler {
});
}

runByGroup(handler: Handler) {
return commands.registerCommand('phpunit.run-by-group', async () => {
const groups = GroupRegistry.getInstance().getAll();
if (groups.length === 0) {
window.showInformationMessage('No PHPUnit groups found. Add @group annotations or #[Group] attributes to your tests.');
return;
}

const selectedGroup = await window.showQuickPick(groups, {
placeHolder: 'Select a PHPUnit group to run',
title: 'Run Tests by Group',
});

if (!selectedGroup || !handler) {
return;
}

const cancellation = new CancellationTokenSource().token;
await handler.startGroupTestRun(selectedGroup, cancellation);
});
}

private async run(include: readonly TestItem[] | undefined) {
const cancellation = new CancellationTokenSource().token;

Expand Down
25 changes: 25 additions & 0 deletions src/Handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,31 @@ export class Handler {
this.previousRequest = request;
}

async startGroupTestRun(group: string, cancellation?: CancellationToken) {
const builder = new Builder(this.configuration, { cwd: this.phpUnitXML.root() });
builder.setArguments(`--group ${group}`);

const request = new TestRunRequest();
const testRun = this.ctrl.createTestRun(request);

const runner = new TestRunner();
const queue = await this.discoverTests(this.gatherTestItems(this.ctrl.items), request);
queue.forEach((testItem) => testRun.enqueued(testItem));

runner.observe(new TestResultObserver(queue, testRun));
runner.observe(new OutputChannelObserver(this.outputChannel, this.configuration, this.printer, request));
runner.observe(new MessageObserver(this.configuration));

runner.emit(TestRunnerEvent.start, undefined);

const process = runner.run(builder);
cancellation?.onCancellationRequested(() => process.abort());

await process.run();
runner.emit(TestRunnerEvent.done, undefined);
testRun.end();
}

private async runTestQueue(builder: Builder, testRun: TestRun, request: TestRunRequest, cancellation?: CancellationToken) {
const queue = await this.discoverTests(request.include ?? this.gatherTestItems(this.ctrl.items), request);
queue.forEach((testItem) => testRun.enqueued(testItem));
Expand Down
2 changes: 1 addition & 1 deletion src/PHPUnit/TestParser/AnnotationParser.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Declaration, Method } from 'php-parser';
import { Annotations } from '../types';

const lookup = ['depends', 'dataProvider', 'testdox'];
const lookup = ['depends', 'dataProvider', 'testdox', 'group'];

export class AttributeParser {
public parse(declaration: Declaration) {
Expand Down
78 changes: 78 additions & 0 deletions src/PHPUnit/TestParser/PHPUnitParser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,4 +489,82 @@ final class TestDoxTest extends TestCase {
depth: 2,
}));
});

it('parse @group annotation', () => {
const file = phpUnitProject('tests/GroupTest.php');
const content = `<?php declare(strict_types=1);

use PHPUnit\\Framework\\TestCase;

final class GroupTest extends TestCase {
/**
* @group integration
* @group slow
*/
public function test_with_groups() {
$this->assertTrue(true);
}
}
`;
expect(givenTest(file, content, 'test_with_groups')).toEqual(expect.objectContaining({
type: TestType.method,
file,
id: 'Group::With groups',
classFQN: 'GroupTest',
className: 'GroupTest',
methodName: 'test_with_groups',
annotations: { group: ['integration', 'slow'] },
depth: 2,
}));
});

it('parse #[Group] attribute', () => {
const file = phpUnitProject('tests/GroupAttributeTest.php');
const content = `<?php declare(strict_types=1);

use PHPUnit\\Framework\\TestCase;
use PHPUnit\\Framework\\Attributes\\Group;

final class GroupAttributeTest extends TestCase {
#[Group('plaid')]
#[Group('api')]
public function test_with_group_attributes() {
$this->assertTrue(true);
}
}
`;
expect(givenTest(file, content, 'test_with_group_attributes')).toEqual(expect.objectContaining({
type: TestType.method,
file,
id: 'Group Attribute::With group attributes',
classFQN: 'GroupAttributeTest',
className: 'GroupAttributeTest',
methodName: 'test_with_group_attributes',
annotations: { group: ['plaid', 'api'] },
depth: 2,
}));
});

it('parse single @group annotation', () => {
const file = phpUnitProject('tests/SingleGroupTest.php');
const content = `<?php declare(strict_types=1);

use PHPUnit\\Framework\\TestCase;

final class SingleGroupTest extends TestCase {
/**
* @group unit
*/
public function test_unit() {
$this->assertTrue(true);
}
}
`;
expect(givenTest(file, content, 'test_unit')).toEqual(expect.objectContaining({
type: TestType.method,
file,
methodName: 'test_unit',
annotations: { group: ['unit'] },
}));
});
});
1 change: 1 addition & 0 deletions src/PHPUnit/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type Annotations = {
depends?: string[];
dataProvider?: string[];
testdox?: string[];
group?: string[];
};
export type TestDefinition = {
type: TestType;
Expand Down
4 changes: 4 additions & 0 deletions src/TestCollection/TestCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export class TestCase {
return this.testDefinition.type;
}

get groups(): string[] {
return (this.testDefinition.annotations?.group as string[]) ?? [];
}

update(builder: Builder, index: number) {
return builder.clone()
.setXdebug(builder.getXdebug()?.clone().setIndex(index))
Expand Down
48 changes: 47 additions & 1 deletion src/TestCollection/TestHierarchyBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,35 @@
import { Position, Range, TestController, TestItem, Uri } from 'vscode';
import { Position, Range, TestController, TestItem, TestTag, Uri } from 'vscode';
import { CustomWeakMap, TestDefinition, TestParser, TestType, TransformerFactory } from '../PHPUnit';
import { TestCase } from './TestCase';

export class GroupRegistry {
private static instance: GroupRegistry;
private groups = new Set<string>();

static getInstance(): GroupRegistry {
if (!GroupRegistry.instance) {
GroupRegistry.instance = new GroupRegistry();
}
return GroupRegistry.instance;
}

add(group: string) {
this.groups.add(group);
}

addAll(groups: string[]) {
groups.forEach(g => this.groups.add(g));
}

getAll(): string[] {
return Array.from(this.groups).sort();
}

clear() {
this.groups.clear();
}
}

export class TestHierarchyBuilder {
private icons = {
[TestType.namespace]: '$(symbol-namespace)',
Expand Down Expand Up @@ -83,6 +111,18 @@ export class TestHierarchyBuilder {
const parent = this.ancestors[this.ancestors.length - 1];
parent.children.push(testItem);

// Inherit group tags from parent class to methods for proper filter inheritance
if (testDefinition.type === TestType.method && parent.type === TestType.class) {
const parentTags = (parent.item.tags ?? []).filter(t => t.id.startsWith('group:'));
if (parentTags.length > 0) {
const ownTags = testItem.tags ?? [];
testItem.tags = [
...ownTags,
...parentTags.filter(pt => !ownTags.some(ot => ot.id === pt.id)),
];
}
}

if (testDefinition.type !== TestType.method) {
this.ancestors.push({ item: testItem, type: testDefinition.type, children: [] });
}
Expand All @@ -96,6 +136,12 @@ export class TestHierarchyBuilder {
testItem.sortText = sortText;
testItem.range = this.createRange(testDefinition);

const groups = (testDefinition.annotations?.group as string[]) ?? [];
if (groups.length > 0) {
GroupRegistry.getInstance().addAll(groups);
testItem.tags = groups.map(g => new TestTag(`group:${g}`));
}

return testItem;
}

Expand Down
3 changes: 2 additions & 1 deletion src/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { Configuration } from './Configuration';
import { activate } from './extension';
import { getPhpUnitVersion, getPhpVersion, normalPath, pestProject, phpUnitProject } from './PHPUnit/__tests__/utils';

jest.mock('child_process');
//updated to match spawn for tests
jest.mock('node:child_process');

const setTextDocuments = (textDocuments: TextDocument[]) => {
Object.defineProperty(workspace, 'textDocuments', {
Expand Down
1 change: 1 addition & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export async function activate(context: ExtensionContext) {
context.subscriptions.push(commandHandler.runFile());
context.subscriptions.push(commandHandler.runTestAtCursor());
context.subscriptions.push(commandHandler.rerun(handler));
context.subscriptions.push(commandHandler.runByGroup(handler));
}

async function getWorkspaceTestPatterns() {
Expand Down
Loading