From abe8149ef8eb34abf7a438052003e285d8c43952 Mon Sep 17 00:00:00 2001 From: AnanyaJha Date: Tue, 19 Jan 2021 14:50:12 -0600 Subject: [PATCH] fix: handle test method covering multiple classes (#122) @W-8674084@ --- .../apex-node/src/reporters/humanReporter.ts | 34 ++-- packages/apex-node/src/tests/testService.ts | 59 ++++--- packages/apex-node/src/tests/types.ts | 4 +- .../apex-node/test/tests/codeCoverage.test.ts | 147 +++++++++++++----- .../plugin-apex/src/reporters/jsonReporter.ts | 34 ++-- .../test/commands/force/apex/test/testData.ts | 22 +-- 6 files changed, 189 insertions(+), 111 deletions(-) diff --git a/packages/apex-node/src/reporters/humanReporter.ts b/packages/apex-node/src/reporters/humanReporter.ts index 9273252b..ab6aece5 100644 --- a/packages/apex-node/src/reporters/humanReporter.ts +++ b/packages/apex-node/src/reporters/humanReporter.ts @@ -129,31 +129,29 @@ export class HumanReporter { private formatDetailedCov(testResult: TestResult): string { const tb = new Table(); const testRowArray: Row[] = []; - testResult.tests.forEach( - (elem: { - fullName: string; - outcome: string; - perTestCoverage?: { - apexClassOrTriggerName: string; - percentage: string; - }; - message: string | null; - runTime: number; - }) => { + testResult.tests.forEach((elem: ApexTestResultData) => { + if (elem.perClassCoverage) { + elem.perClassCoverage.forEach(perClassCov => { + testRowArray.push({ + name: elem.fullName, + coveredClassName: perClassCov.apexClassOrTriggerName, + outcome: elem.outcome, + coveredClassPercentage: perClassCov.percentage, + msg: elem.message ?? '', + runtime: `${elem.runTime}` + }); + }); + } else { testRowArray.push({ name: elem.fullName, - coveredClassName: elem.perTestCoverage - ? elem.perTestCoverage.apexClassOrTriggerName - : '', + coveredClassName: '', outcome: elem.outcome, - coveredClassPercentage: elem.perTestCoverage - ? elem.perTestCoverage.percentage - : '', + coveredClassPercentage: '', msg: elem.message ? elem.message : '', runtime: `${elem.runTime}` }); } - ); + }); let detailedCovTable = '\n\n'; detailedCovTable += tb.createTable( diff --git a/packages/apex-node/src/tests/testService.ts b/packages/apex-node/src/tests/testService.ts index e94ff51d..b86eae2d 100644 --- a/packages/apex-node/src/tests/testService.ts +++ b/packages/apex-node/src/tests/testService.ts @@ -21,7 +21,7 @@ import { ApexTestRunResultStatus, TestResult, ApexCodeCoverage, - PerTestCoverage, + PerClassCoverage, OutputDirConfig } from './types'; import * as util from 'util'; @@ -108,15 +108,17 @@ export class TestService { }; if (codeCoverage) { - const perTestCovMap = await this.getPerTestCodeCoverage( + const perClassCovMap = await this.getPerClassCodeCoverage( apexTestClassIdSet ); result.tests.forEach(item => { const keyCodeCov = `${item.apexClass.id}-${item.methodName}`; - const perTestCov = perTestCovMap.get(keyCodeCov); - coveredApexClassIdSet.add(perTestCov.apexClassOrTriggerId); - item.perTestCoverage = perTestCov; + const perClassCov = perClassCovMap.get(keyCodeCov); + perClassCov.forEach(classCov => + coveredApexClassIdSet.add(classCov.apexClassOrTriggerId) + ); + item.perClassCoverage = perClassCov; }); const { @@ -297,17 +299,19 @@ export class TestService { }; if (codeCoverage) { - const perTestCovMap = await this.getPerTestCodeCoverage( + const perClassCovMap = await this.getPerClassCodeCoverage( apexTestClassIdSet ); result.tests.forEach(item => { const keyCodeCov = `${item.apexClass.id}-${item.methodName}`; - const perTestCov = perTestCovMap.get(keyCodeCov); - // Skipped test is not in coverage map, check to see if perTestCov exists first - if (perTestCov) { - coveredApexClassIdSet.add(perTestCov.apexClassOrTriggerId); - item.perTestCoverage = perTestCov; + const perClassCov = perClassCovMap.get(keyCodeCov); + // Skipped test is not in coverage map, check to see if perClassCov exists first + if (perClassCov) { + perClassCov.forEach(classCov => + coveredApexClassIdSet.add(classCov.apexClassOrTriggerId) + ); + item.perClassCoverage = perClassCov; } }); @@ -445,31 +449,31 @@ export class TestService { return `${orgWideCoverageResult.records[0].PercentCovered}%`; } - public async getPerTestCodeCoverage( + //NOTE: a test could cover more than one class, map should contain a record for each covered class + public async getPerClassCodeCoverage( apexTestClassSet: Set - ): Promise> { + ): Promise> { let str = ''; apexTestClassSet.forEach(elem => { str += `'${elem}',`; }); str = str.slice(0, -1); - const perTestCodeCovQuery = + const perClassCodeCovQuery = 'SELECT ApexTestClassId, ApexClassOrTrigger.Id, ApexClassOrTrigger.Name, TestMethodName, NumLinesCovered, NumLinesUncovered, Coverage FROM ApexCodeCoverage WHERE ApexTestClassId IN (%s)'; - const perTestCodeCovResuls = (await this.connection.tooling.query( - util.format(perTestCodeCovQuery, `${str}`) + const perClassCodeCovResuls = (await this.connection.tooling.query( + util.format(perClassCodeCovQuery, `${str}`) )) as ApexCodeCoverage; - const perTestCoverageMap = new Map(); - perTestCodeCovResuls.records.forEach(item => { + const perClassCoverageMap = new Map(); + perClassCodeCovResuls.records.forEach(item => { const totalLines = item.NumLinesCovered + item.NumLinesUncovered; const percentage = this.calculatePercentage( item.NumLinesCovered, totalLines ); - //NOTE: a test could cover more than one class, we should change this in order to handle that - perTestCoverageMap.set(`${item.ApexTestClassId}-${item.TestMethodName}`, { + const value = { apexClassOrTriggerName: item.ApexClassOrTrigger.Name, apexClassOrTriggerId: item.ApexClassOrTrigger.Id, apexTestClassId: item.ApexTestClassId, @@ -478,10 +482,19 @@ export class TestService { numLinesUncovered: item.NumLinesUncovered, percentage, ...(item.Coverage ? { coverage: item.Coverage } : {}) - }); + }; + const key = `${item.ApexTestClassId}-${item.TestMethodName}`; + if (perClassCoverageMap.get(key)) { + perClassCoverageMap.get(key).push(value); + } else { + perClassCoverageMap.set( + `${item.ApexTestClassId}-${item.TestMethodName}`, + [value] + ); + } }); - return perTestCoverageMap; + return perClassCoverageMap; } public async getAggregateCodeCoverage( @@ -584,7 +597,7 @@ export class TestService { if (codeCoverage) { const coverageRecords = result.tests.map(record => { - return record.perTestCoverage; + return record.perClassCoverage; }); fileMap.push({ path: join( diff --git a/packages/apex-node/src/tests/types.ts b/packages/apex-node/src/tests/types.ts index 96cada6d..8e67d03a 100644 --- a/packages/apex-node/src/tests/types.ts +++ b/packages/apex-node/src/tests/types.ts @@ -340,7 +340,7 @@ export type ApexTestResultData = { /** * The associated ApexCodeCoverage object */ - perTestCoverage?: PerTestCoverage; + perClassCoverage?: PerClassCoverage[]; }; export type CodeCoverageResult = { @@ -403,7 +403,7 @@ export type ApexCodeCoverage = { records: ApexCodeCoverageRecord[]; }; -export type PerTestCoverage = { +export type PerClassCoverage = { apexClassOrTriggerName: string; apexClassOrTriggerId: string; apexTestClassId: string; diff --git a/packages/apex-node/test/tests/codeCoverage.test.ts b/packages/apex-node/test/tests/codeCoverage.test.ts index 039c99f6..676226d5 100644 --- a/packages/apex-node/test/tests/codeCoverage.test.ts +++ b/packages/apex-node/test/tests/codeCoverage.test.ts @@ -161,8 +161,8 @@ describe('Get code coverage results', () => { expect(coveredLines).to.equal(0); }); - it('should return per test code coverage', async () => { - const perTestCodeCovResult = [ + it('should return per class code coverage for multiple test classes', async () => { + const perClassCodeCovResult = [ { ApexClassOrTrigger: { Id: '0001x05958', Name: 'ApexTrigger1' }, TestMethodName: 'MethodOne', @@ -194,63 +194,126 @@ describe('Get code coverage results', () => { toolingQueryStub.resolves({ done: true, totalSize: 3, - records: perTestCodeCovResult + records: perClassCodeCovResult } as ApexCodeCoverage); const testSrv = new TestService(mockConnection); - const perTestCoverageMap = await testSrv.getPerTestCodeCoverage( + const perClassCoverageMap = await testSrv.getPerClassCodeCoverage( new Set(['0001x05958', '0001x05959', '0001x05951']) ); - expect(perTestCoverageMap.size).to.eql(3); - expect(perTestCoverageMap.get('0001x05958-MethodOne')).to.deep.equal({ - apexClassOrTriggerName: 'ApexTrigger1', - apexClassOrTriggerId: '0001x05958', - apexTestClassId: '0001x05958', - apexTestMethodName: 'MethodOne', - percentage: '83%', - coverage: { coveredLines: [1, 2, 3, 4, 5], uncoveredLines: [6] }, - numLinesCovered: 5, - numLinesUncovered: 1 - }); - expect(perTestCoverageMap.get('0001x05959-MethodTwo')).to.deep.equal({ - apexClassOrTriggerName: 'ApexTrigger2', - apexClassOrTriggerId: '0001x05959', - apexTestClassId: '0001x05959', - apexTestMethodName: 'MethodTwo', - percentage: '75%', - coverage: { - coveredLines: [1, 2, 3, 4, 5, 6], - uncoveredLines: [7, 8] + expect(perClassCoverageMap.size).to.eql(3); + expect(perClassCoverageMap.get('0001x05958-MethodOne')).to.deep.equal([ + { + apexClassOrTriggerName: 'ApexTrigger1', + apexClassOrTriggerId: '0001x05958', + apexTestClassId: '0001x05958', + apexTestMethodName: 'MethodOne', + percentage: '83%', + coverage: { coveredLines: [1, 2, 3, 4, 5], uncoveredLines: [6] }, + numLinesCovered: 5, + numLinesUncovered: 1 + } + ]); + expect(perClassCoverageMap.get('0001x05959-MethodTwo')).to.deep.equal([ + { + apexClassOrTriggerName: 'ApexTrigger2', + apexClassOrTriggerId: '0001x05959', + apexTestClassId: '0001x05959', + apexTestMethodName: 'MethodTwo', + percentage: '75%', + coverage: { + coveredLines: [1, 2, 3, 4, 5, 6], + uncoveredLines: [7, 8] + }, + numLinesCovered: 6, + numLinesUncovered: 2 + } + ]); + expect(perClassCoverageMap.get('0001x05951-MethodThree')).to.deep.equal([ + { + apexClassOrTriggerName: 'ApexTrigger3', + apexClassOrTriggerId: '0001x05951', + apexTestClassId: '0001x05951', + apexTestMethodName: 'MethodThree', + percentage: '70%', + coverage: { + coveredLines: [1, 2, 3, 4, 5, 6, 7], + uncoveredLines: [8, 9, 10] + }, + numLinesCovered: 7, + numLinesUncovered: 3 + } + ]); + }); + + it('should return per class code coverage for test that covers multiple classes', async () => { + const perClassCodeCovResult = [ + { + ApexClassOrTrigger: { Id: '0001x05958', Name: 'ApexTrigger1' }, + TestMethodName: 'MethodOne', + ApexTestClassId: '0001x05958', + NumLinesCovered: 5, + NumLinesUncovered: 1, + Coverage: { coveredLines: [1, 2, 3, 4, 5], uncoveredLines: [6] } }, - numLinesCovered: 6, - numLinesUncovered: 2 - }); - expect(perTestCoverageMap.get('0001x05951-MethodThree')).to.deep.equal({ - apexClassOrTriggerName: 'ApexTrigger3', - apexClassOrTriggerId: '0001x05951', - apexTestClassId: '0001x05951', - apexTestMethodName: 'MethodThree', - percentage: '70%', - coverage: { - coveredLines: [1, 2, 3, 4, 5, 6, 7], - uncoveredLines: [8, 9, 10] + { + ApexClassOrTrigger: { Id: '0001x05959', Name: 'ApexTrigger2' }, + ApexTestClassId: '0001x05958', + TestMethodName: 'MethodOne', + NumLinesCovered: 6, + NumLinesUncovered: 2, + Coverage: { coveredLines: [1, 2, 3, 4, 5, 6], uncoveredLines: [7, 8] } + } + ]; + toolingQueryStub.resolves({ + done: true, + totalSize: 2, + records: perClassCodeCovResult + } as ApexCodeCoverage); + + const testSrv = new TestService(mockConnection); + const perClassCoverageMap = await testSrv.getPerClassCodeCoverage( + new Set(['0001x05958']) + ); + expect(perClassCoverageMap.size).to.eql(1); + expect(perClassCoverageMap.get('0001x05958-MethodOne')).to.deep.equal([ + { + apexClassOrTriggerName: 'ApexTrigger1', + apexClassOrTriggerId: '0001x05958', + apexTestClassId: '0001x05958', + apexTestMethodName: 'MethodOne', + percentage: '83%', + coverage: { coveredLines: [1, 2, 3, 4, 5], uncoveredLines: [6] }, + numLinesCovered: 5, + numLinesUncovered: 1 }, - numLinesCovered: 7, - numLinesUncovered: 3 - }); + { + apexClassOrTriggerName: 'ApexTrigger2', + apexClassOrTriggerId: '0001x05959', + apexTestClassId: '0001x05958', + apexTestMethodName: 'MethodOne', + percentage: '75%', + coverage: { + coveredLines: [1, 2, 3, 4, 5, 6], + uncoveredLines: [7, 8] + }, + numLinesCovered: 6, + numLinesUncovered: 2 + } + ]); }); - it('should return per test coverage', async () => { + it('should return per class coverage for test that covers 0 classes', async () => { toolingQueryStub.resolves({ done: true, totalSize: 0, records: [] } as ApexCodeCoverage); const testSrv = new TestService(mockConnection); - const perTestCoverageMap = await testSrv.getPerTestCodeCoverage( + const perClassCoverageMap = await testSrv.getPerClassCodeCoverage( new Set([]) ); - expect(perTestCoverageMap.size).to.equal(0); + expect(perClassCoverageMap.size).to.equal(0); }); }); diff --git a/packages/plugin-apex/src/reporters/jsonReporter.ts b/packages/plugin-apex/src/reporters/jsonReporter.ts index ae374170..222df59c 100644 --- a/packages/plugin-apex/src/reporters/jsonReporter.ts +++ b/packages/plugin-apex/src/reporters/jsonReporter.ts @@ -38,7 +38,7 @@ type ClassCoverage = { coveredPercent: number; }; -type PerTestCoverage = { +type PerClassCoverage = { ApexTestClass: { Id: string; Name: string; @@ -55,7 +55,7 @@ type PerTestCoverage = { type CliCoverageResult = { coverage: ClassCoverage[]; - records: PerTestCoverage[]; + records: PerClassCoverage[]; summary: { totalLines: number; coveredLines: number; @@ -161,20 +161,22 @@ export class JsonReporter { }); testResult.tests.forEach(test => { - if (test.perTestCoverage) { - formattedCov.records.push({ - ApexTestClass: { Id: test.id, Name: test.apexClass.name }, - ...(test.perTestCoverage.coverage - ? { Coverage: test.perTestCoverage.coverage } - : {}), - TestMethodName: test.methodName, - NumLinesCovered: test.perTestCoverage.numLinesCovered, - ApexClassOrTrigger: { - Id: test.perTestCoverage.apexClassOrTriggerId, - Name: test.perTestCoverage.apexClassOrTriggerName - }, - NumLinesUncovered: test.perTestCoverage.numLinesUncovered - } as PerTestCoverage); + if (test.perClassCoverage) { + test.perClassCoverage.forEach(perClassCov => { + formattedCov.records.push({ + ApexTestClass: { Id: test.id, Name: test.apexClass.name }, + ...(perClassCov.coverage + ? { Coverage: perClassCov.coverage } + : {}), + TestMethodName: test.methodName, + NumLinesCovered: perClassCov.numLinesCovered, + ApexClassOrTrigger: { + Id: perClassCov.apexClassOrTriggerId, + Name: perClassCov.apexClassOrTriggerName + }, + NumLinesUncovered: perClassCov.numLinesUncovered + } as PerClassCoverage); + }); } }); } diff --git a/packages/plugin-apex/test/commands/force/apex/test/testData.ts b/packages/plugin-apex/test/commands/force/apex/test/testData.ts index f602adbb..2f2e0f53 100644 --- a/packages/plugin-apex/test/commands/force/apex/test/testData.ts +++ b/packages/plugin-apex/test/commands/force/apex/test/testData.ts @@ -90,16 +90,18 @@ export const runWithCoverage = { runTime: 53, testTimestamp: '2020-08-25T00:48:02.000+0000', fullName: 'MyApexTests.testConfig', - perTestCoverage: { - apexTestClassId: '01pxx00000NnP2KQAV', - apexClassOrTriggerName: 'ApexClassExample', - apexClassOrTriggerId: '01pxx00000avcNeAAL', - apexTestMethodName: 'testAssignContains', - numLinesCovered: 1, - numLinesUncovered: 4, - percentage: '20%', - coverage: { coveredLines: [1], uncoveredLines: [2, 3, 4, 5] } - } + perClassCoverage: [ + { + apexTestClassId: '01pxx00000NnP2KQAV', + apexClassOrTriggerName: 'ApexClassExample', + apexClassOrTriggerId: '01pxx00000avcNeAAL', + apexTestMethodName: 'testAssignContains', + numLinesCovered: 1, + numLinesUncovered: 4, + percentage: '20%', + coverage: { coveredLines: [1], uncoveredLines: [2, 3, 4, 5] } + } + ] } ], codecoverage: [