Skip to content
This repository has been archived by the owner on Mar 18, 2024. It is now read-only.

Commit

Permalink
Include untouched Apex classes & triggers in package coverage calcula…
Browse files Browse the repository at this point in the history
…tion (#560)

* Include untouched Apex classes & triggers in package coverage calculation

* Rename function parameter

Remove awaits

* Remove await

* Fix test class

* Move getConnection() to TriggerApexTests

* Remove await

Co-authored-by: Azlam <[email protected]>
  • Loading branch information
aly76 and azlam-abdulsalam authored Jun 24, 2021
1 parent 4db5806 commit d9a38bc
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 67 deletions.
9 changes: 9 additions & 0 deletions packages/core/package-lock.json

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

1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
186 changes: 151 additions & 35 deletions packages/core/src/package/PackageTestCoverage.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
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;
private packageTestCoverage: number=-1 // Set inital value

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<number> {
let packageClasses: string[] = this.pkg.apexClassWithOutTestClasses;
let triggers: string[] = this.pkg.triggers;

Expand All @@ -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
Expand Down Expand Up @@ -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: {
Expand All @@ -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
Expand Down Expand Up @@ -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<string[]> {
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<string[]> {
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;
}
}
12 changes: 8 additions & 4 deletions packages/core/src/sfpcommands/apextest/TriggerApexTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,37 +10,40 @@ 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;
let testTotalTime;
let testsRan;

try {

let triggerApexTestImpl: TriggerApexTestImpl = new TriggerApexTestImpl(
this.target_org,
this.project_directory,
this.testOptions
);

SFPLogger.log(
`Executing Command
`Executing Command
${triggerApexTestImpl.getGeneratedSFDXCommandWithParams()}`,
this.fileLogger
);
Expand Down Expand Up @@ -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(
Expand Down
Loading

0 comments on commit d9a38bc

Please sign in to comment.