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
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"displayName": "PHPUnit Test Explorer",
"icon": "img/icon.png",
"publisher": "recca0120",
"version": "3.7.4",
"version": "3.7.5",
"private": true,
"license": "MIT",
"repository": {
Expand Down Expand Up @@ -192,7 +192,7 @@
"@vscode/vsce": "^3.3.2",
"chai": "^5.2.0",
"eslint": "^9.26.0",
"fast-xml-parser": "^5.2.2",
"fast-xml-parser": "^5.2.3",
"glob": "^11.0.2",
"minimatch": "^10.0.1",
"mocha": "^11.2.2",
Expand Down
32 changes: 29 additions & 3 deletions src/PHPUnit/ProblemMatcher/PHPUnitProblemMatcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,33 @@ describe('PHPUnit ProblemMatcher Text', () => {
);
});

fit('fix PHPUnit 10 without details', () => {
it('fix PHPUnit10 testFailed', () => {
const contents = [
`##teamcity[testSuiteStarted name='App\\Tests\\Ecommerce\\Offer\\Synchronizer\\PriceSynchronizerTest' locationHint='php_qn:///srv/app/tests/Ecommerce/Offer/Synchronizer/PriceSynchronizerTest.php::\\App\\Tests\\Ecommerce\\Offer\\Synchronizer\\PriceSynchronizerTest' flowId='5161']`,
`##teamcity[testFailed name='testProductNeedUpdateReturnsFalseWhenPriceSyncNotEnabled' message='Error: Class "App\\Ecommerce\\Offer\\Synchronizer\\PriceSynchronizer" not found' details='/srv/app/tests/Ecommerce/Offer/Synchronizer/PriceSynchronizerTest.php:28|n' duration='0' flowId='5161']`,
`##teamcity[testSuiteFinished name='App\\Tests\\Ecommerce\\Offer\\Synchronizer\\PriceSynchronizerTest' flowId='5161']`,
];

problemMatcher.parse(contents[0]);
expect(problemMatcher.parse(contents[1])).toEqual(
expect.objectContaining({
event: TeamcityEvent.testFailed,
name: 'testProductNeedUpdateReturnsFalseWhenPriceSyncNotEnabled',
// locationHint: 'php_qn:///srv/app/tests/Ecommerce/Offer/Synchronizer/PriceSynchronizerTest.php::\\App\\Tests\\Ecommerce\\Offer\\Synchronizer\\PriceSynchronizerTest::testProductNeedUpdateReturnsFalseWhenPriceSyncNotEnabled',
flowId: 5161,
id: '/srv/app/tests/Ecommerce/Offer/Synchronizer/PriceSynchronizerTest.php::testProductNeedUpdateReturnsFalseWhenPriceSyncNotEnabled',
file: '/srv/app/tests/Ecommerce/Offer/Synchronizer/PriceSynchronizerTest.php',
message: 'Error: Class "App\\Ecommerce\\Offer\\Synchronizer\\PriceSynchronizer" not found',
details: [{
file: '/srv/app/tests/Ecommerce/Offer/Synchronizer/PriceSynchronizerTest.php',
line: 28,
}],
duration: 0,
}),
);
});

it('fix PHPUnit10 testIgnored', () => {
const contents = [
`##teamcity[testSuiteStarted name='Tests\\Feature\\ChatControllerTest' locationHint='php_qn:///var/www/html/tests/Feature/ChatControllerTest.php::\\Tests\\Feature\\ChatControllerTest' flowId='22946']`,
`##teamcity[testIgnored name='test_permission' message='ChatControllerTest uses PlayerService' duration='0' flowId='22946']`,
Expand All @@ -251,7 +277,7 @@ describe('PHPUnit ProblemMatcher Text', () => {
expect.objectContaining({
event: TeamcityEvent.testIgnored,
name: 'test_permission',
locationHint: 'php_qn:///var/www/html/tests/Feature/ChatControllerTest.php::test_permission',
locationHint: 'php_qn:///var/www/html/tests/Feature/ChatControllerTest.php::\\Tests\\Feature\\ChatControllerTest::test_permission',
flowId: 22946,
id: '/var/www/html/tests/Feature/ChatControllerTest.php::test_permission',
file: '/var/www/html/tests/Feature/ChatControllerTest.php',
Expand All @@ -268,7 +294,7 @@ describe('PHPUnit ProblemMatcher Text', () => {
expect.objectContaining({
event: TeamcityEvent.testIgnored,
name: 'test_grant_chat_token',
locationHint: 'php_qn:///var/www/html/tests/Feature/ChatControllerTest.php::test_grant_chat_token',
locationHint: 'php_qn:///var/www/html/tests/Feature/ChatControllerTest.php::\\Tests\\Feature\\ChatControllerTest::test_grant_chat_token',
flowId: 22946,
id: '/var/www/html/tests/Feature/ChatControllerTest.php::test_grant_chat_token',
file: '/var/www/html/tests/Feature/ChatControllerTest.php',
Expand Down
121 changes: 121 additions & 0 deletions src/PHPUnit/Transformer/Fixer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { TeamcityEvent, TestResult, TestStarted, TestSuiteStarted } from '../ProblemMatcher';
import { capitalize } from '../utils';

export class PestV1Fixer {
static fixLocationHint(locationHint: string) {
return this.fixDataSet(/^tests\//.test(locationHint) ? locationHint : locationHint.substring(locationHint.lastIndexOf('tests/')));
}

static fixFlowId(results = new Map<string, TestResult>(), testResult?: TestResult) {
if (!testResult) {
return testResult;
}

const events = [TeamcityEvent.testStarted, TeamcityEvent.testFailed, TeamcityEvent.testIgnored];
if ('event' in testResult && !events.includes(testResult.event) || (testResult as any).flowId) {
return testResult;
}

const result = Array.from(results.values()).reverse().find((result: TestResult) => {
if (testResult.event !== TeamcityEvent.testStarted) {
return result.event === TeamcityEvent.testStarted && (result as any).name === (testResult as any).name;
}

const matched = (testResult as any).id?.match(/\((?<id>.+)\)/);

return matched && (result as any).id === matched.groups?.id.replace(/\\/g, '/') + 'Test';
});

(testResult as any).flowId = (result as any)?.flowId;

return testResult;
}

private static fixDataSet(locationHint: string) {
const matched = locationHint.match(/(?<description>.+)\swith\s\('(?<data>.+)'\)/);

return matched && matched.groups?.description
? `${matched.groups.description} with data set "(\'${matched.groups.data}\')"`
: locationHint;
}
}

export class Str {
static prefix = '__pest_evaluable_';

static evaluable(code: string) {
return this.prefix + code.replace(/_/g, '__').replace(/\s/g, '_').replace(/[^a-zA-Z0-9_\u0080-\uFFFF]/g, '_');
}
}

export class PestV2Fixer {
static fixId(location: string, name: string) {
return this.hasPrefix(name) ? name : location;
}

static isEqualsPestV2DataSetId(result: TestResult, testItemId: string) {
if (!('id' in result) || !this.hasPrefix(result.id)) {
return false;
}

let [classFQN, method] = testItemId.split('::');
classFQN = capitalize(classFQN.replace(/\//g, '\\').replace(/\.php$/, ''));

return [classFQN, this.methodName(method)].join('::') === result.id;
}

private static hasPrefix(id?: string) {
return id && new RegExp(Str.prefix).test(id);
}

static methodName(methodName: string) {
methodName = methodName.replace(/\{@\*}/g, '*/');
const matched = methodName.match(/(?<method>.*)\swith\sdata\sset\s(?<dataset>.+)/);
let dataset = '';
if (matched) {
methodName = matched.groups!.method;
dataset = matched.groups!.dataset.replace(/\|'/g, '\'');
}

return Str.evaluable(methodName) + dataset;
}
}

export class PHPUnitFixer {
static fixDetails(results = new Map<string, TestResult>(), testResult: TestResult & {
name: string,
locationHint?: string,
file?: string,
details?: Array<{ file: string, line: number }>,
}) {
if (testResult.details && testResult.file) {
return testResult;
}

Check warning on line 93 in src/PHPUnit/Transformer/Fixer.ts

View check run for this annotation

Codecov / codecov/patch

src/PHPUnit/Transformer/Fixer.ts#L92-L93

Added lines #L92 - L93 were not covered by tests

const result = Array.from(results.values()).reverse().find((result) => {
return [TeamcityEvent.testSuiteStarted, TeamcityEvent.testStarted].includes(result.event);
}) as (TestSuiteStarted | TestStarted | undefined);

if (!result) {
return testResult;
}

Check warning on line 101 in src/PHPUnit/Transformer/Fixer.ts

View check run for this annotation

Codecov / codecov/patch

src/PHPUnit/Transformer/Fixer.ts#L100-L101

Added lines #L100 - L101 were not covered by tests

const file = result.file!;
if (!testResult.file) {
testResult.file = file;
}

if (!testResult.details) {
testResult.details = [{ file: file, line: 1 }];
}

if (!testResult.locationHint) {
const locationHint = result.locationHint?.split('::').slice(0, 2).join('::');
testResult.locationHint = [locationHint, testResult.name]
.filter(value => !!value)
.join('::');
}

return testResult;
}
}
40 changes: 0 additions & 40 deletions src/PHPUnit/Transformer/PHPUnitTransformer.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,7 @@
import { TeamcityEvent, TestResult, TestStarted, TestSuiteStarted } from '../ProblemMatcher';
import { TestDefinition, TestType } from '../types';
import { capitalize, snakeCase, titleCase } from '../utils';
import { Transformer } from './Transformer';

export class PHPUnitFixer {
static fixDetails(results = new Map<string, TestResult>(), testResult: TestResult & {
name: string,
locationHint?: string,
file?: string,
details?: Array<{ file: string, line: number }>,
}) {
if (testResult.details && testResult.file) {
return testResult;
}

const result = Array.from(results.values()).reverse().find((result) => {
return [TeamcityEvent.testSuiteStarted, TeamcityEvent.testStarted].includes(result.event);
}) as (TestSuiteStarted | TestStarted | undefined);

if (!result) {
return testResult;
}

const file = result.file!;
if (!testResult.file) {
testResult.file = file;
}

if (!testResult.details) {
testResult.details = [{ file: file, line: 1 }];
}

if (!testResult.locationHint) {
const locationHint = result.locationHint?.split('::').slice(0, 1).join('::');
testResult.locationHint = [locationHint, testResult.name]
.filter(value => !!value)
.join('::');
}

return testResult;
}
}

export class PHPUnitTransformer extends Transformer {
uniqueId(testDefinition: Pick<TestDefinition, 'type' | 'classFQN' | 'methodName' | 'annotations'>): string {
let { type, classFQN } = testDefinition;
Expand Down
3 changes: 2 additions & 1 deletion src/PHPUnit/Transformer/PestTransFormer.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TestType } from '../types';
import { PestTransformer, PestV2Fixer } from './PestTransformer';
import { PestV2Fixer } from './Fixer';
import { PestTransformer } from './PestTransformer';

describe('PestTransformer', () => {
const transformer = new PestTransformer();
Expand Down
85 changes: 2 additions & 83 deletions src/PHPUnit/Transformer/PestTransformer.ts
Original file line number Diff line number Diff line change
@@ -1,90 +1,9 @@
import { TeamcityEvent, TestResult } from '../ProblemMatcher';
import { TestDefinition, TestType } from '../types';
import { capitalize, uncapitalize } from '../utils';
import { uncapitalize } from '../utils';
import { PestV1Fixer, PestV2Fixer } from './Fixer';
import { PHPUnitTransformer } from './PHPUnitTransformer';
import { TransformerFactory } from './TransformerFactory';


export class Str {
static prefix = '__pest_evaluable_';

static evaluable(code: string) {
return this.prefix + code.replace(/_/g, '__').replace(/\s/g, '_').replace(/[^a-zA-Z0-9_\u0080-\uFFFF]/g, '_');
}
}

export class PestV1Fixer {
static fixLocationHint(locationHint: string) {
return this.fixDataSet(/^tests\//.test(locationHint) ? locationHint : locationHint.substring(locationHint.lastIndexOf('tests/')));
}

static fixFlowId(results = new Map<string, TestResult>(), testResult?: TestResult) {
if (!testResult) {
return testResult;
}

const events = [TeamcityEvent.testStarted, TeamcityEvent.testFailed, TeamcityEvent.testIgnored];
if ('event' in testResult && !events.includes(testResult.event) || (testResult as any).flowId) {
return testResult;
}

const result = Array.from(results.values()).reverse().find((result: TestResult) => {
if (testResult.event !== TeamcityEvent.testStarted) {
return result.event === TeamcityEvent.testStarted && (result as any).name === (testResult as any).name;
}

const matched = (testResult as any).id?.match(/\((?<id>.+)\)/);

return matched && (result as any).id === matched.groups?.id.replace(/\\/g, '/') + 'Test';
});

(testResult as any).flowId = (result as any)?.flowId;

return testResult;
}

private static fixDataSet(locationHint: string) {
const matched = locationHint.match(/(?<description>.+)\swith\s\('(?<data>.+)'\)/);

return matched && matched.groups?.description
? `${matched.groups.description} with data set "(\'${matched.groups.data}\')"`
: locationHint;
}
}

export class PestV2Fixer {
static fixId(location: string, name: string) {
return this.hasPrefix(name) ? name : location;
}

static isEqualsPestV2DataSetId(result: TestResult, testItemId: string) {
if (!('id' in result) || !this.hasPrefix(result.id)) {
return false;
}

let [classFQN, method] = testItemId.split('::');
classFQN = capitalize(classFQN.replace(/\//g, '\\').replace(/\.php$/, ''));

return [classFQN, this.methodName(method)].join('::') === result.id;
}

private static hasPrefix(id?: string) {
return id && new RegExp(Str.prefix).test(id);
}

static methodName(methodName: string) {
methodName = methodName.replace(/\{@\*}/g, '*/');
const matched = methodName.match(/(?<method>.*)\swith\sdata\sset\s(?<dataset>.+)/);
let dataset = '';
if (matched) {
methodName = matched.groups!.method;
dataset = matched.groups!.dataset.replace(/\|'/g, '\'');
}

return Str.evaluable(methodName) + dataset;
}
}

export class PestTransformer extends PHPUnitTransformer {
uniqueId(testDefinition: Pick<TestDefinition, 'type' | 'classFQN' | 'methodName' | 'annotations'>): string {
if (!TransformerFactory.isPest(testDefinition.classFQN!)) {
Expand Down
3 changes: 2 additions & 1 deletion src/PHPUnit/Transformer/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './Fixer';
export * from './TransformerFactory';
export * from './Transformer';
export * from './PHPUnitTransformer';
export * from './PestTransformer';
export * from './PestTransformer';