diff --git a/package.json b/package.json index 65aef6c5..d5c19e1c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "displayName": "PHPUnit Test Explorer", "icon": "img/icon.png", "publisher": "recca0120", - "version": "3.7.5", + "version": "3.7.6", "private": true, "license": "MIT", "repository": { diff --git a/src/PHPUnit/ProblemMatcher/PHPUnitProblemMatcher.test.ts b/src/PHPUnit/ProblemMatcher/PHPUnitProblemMatcher.test.ts index b95889ef..bdcb1480 100644 --- a/src/PHPUnit/ProblemMatcher/PHPUnitProblemMatcher.test.ts +++ b/src/PHPUnit/ProblemMatcher/PHPUnitProblemMatcher.test.ts @@ -248,9 +248,9 @@ describe('PHPUnit ProblemMatcher Text', () => { 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', + 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', + id: 'Price Synchronizer (App\\Tests\\Ecommerce\\Offer\\Synchronizer\\PriceSynchronizer)::Product need update returns false when price sync not enabled', file: '/srv/app/tests/Ecommerce/Offer/Synchronizer/PriceSynchronizerTest.php', message: 'Error: Class "App\\Ecommerce\\Offer\\Synchronizer\\PriceSynchronizer" not found', details: [{ @@ -279,13 +279,9 @@ describe('PHPUnit ProblemMatcher Text', () => { name: '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', + id: 'Chat Controller (Tests\\Feature\\ChatController)::Permission', file: '/var/www/html/tests/Feature/ChatControllerTest.php', message: 'ChatControllerTest uses PlayerService', - details: [{ - file: '/var/www/html/tests/Feature/ChatControllerTest.php', - line: 1, - }], duration: 0, }), ); @@ -296,13 +292,9 @@ describe('PHPUnit ProblemMatcher Text', () => { name: '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', + id: 'Chat Controller (Tests\\Feature\\ChatController)::Grant chat token', file: '/var/www/html/tests/Feature/ChatControllerTest.php', message: 'ChatControllerTest uses PlayerService', - details: [{ - file: '/var/www/html/tests/Feature/ChatControllerTest.php', - line: 1, - }], duration: 0, }), ); diff --git a/src/PHPUnit/ProblemMatcher/PestProblemMatcher.test.ts b/src/PHPUnit/ProblemMatcher/PestProblemMatcher.test.ts index 7bb04f21..e19b8375 100644 --- a/src/PHPUnit/ProblemMatcher/PestProblemMatcher.test.ts +++ b/src/PHPUnit/ProblemMatcher/PestProblemMatcher.test.ts @@ -754,4 +754,18 @@ describe('Pest ProblemMatcher Text', () => { it('testFinished without TestStarted', () => { resultShouldBe('##teamcity[testFinished name=\'`before each` → example\' duration=\'12\' flowId=\'97972\']', undefined); }); + + it('PHPUnit without TestStarted', () => { + resultShouldBe(`##teamcity[testSuiteStarted name='Tests\\Feature\\AuthenticationTest' locationHint='pest_qn://Authentication (Tests\\Feature\\Authentication)' flowId='6611']`, {}); + resultShouldBe(`##teamcity[testCount count='1' flowId='6611']`, {}); + resultShouldBe(`##teamcity[testIgnored name='Login screen can be rendered' message='This test was ignored.' details='' flowId='6611']`, { + event: TeamcityEvent.testIgnored, + flowId: 6611, + id: 'Authentication (Tests\\Feature\\Authentication)::Login screen can be rendered', + name: 'Login screen can be rendered', + message: 'This test was ignored.', + duration: 0, + }); + resultShouldBe(`##teamcity[testSuiteFinished name='Tests\\Feature\\AuthenticationTest' flowId='6611']`, {}); + }); }); diff --git a/src/PHPUnit/ProblemMatcher/ProblemMatcher.ts b/src/PHPUnit/ProblemMatcher/ProblemMatcher.ts index 2c4e6293..f7062081 100644 --- a/src/PHPUnit/ProblemMatcher/ProblemMatcher.ts +++ b/src/PHPUnit/ProblemMatcher/ProblemMatcher.ts @@ -2,10 +2,10 @@ import { TeamcityEvent, TestFailed, TestFinished, TestIgnored, TestResult, TestResultParser, TestStarted, TestSuiteFinished, TestSuiteStarted, } from '.'; -import { PestV1Fixer, PHPUnitFixer } from '../Transformer'; +import { PestFixer, PestV1Fixer, PHPUnitFixer } from '../Transformer'; export class ProblemMatcher { - private results = new Map(); + private cache = new Map(); private lookup: { [p: string]: (result: any) => TestResult | undefined } = { [TeamcityEvent.testSuiteStarted]: this.handleStarted, @@ -19,8 +19,8 @@ export class ProblemMatcher { constructor(private testResultParser: TestResultParser = new TestResultParser()) { } parse(input: string | Buffer): TestResult | undefined { - const result = this.testResultParser.parse(input.toString()); - PestV1Fixer.fixFlowId(this.results, result); + let result = this.testResultParser.parse(input.toString()); + result = PestV1Fixer.fixFlowId(this.cache, result); return this.isResult(result) ? this.lookup[result!.event]?.call(this, result) : result; } @@ -30,50 +30,50 @@ export class ProblemMatcher { } private handleStarted(testResult: TestSuiteStarted | TestStarted) { - const id = this.generateId(testResult); - this.results.set(id, testResult); + const cacheId = this.cacheId(testResult); + this.cache.set(cacheId, testResult); - return this.results.get(id); + return this.cache.get(cacheId); } private handleFault(testResult: TestFailed | TestIgnored): TestResult | undefined { - const id = this.generateId(testResult); - let prevData = this.results.get(id) as (TestFailed | TestIgnored); + const cacheId = this.cacheId(testResult); + let prevTestResult = this.cache.get(cacheId) as (TestFailed | TestIgnored); - if (!prevData) { - PHPUnitFixer.fixDetails(this.results, testResult); - const file = testResult.details[0].file; + if (!prevTestResult) { + PHPUnitFixer.fixNoTestStarted(this.cache, testResult); + PestFixer.fixNoTestStarted(this.cache, testResult); - return { ...testResult, id: [file, testResult.name].join('::'), file, duration: 0 }; + return { ...testResult, duration: 0 }; } - if (prevData.event === TeamcityEvent.testStarted) { - this.results.set(id, { ...(prevData ?? {}), ...testResult }); + if (prevTestResult.event === TeamcityEvent.testStarted) { + this.cache.set(cacheId, { ...(prevTestResult ?? {}), ...testResult }); return; } if (testResult.message) { - prevData.message += '\n\n' + testResult.message; + prevTestResult.message += '\n\n' + testResult.message; } - prevData.details.push(...testResult.details); + prevTestResult.details.push(...testResult.details); - this.results.set(id, prevData); + this.cache.set(cacheId, prevTestResult); return undefined; } private handleFinished(testResult: TestSuiteFinished | TestFinished) { - const id = this.generateId(testResult); + const cacheId = this.cacheId(testResult); - if (!this.results.has(id)) { + if (!this.cache.has(cacheId)) { return; } - const prevData = this.results.get(id)!; - const event = this.isFault(prevData) ? prevData.event : testResult.event; - const result = { ...prevData, ...testResult, event }; - this.results.delete(id); + const prevTestResult = this.cache.get(cacheId)!; + const event = this.isFault(prevTestResult) ? prevTestResult.event : testResult.event; + const result = { ...prevTestResult, ...testResult, event }; + this.cache.delete(cacheId); return result; } @@ -82,7 +82,7 @@ export class ProblemMatcher { return [TeamcityEvent.testFailed, TeamcityEvent.testIgnored].includes(testResult.event); } - private generateId(testResult: TestSuiteStarted | TestStarted | TestFailed | TestIgnored | TestSuiteFinished | TestFinished) { + private cacheId(testResult: TestSuiteStarted | TestStarted | TestFailed | TestIgnored | TestSuiteFinished | TestFinished) { return `${testResult.name}-${testResult.flowId}`; } } diff --git a/src/PHPUnit/Transformer/PHPUnitFixer.ts b/src/PHPUnit/Transformer/PHPUnitFixer.ts new file mode 100644 index 00000000..8f6f3cba --- /dev/null +++ b/src/PHPUnit/Transformer/PHPUnitFixer.ts @@ -0,0 +1,31 @@ +import { TestFailed, TestIgnored, TestResult } from '../ProblemMatcher'; +import { TransformerFactory } from './TransformerFactory'; +import { getPrevTestResult } from './utils'; + +export class PHPUnitFixer { + static fixNoTestStarted(cache: Map, testResult: TestFailed | TestIgnored) { + if (testResult.id) { + return testResult; + } + + const prevTestResult = getPrevTestResult(new RegExp('^(php_qn):\/\/'), cache, testResult); + if (!prevTestResult) { + return testResult; + } + + if (!testResult.locationHint) { + const parts = prevTestResult.locationHint?.split('::') ?? []; + const locationHint = parts.slice(0, Math.max(2, parts.length - 1)).join('::'); + testResult.locationHint = [locationHint, testResult.name] + .filter(value => !!value) + .join('::'); + } + + const transformer = TransformerFactory.factory(testResult.locationHint); + const { id, file } = transformer.fromLocationHit(testResult.locationHint, testResult.name); + testResult.id = id; + testResult.file = file; + + return testResult; + } +} diff --git a/src/PHPUnit/Transformer/Fixer.ts b/src/PHPUnit/Transformer/PestFixer.ts similarity index 68% rename from src/PHPUnit/Transformer/Fixer.ts rename to src/PHPUnit/Transformer/PestFixer.ts index 5c9198b4..8fc52bd8 100644 --- a/src/PHPUnit/Transformer/Fixer.ts +++ b/src/PHPUnit/Transformer/PestFixer.ts @@ -1,50 +1,45 @@ -import { TeamcityEvent, TestResult, TestStarted, TestSuiteStarted } from '../ProblemMatcher'; +import { TeamcityEvent, TestFailed, TestIgnored, TestResult } from '../ProblemMatcher'; import { capitalize } from '../utils'; +import { getPrevTestResult } from './utils'; -export class PHPUnitFixer { - static fixDetails(results = new Map(), testResult: TestResult & { - name: string, - locationHint?: string, - file?: string, - details?: Array<{ file: string, line: number }>, - }) { - if (testResult.details && testResult.file) { - return testResult; - } +class Str { + static prefix = '__pest_evaluable_'; - const result = Array.from(results.values()).reverse().find((result) => { - return [TeamcityEvent.testSuiteStarted, TeamcityEvent.testStarted].includes(result.event); - }) as (TestSuiteStarted | TestStarted | undefined); + static evaluable(code: string) { + return this.prefix + code.replace(/_/g, '__').replace(/\s/g, '_').replace(/[^a-zA-Z0-9_\u0080-\uFFFF]/g, '_'); + } +} - if (!result) { +export class PestFixer { + static fixNoTestStarted(cache: Map, testResult: TestFailed | TestIgnored) { + if (testResult.id) { return testResult; } - const file = result.file!; - if (!testResult.file) { - testResult.file = file; + if (!testResult.duration) { + testResult.duration = 0; } - if (!testResult.details) { - testResult.details = [{ file: file, line: 1 }]; - } + if ('details' in testResult && testResult.details.length > 0) { + const file = testResult.details[0].file; + testResult.id = [file, testResult.name].join('::'); + testResult.file = file; - if (!testResult.locationHint) { - const locationHint = result.locationHint?.split('::').slice(0, 2).join('::'); - testResult.locationHint = [locationHint, testResult.name] - .filter(value => !!value) - .join('::'); + return testResult; } - return testResult; - } -} + const pattern = new RegExp('^(pest_qn|file):\/\/'); + const prevTestResult = getPrevTestResult(pattern, cache, testResult); + if (prevTestResult) { + testResult.id = [ + prevTestResult.locationHint?.replace(pattern, ''), + testResult.name, + ].filter(v => !!v).join('::'); -class Str { - static prefix = '__pest_evaluable_'; + return testResult; + } - static evaluable(code: string) { - return this.prefix + code.replace(/_/g, '__').replace(/\s/g, '_').replace(/[^a-zA-Z0-9_\u0080-\uFFFF]/g, '_'); + return testResult; } } @@ -53,7 +48,7 @@ export class PestV1Fixer { return this.fixDataSet(/^tests\//.test(locationHint) ? locationHint : locationHint.substring(locationHint.lastIndexOf('tests/'))); } - static fixFlowId(results = new Map(), testResult?: TestResult) { + static fixFlowId(cache: Map, testResult?: TestResult) { if (!testResult) { return testResult; } @@ -63,7 +58,7 @@ export class PestV1Fixer { return testResult; } - const result = Array.from(results.values()).reverse().find((result: TestResult) => { + const result = Array.from(cache.values()).reverse().find((result: TestResult) => { if (testResult.event !== TeamcityEvent.testStarted) { return result.event === TeamcityEvent.testStarted && (result as any).name === (testResult as any).name; } @@ -118,4 +113,4 @@ export class PestV2Fixer { return Str.evaluable(methodName) + dataset; } -} \ No newline at end of file +} diff --git a/src/PHPUnit/Transformer/PestTransFormer.test.ts b/src/PHPUnit/Transformer/PestTransFormer.test.ts index e042954e..d0d2d4b4 100644 --- a/src/PHPUnit/Transformer/PestTransFormer.test.ts +++ b/src/PHPUnit/Transformer/PestTransFormer.test.ts @@ -1,5 +1,5 @@ import { TestType } from '../types'; -import { PestV2Fixer } from './Fixer'; +import { PestV2Fixer } from './PestFixer'; import { PestTransformer } from './PestTransformer'; describe('PestTransformer', () => { diff --git a/src/PHPUnit/Transformer/PestTransformer.ts b/src/PHPUnit/Transformer/PestTransformer.ts index 98f5592d..80ad5f04 100644 --- a/src/PHPUnit/Transformer/PestTransformer.ts +++ b/src/PHPUnit/Transformer/PestTransformer.ts @@ -1,6 +1,6 @@ import { TestDefinition, TestType } from '../types'; import { uncapitalize } from '../utils'; -import { PestV1Fixer, PestV2Fixer } from './Fixer'; +import { PestV1Fixer, PestV2Fixer } from './PestFixer'; import { PHPUnitTransformer } from './PHPUnitTransformer'; import { TransformerFactory } from './TransformerFactory'; diff --git a/src/PHPUnit/Transformer/index.ts b/src/PHPUnit/Transformer/index.ts index cb9b0d87..db1fbda3 100644 --- a/src/PHPUnit/Transformer/index.ts +++ b/src/PHPUnit/Transformer/index.ts @@ -1,5 +1,7 @@ -export * from './Fixer'; -export * from './TransformerFactory'; export * from './Transformer'; +export * from './TransformerFactory'; +export * from './PHPUnitFixer'; export * from './PHPUnitTransformer'; -export * from './PestTransformer'; \ No newline at end of file +export * from './PestFixer'; +export * from './PestTransformer'; +export { getPrevTestResult } from './utils'; \ No newline at end of file diff --git a/src/PHPUnit/Transformer/utils.ts b/src/PHPUnit/Transformer/utils.ts new file mode 100644 index 00000000..19ef7b74 --- /dev/null +++ b/src/PHPUnit/Transformer/utils.ts @@ -0,0 +1,24 @@ +import { TeamcityEvent, TestFailed, TestIgnored, TestResult, TestStarted, TestSuiteStarted } from '../ProblemMatcher'; + +export const getPrevTestResult = (pattern: RegExp, cache: Map, testResult: TestFailed | TestIgnored) => { + for (const prevTestResult of Array.from(cache.values()).reverse()) { + if (isTestStarted(pattern, prevTestResult)) { + return prevTestResult as TestStarted | TestSuiteStarted; + } + + if (prevTestResult.event === TeamcityEvent.testCount) { + continue; + } + + if (prevTestResult.event !== testResult.event) { + break; + } + } + + return undefined; +}; + +const isTestStarted = (pattern: RegExp, testResult: TestResult & { locationHint?: string }) => { + return [TeamcityEvent.testStarted, TeamcityEvent.testSuiteStarted].includes(testResult.event) + && pattern.test(testResult.locationHint ?? ''); +}; \ No newline at end of file