diff --git a/packages/core/package-lock.json b/packages/core/package-lock.json index cffc98439..0f723934d 100644 --- a/packages/core/package-lock.json +++ b/packages/core/package-lock.json @@ -893,6 +893,15 @@ "pretty-format": "^26.0.0" } }, + "@types/jsforce": { + "version": "1.9.29", + "resolved": "https://registry.npmjs.org/@types/jsforce/-/jsforce-1.9.29.tgz", + "integrity": "sha512-wG26GTIeRzBQ1GjeOfaM/ZVEP+/m1NpX0KUd/W3QGs8GvHAU6Am1nqm8F7zQDYEJ3p0PssNNbdhqdxJGcQsbxQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/node": { "version": "14.14.16", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.16.tgz", diff --git a/packages/core/package.json b/packages/core/package.json index 4385985e1..8ac7e59e0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -41,6 +41,7 @@ "@types/async-retry": "^1.4.2", "@types/datadog-metrics": "^0.6.1", "@types/jest": "^26.0.19", + "@types/jsforce": "^1.9.29", "jest": "^26.6.3", "ts-jest": "^26.4.4", "ts-node": "^9.1.1", diff --git a/packages/core/src/package/PackageTestCoverage.ts b/packages/core/src/package/PackageTestCoverage.ts index eac0a0b79..7290b205f 100644 --- a/packages/core/src/package/PackageTestCoverage.ts +++ b/packages/core/src/package/PackageTestCoverage.ts @@ -1,6 +1,7 @@ import SFPLogger from "../logger/SFPLogger"; import IndividualClassCoverage from "./IndividualClassCoverage"; import SFPPackage from "./SFPPackage"; +import { Connection } from "@salesforce/core"; export default class PackageTestCoverage { private individualClassCoverage: IndividualClassCoverage; @@ -8,14 +9,15 @@ export default class PackageTestCoverage { public constructor( private pkg: SFPPackage, - private codeCoverage: any + private codeCoverage: any, + private readonly conn: Connection ) { this.individualClassCoverage = new IndividualClassCoverage( this.codeCoverage ); } - public getCurrentPackageTestCoverage(): number { + public async getCurrentPackageTestCoverage(): Promise { let packageClasses: string[] = this.pkg.apexClassWithOutTestClasses; let triggers: string[] = this.pkg.triggers; @@ -33,22 +35,47 @@ export default class PackageTestCoverage { totalCovered += classCoverage.totalCovered; } } + + let listOfApexClassOrTriggerId: string[] = []; + + let classesNotTouchedByTestClass = this.getClassesNotTouchedByTestClass(packageClasses, this.codeCoverage); + if (classesNotTouchedByTestClass.length > 0) { + let apexClassIds = await this.queryApexClassIdByName(classesNotTouchedByTestClass); + listOfApexClassOrTriggerId = listOfApexClassOrTriggerId.concat(apexClassIds); + } + + let triggersNotTouchedByTestClass = this.getTriggersNotTouchedByTestClass(triggers, this.codeCoverage); + if (triggersNotTouchedByTestClass.length > 0) { + let triggerIds = await this.queryTriggerIdByName(triggersNotTouchedByTestClass); + listOfApexClassOrTriggerId = listOfApexClassOrTriggerId.concat(triggerIds); + } + + if (listOfApexClassOrTriggerId.length > 0) { + let recordsOfApexCodeCoverageAggregate = await this.queryApexCodeCoverageAggregateById(listOfApexClassOrTriggerId); + + if (recordsOfApexCodeCoverageAggregate.length > 0) { + let numLinesUncovered: number = 0; // aggregate number of unconvered lines for classes & triggers that are not touched by any test classes + recordsOfApexCodeCoverageAggregate.forEach((record) => { numLinesUncovered += record.NumLinesUncovered; }); + totalLines += numLinesUncovered; + } + } + let testCoverage = Math.floor((totalCovered / totalLines) * 100); this.packageTestCoverage=testCoverage; return testCoverage; } - public validateTestCoverage( + public async validateTestCoverage( coverageThreshold?:number - ): { + ): Promise<{ result: boolean; message?: string; packageTestCoverage: number; classesCovered?: { name: string; coveredPercent: number }[]; classesWithInvalidCoverage?: { name: string; coveredPercent: number }[]; - } { + }> { if(this.packageTestCoverage==-1) //No Value available - this.getCurrentPackageTestCoverage(); + await this.getCurrentPackageTestCoverage(); let classesCovered = this.getIndividualClassCoverageByPackage( this.codeCoverage @@ -136,19 +163,7 @@ export default class PackageTestCoverage { } } - - // Check for package classes with no test class - let namesOfClassesWithoutTest: string[] = packageClasses.filter( - (packageClass) => { - // Filter out package class if accounted for in coverage json - for (let classCoverage of codeCoverageReport) { - if (classCoverage["name"] === packageClass) { - return false; - } - } - return true; - } - ); + let namesOfClassesWithoutTest: string[] = this.getClassesNotTouchedByTestClass(packageClasses, codeCoverageReport); if (namesOfClassesWithoutTest.length > 0) { let classesWithoutTest: { @@ -162,32 +177,68 @@ export default class PackageTestCoverage { ); } + // Check for triggers with no test class + let namesOfTriggersWithoutTest: string[] = this.getTriggersNotTouchedByTestClass(triggers, codeCoverageReport); + + if (namesOfTriggersWithoutTest.length > 0) { + let triggersWithoutTest: { + name: string; + coveredPercent: number; + }[] = namesOfTriggersWithoutTest.map((triggerName) => { + return { name: triggerName, coveredPercent: 0 }; + }); + individualClassCoverage = individualClassCoverage.concat( + triggersWithoutTest + ); + } + + return individualClassCoverage; + } + + /** + * Returns names of triggers in the package that are not triggered by the execution of any test classes + * Returns empty array if triggers is null or undefined + * @param triggers + * @param codeCoverageReport + * @returns + */ + private getTriggersNotTouchedByTestClass(triggers: string[], codeCoverageReport: any): string[] { if (triggers != null) { - // Check for triggers with no test class - let namesOfTriggersWithoutTest: string[] = triggers.filter((trigger) => { - // Filter out triggers if accounted for in coverage json + return triggers.filter((trigger) => { for (let classCoverage of codeCoverageReport) { if (classCoverage["name"] === trigger) { + // Filter out triggers if accounted for in coverage json return false; } } return true; }); + } else return []; + } - if (namesOfTriggersWithoutTest.length > 0) { - let triggersWithoutTest: { - name: string; - coveredPercent: number; - }[] = namesOfTriggersWithoutTest.map((triggerName) => { - return { name: triggerName, coveredPercent: 0 }; - }); - individualClassCoverage = individualClassCoverage.concat( - triggersWithoutTest - ); - } - } - return individualClassCoverage; + /** + * Returns name of classes in the package that are not touched by the execution of any test classes + * Returns empty array if packageClasses is null or undefined + * @param packageClasses + * @param codeCoverageReport + * @returns + */ + private getClassesNotTouchedByTestClass(packageClasses: string[], codeCoverageReport: any): string[] { + if (packageClasses != null) { + return packageClasses.filter( + (packageClass) => { + for (let classCoverage of codeCoverageReport) { + if (classCoverage["name"] === packageClass) { + // Filter out package class if accounted for in coverage json + return false; + } + } + return true; + } + ); + } else return []; } + /** * Filter code coverage to classes and triggers in the package * @param codeCoverage @@ -219,4 +270,69 @@ export default class PackageTestCoverage { return filteredCodeCoverage; } + + /** + * Query ApexCodeCoverageAggregate by list of ApexClassorTriggerId + * @param listOfApexClassOrTriggerId + * @returns + */ + private async queryApexCodeCoverageAggregateById( + listOfApexClassOrTriggerId: string[] + ) { + let collection = listOfApexClassOrTriggerId.map((ApexClassOrTriggerId) => `'${ApexClassOrTriggerId}'`).toString(); + let query = `SELECT ApexClassorTriggerId, NumLinesCovered, NumLinesUncovered, Coverage FROM ApexCodeCoverageAggregate WHERE ApexClassorTriggerId IN (${collection})`; + + return ( + await this.conn.tooling.query<{ + ApexClassOrTriggerId: string; + NumLinesCovered: number; + NumLinesUncovered: number; + Coverage: any; + }>(query) + ).records; + } + + /** + * Query Ids of Triggers by Name + * Returns empty array if no Id's are found + * @param triggersNotTouchedByTestClass + * @returns + */ + private async queryTriggerIdByName( + triggers: string[] + ): Promise { + let triggerIds: string[] = []; + + let collection = triggers.map((trigger) => `'${trigger}'`).toString(); // transform into formatted string for query + let query = `SELECT ID, Name FROM ApexTrigger WHERE Name IN (${collection})`; + let records = (await this.conn.query<{ Id: string; Name: string; }>(query)).records; + + if (records.length > 0) { + records.forEach((record) => { triggerIds.push(record.Id); }); + } + + return triggerIds; + } + + /** + * Query Ids of Apex Classes by Name + * Returns empty array if no Id's are found + * @param classes + * @returns + */ + private async queryApexClassIdByName( + classes: string[], + ): Promise { + let apexClassIds: string[] = []; + + let collection = classes.map((cls) => `'${cls}'`).toString(); // transform into formatted string for query + let query = `SELECT ID, Name FROM ApexClass WHERE Name IN (${collection})`; + let records = (await this.conn.query<{ Id: string; Name: string; }>(query)).records; + + if (records.length > 0) { + records.forEach((record) => { apexClassIds.push(record.Id); }); + } + + return apexClassIds; + } } diff --git a/packages/core/src/sfpcommands/apextest/TriggerApexTests.ts b/packages/core/src/sfpcommands/apextest/TriggerApexTests.ts index 14a59b175..7e6321119 100644 --- a/packages/core/src/sfpcommands/apextest/TriggerApexTests.ts +++ b/packages/core/src/sfpcommands/apextest/TriggerApexTests.ts @@ -10,21 +10,25 @@ import PackageTestCoverage from "../../package/PackageTestCoverage"; import SFPLogger from "../../logger/SFPLogger"; import { RunAllTestsInPackageOptions } from "./ExtendedTestOptions"; import SFPStatsSender from "../../stats/SFPStatsSender"; +import { Connection, Org } from "@salesforce/core"; export default class TriggerApexTests { + private conn: Connection; + public constructor( private target_org: string, private testOptions: TestOptions, private coverageOptions: CoverageOptions, private project_directory: string, private fileLogger?: any - ) { } + ) {} public async exec(): Promise<{ id: string; result: boolean; message: string; }> { + this.conn = (await Org.create({aliasOrUsername: this.target_org})).getConnection(); let startTime = Date.now(); let testExecutionResult: boolean = false; @@ -32,7 +36,6 @@ export default class TriggerApexTests { let testsRan; try { - let triggerApexTestImpl: TriggerApexTestImpl = new TriggerApexTestImpl( this.target_org, this.project_directory, @@ -40,7 +43,7 @@ export default class TriggerApexTests { ); SFPLogger.log( - `Executing Command + `Executing Command ${triggerApexTestImpl.getGeneratedSFDXCommandWithParams()}`, this.fileLogger ); @@ -179,7 +182,8 @@ export default class TriggerApexTests { let packageTestCoverage: PackageTestCoverage = new PackageTestCoverage( this.testOptions.sfppackage, - this.getCoverageReport() + this.getCoverageReport(), + this.conn ); return packageTestCoverage.validateTestCoverage( diff --git a/packages/core/tests/package/PackageTestCoverage.test.ts b/packages/core/tests/package/PackageTestCoverage.test.ts index 95e08f6f4..effb29bf8 100644 --- a/packages/core/tests/package/PackageTestCoverage.test.ts +++ b/packages/core/tests/package/PackageTestCoverage.test.ts @@ -1,6 +1,6 @@ import SFPPackage from "../../src/package/SFPPackage"; import PackageTestCoverage from "../../src/package/PackageTestCoverage" - +import { Connection } from "@salesforce/core"; import { jest, expect } from "@jest/globals"; @@ -35,18 +35,26 @@ jest.mock("../../src/package/SFPPackage",()=>{ describe("Given a sfpowerscripts package andcode coverage report, a package coverage calculator",()=>{ - it("should be able to provide the coverage of a provided unlocked package",async ()=>{ - let sfpPackage:SFPPackage = await SFPPackage.buildPackageFromProjectConfig(null,"es-base-code",null,null); - let packageTestCoverage:PackageTestCoverage = new PackageTestCoverage(sfpPackage,succesfulTestCoverage); - expect (packageTestCoverage.getCurrentPackageTestCoverage()).toBe(88); + it("should be able to provide the coverage of a provided unlocked package",async ()=>{ + let sfpPackage:SFPPackage = await SFPPackage.buildPackageFromProjectConfig(null,"es-base-code",null,null); + let packageTestCoverage:PackageTestCoverage = new PackageTestCoverage( + sfpPackage, + succesfulTestCoverage, + {} as Connection + ); + expect (await packageTestCoverage.getCurrentPackageTestCoverage()).toBe(88); }); - it("should able to validate whether the coverage of unlocked package is above a certain threshold",async ()=>{ + it("should able to validate whether the coverage of unlocked package is above a certain threshold",async ()=>{ let sfpPackage:SFPPackage = await SFPPackage.buildPackageFromProjectConfig(null,"es-base-code",null,null); - let packageTestCoverage:PackageTestCoverage = new PackageTestCoverage(sfpPackage,succesfulTestCoverage); - let requiredCoverage=80; - let result=packageTestCoverage.validateTestCoverage(requiredCoverage); + let packageTestCoverage:PackageTestCoverage = new PackageTestCoverage( + sfpPackage, + succesfulTestCoverage, + {} as Connection + ); + let requiredCoverage = 80; + let result = await packageTestCoverage.validateTestCoverage(requiredCoverage); expect (result.result).toBe(true); expect (result.packageTestCoverage).toBe(88); expect (result.message).toStrictEqual(`Package overall coverage is greater than ${requiredCoverage}%`); @@ -54,14 +62,18 @@ describe("Given a sfpowerscripts package andcode coverage report, a package cove { name: 'CustomerServices', coveredPercent: 87.09677419354838 }, { name: 'MarketServices', coveredPercent: 100 } ]); - expect(result.classesWithInvalidCoverage).toBeUndefined(); + expect(result.classesWithInvalidCoverage).toBeUndefined(); }); -it("should able to validate whether the coverage of unlocked package is above mandatory threshold",async ()=>{ +it("should able to validate whether the coverage of unlocked package is above mandatory threshold",async ()=>{ let sfpPackage:SFPPackage = await SFPPackage.buildPackageFromProjectConfig(null,"es-base-code",null,null); - let packageTestCoverage:PackageTestCoverage = new PackageTestCoverage(sfpPackage,succesfulTestCoverage); - let requiredCoverage=75; - let result=packageTestCoverage.validateTestCoverage(); + let packageTestCoverage:PackageTestCoverage = new PackageTestCoverage( + sfpPackage, + succesfulTestCoverage, + {} as Connection + ); + let requiredCoverage = 75; + let result = await packageTestCoverage.validateTestCoverage(); expect (result.result).toBe(true); expect (result.packageTestCoverage).toBe(88); expect (result.message).toStrictEqual(`Package overall coverage is greater than ${requiredCoverage}%`); @@ -69,24 +81,32 @@ it("should able to validate whether the coverage of unlocked package is above m { name: 'CustomerServices', coveredPercent: 87.09677419354838 }, { name: 'MarketServices', coveredPercent: 100 } ]); - expect(result.classesWithInvalidCoverage).toBeUndefined(); + expect(result.classesWithInvalidCoverage).toBeUndefined(); }); -it("should be able to provide the coverage of a provided source package",async ()=>{ +it("should be able to provide the coverage of a provided source package",async ()=>{ packageType="Source"; let sfpPackage:SFPPackage = await SFPPackage.buildPackageFromProjectConfig(null,"es-base-code",null,null); - let packageTestCoverage:PackageTestCoverage = new PackageTestCoverage(sfpPackage,succesfulTestCoverage); - expect (packageTestCoverage.getCurrentPackageTestCoverage()).toBe(88); + let packageTestCoverage:PackageTestCoverage = new PackageTestCoverage( + sfpPackage, + succesfulTestCoverage, + {} as Connection + ); + expect (await packageTestCoverage.getCurrentPackageTestCoverage()).toBe(88); }); -it("should able to validate whether the coverage of source package is above a certain threshold",async ()=>{ +it("should able to validate whether the coverage of source package is above a certain threshold",async ()=>{ packageType="Source"; let sfpPackage:SFPPackage = await SFPPackage.buildPackageFromProjectConfig(null,"es-base-code",null,null); - let packageTestCoverage:PackageTestCoverage = new PackageTestCoverage(sfpPackage,succesfulTestCoverage); - let requiredCoverage=80; - let result=packageTestCoverage.validateTestCoverage(requiredCoverage); + let packageTestCoverage:PackageTestCoverage = new PackageTestCoverage( + sfpPackage, + succesfulTestCoverage, + {} as Connection + ); + let requiredCoverage = 80; + let result = await packageTestCoverage.validateTestCoverage(requiredCoverage); expect (result.result).toBe(true); expect (result.packageTestCoverage).toBe(88); expect (result.message).toStrictEqual(`Individidual coverage of classes is greater than ${requiredCoverage}%`); @@ -94,16 +114,20 @@ it("should able to validate whether the coverage of source package is above a c { name: 'CustomerServices', coveredPercent: 87.09677419354838 }, { name: 'MarketServices', coveredPercent: 100 } ]); - expect(result.classesWithInvalidCoverage).toBeUndefined(); + expect(result.classesWithInvalidCoverage).toBeUndefined(); }); -it("should able to validate whether the coverage of source package is above mandatory threshold",async ()=>{ +it("should able to validate whether the coverage of source package is above mandatory threshold",async ()=>{ packageType="Source"; let sfpPackage:SFPPackage = await SFPPackage.buildPackageFromProjectConfig(null,"es-base-code",null,null); - let packageTestCoverage:PackageTestCoverage = new PackageTestCoverage(sfpPackage,succesfulTestCoverage); - let requiredCoverage=75; - let result=packageTestCoverage.validateTestCoverage(); + let packageTestCoverage:PackageTestCoverage = new PackageTestCoverage( + sfpPackage, + succesfulTestCoverage, + {} as Connection + ); + let requiredCoverage = 75; + let result = await packageTestCoverage.validateTestCoverage(); expect (result.result).toBe(true); expect (result.packageTestCoverage).toBe(88); expect (result.message).toStrictEqual(`Individidual coverage of classes is greater than ${requiredCoverage}%`); @@ -111,7 +135,7 @@ it("should able to validate whether the coverage of source package is above man { name: 'CustomerServices', coveredPercent: 87.09677419354838 }, { name: 'MarketServices', coveredPercent: 100 } ]); - expect(result.classesWithInvalidCoverage).toBeUndefined(); + expect(result.classesWithInvalidCoverage).toBeUndefined(); }); });