diff --git a/README.md b/README.md index ddab978..1c54b08 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,6 @@ sf apex get test --test-run-id --code-coverage --result-format jso The code coverage JSONs created by the Salesforce CLI aren't accepted by SonarQube automatically for git-based Salesforce repositories and needs to be converted using this plugin. -**Disclaimer**: Due to existing bugs with how the Salesforce CLI reports covered lines during deployments (see [5511](https://github.com/forcedotcom/salesforcedx-vscode/issues/5511) and [1568](https://github.com/forcedotcom/cli/issues/1568)), to add support for covered lines in this plugin for deployment coverage files, I had to add a function to re-number out-of-range covered lines the CLI may report (ex: line 100 in a 98-line Apex Class is reported back as covered by the Salesforce CLI deploy command). Salesforce's coverage result may also include extra lines as covered (ex: 120 lines are included in the coverage report for a 100 line file), so the coverage percentage may vary based on how many lines the API returns in the coverage report. Once Salesforce fixes the API to correctly return covered lines in the deploy command, this function will be removed. - ## Install ```bash diff --git a/src/helpers/getTotalLines.ts b/src/helpers/getTotalLines.ts deleted file mode 100644 index c33d319..0000000 --- a/src/helpers/getTotalLines.ts +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -import { readFile } from 'node:fs/promises'; - -export async function getTotalLines(filePath: string): Promise { - const fileContent = await readFile(filePath, 'utf8'); - return fileContent.split(/\r\n|\r|\n/).length; -} diff --git a/src/helpers/setCoveredLines.ts b/src/helpers/setCoveredLines.ts deleted file mode 100644 index 66821c3..0000000 --- a/src/helpers/setCoveredLines.ts +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -import { join } from 'node:path'; - -import { getTotalLines } from './getTotalLines.js'; -import { FileObject } from './types.js'; - -export async function setCoveredLines( - coveredLines: number[], - uncoveredLines: number[], - repoRoot: string, - filePath: string, - fileObj: FileObject -): Promise { - const randomLines: number[] = []; - const totalLines = await getTotalLines(join(repoRoot, filePath)); - for (const coveredLine of coveredLines) { - if (coveredLine > totalLines) { - for (let randomLineNumber = 1; randomLineNumber <= totalLines; randomLineNumber++) { - if ( - !uncoveredLines.includes(randomLineNumber) && - !coveredLines.includes(randomLineNumber) && - !randomLines.includes(randomLineNumber) - ) { - fileObj.lineToCover.push({ - '@lineNumber': randomLineNumber, - '@covered': 'true', - }); - randomLines.push(randomLineNumber); - break; - } - } - } else { - fileObj.lineToCover.push({ - '@lineNumber': coveredLine, - '@covered': 'true', - }); - } - } -} diff --git a/src/helpers/transformDeployCoverageReport.ts b/src/helpers/transformDeployCoverageReport.ts index 8dacf35..45c6777 100644 --- a/src/helpers/transformDeployCoverageReport.ts +++ b/src/helpers/transformDeployCoverageReport.ts @@ -6,7 +6,6 @@ import { create } from 'xmlbuilder2'; import { DeployCoverageData, CoverageObject, FileObject } from './types.js'; import { getPackageDirectories } from './getPackageDirectories.js'; import { findFilePath } from './findFilePath.js'; -import { setCoveredLines } from './setCoveredLines.js'; import { normalizePathToUnix } from './normalizePathToUnix.js'; export async function transformDeployCoverageReport( @@ -26,23 +25,21 @@ export async function transformDeployCoverageReport( warnings.push(`The file name ${formattedFileName} was not found in any package directory.`); continue; } - const uncoveredLines = Object.keys(fileInfo.s) - .filter((lineNumber) => fileInfo.s[lineNumber] === 0) - .map(Number); - const coveredLines = Object.keys(fileInfo.s) - .filter((lineNumber) => fileInfo.s[lineNumber] === 1) - .map(Number); - const fileObj: FileObject = { '@path': normalizePathToUnix(relativeFilePath), - lineToCover: uncoveredLines.map((lineNumber: number) => ({ - '@lineNumber': lineNumber, - '@covered': 'false', - })), + lineToCover: [], }; - // this function is only needed until Salesforce fixes the API to correctly return covered lines - await setCoveredLines(coveredLines, uncoveredLines, repoRoot, relativeFilePath, fileObj); + for (const lineNumberString in fileInfo.s) { + if (!Object.hasOwn(fileInfo.s, lineNumberString)) continue; + + const covered = fileInfo.s[lineNumberString] === 1 ? 'true' : 'false'; + fileObj.lineToCover.push({ + '@lineNumber': Number(lineNumberString), + '@covered': covered, + }); + } + filesProcessed++; coverageObj.coverage.file.push(fileObj); } diff --git a/test/baselines/classes/AccountProfile.cls b/test/baselines/classes/AccountProfile.cls deleted file mode 100644 index 9b0951e..0000000 --- a/test/baselines/classes/AccountProfile.cls +++ /dev/null @@ -1,72 +0,0 @@ -global class PrepareMySandbox implements SandboxPostCopy { - global PrepareMySandbox() { - // Implementations of SandboxPostCopy must have a no-arg constructor. - // This constructor is used during the sandbox copy process. - } - - global void runApexClass(SandboxContext context) { - System.debug('Org ID: ' + context.organizationId()); - System.debug('Sandbox ID: ' + context.sandboxId()); - System.debug('Sandbox Name: ' + context.sandboxName()); - - updateProfilesAndResetPasswordsForPublicGroupMembers(); - // Additional logic to prepare the sandbox for use can be added here. - } - - public void updateProfilesAndResetPasswordsForPublicGroupMembers() { - String publicGroupId = '00G5a000003ji0R'; - String newProfileId = '00e0b000001KWuY'; - - Group publicGroup = getPublicGroup(publicGroupId); - - if (publicGroup != null) { - List usersToUpdate = getUsersToUpdate(publicGroup, newProfileId); - - if (!usersToUpdate.isEmpty()) { - update usersToUpdate; - System.debug('Profile updated for ' + usersToUpdate.size() + ' users.'); - - // Reset passwords for updated users - resetPasswords(usersToUpdate); - } else { - System.debug('No eligible active users found in the Public Group.'); - } - } else { - System.debug('Public Group not found.'); - } - } - - private Group getPublicGroup(String groupId) { - return [SELECT Id FROM Group WHERE Id = :groupId LIMIT 1]; - } - - private List getUsersToUpdate(Group publicGroup, String newProfileId) { - List usersToUpdate = new List(); - Set userIds = new Set(); - - // Get the current running User's Id - Id currentUserId = UserInfo.getUserId(); - - for (GroupMember member : [SELECT UserOrGroupId FROM GroupMember WHERE GroupId = :publicGroup.Id]) { - Id userOrGroupId = member.UserOrGroupId; - if (userOrGroupId != null && userOrGroupId.getSObjectType() == User.SObjectType && userOrGroupId != currentUserId) { - userIds.add(userOrGroupId); - } - } - - // Query and update active User profiles - for (User user : [SELECT Id, ProfileId FROM User WHERE Id IN :userIds AND IsActive = true]) { - user.ProfileId = newProfileId; - usersToUpdate.add(user); - } - - return usersToUpdate; - } - - private void resetPasswords(List users) { - for (User u : users) { - System.resetPassword(u.Id, true); // The second parameter generates a new password and sends an email - } - System.debug('Passwords reset for ' + users.size() + ' users.'); - } -} \ No newline at end of file diff --git a/test/baselines/triggers/AccountTrigger.trigger b/test/baselines/triggers/AccountTrigger.trigger deleted file mode 100644 index cd99f1a..0000000 --- a/test/baselines/triggers/AccountTrigger.trigger +++ /dev/null @@ -1,40 +0,0 @@ -trigger helloWorldAccountTrigger on Account (before insert) { - - Account[] accs = Trigger.new; - - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); - MyHelloWorld.addHelloWorld(accs); -} diff --git a/test/commands/transformer/unit.test.ts b/test/commands/transformer/unit.test.ts index 9e6ff73..cf7633d 100644 --- a/test/commands/transformer/unit.test.ts +++ b/test/commands/transformer/unit.test.ts @@ -1,6 +1,6 @@ 'use strict'; -import { copyFile, readFile, writeFile, rm, mkdir } from 'node:fs/promises'; +import { readFile, writeFile, rm, mkdir } from 'node:fs/promises'; import { strictEqual } from 'node:assert'; import { resolve } from 'node:path'; @@ -12,8 +12,10 @@ import TransformerTransform from '../../../src/commands/apex-code-coverage/trans describe('main', () => { const $$ = new TestContext(); let sfCommandStubs: ReturnType; - const baselineClassPath = resolve('test/baselines/classes/AccountProfile.cls'); - const baselineTriggerPath = resolve('test/baselines/triggers/AccountTrigger.trigger'); + const mockClassContent = '// Test Apex Class'; + const mockTriggerContent = '// Test Apex Trigger'; + const baselineClassPath = resolve('force-app/main/default/classes/AccountProfile.cls'); + const baselineTriggerPath = resolve('packaged/triggers/AccountTrigger.trigger'); const deployCoverageNoExts = resolve('test/deploy_coverage_no_file_exts.json'); const deployCoverageWithExts = resolve('test/deploy_coverage_with_file_exts.json'); const testCoverage = resolve('test/test_coverage.json'); @@ -34,8 +36,8 @@ describe('main', () => { before(async () => { await mkdir('force-app/main/default/classes', { recursive: true }); await mkdir('packaged/triggers', { recursive: true }); - await copyFile(baselineClassPath, 'force-app/main/default/classes/AccountProfile.cls'); - await copyFile(baselineTriggerPath, 'packaged/triggers/AccountTrigger.trigger'); + await writeFile(baselineClassPath, mockClassContent); + await writeFile(baselineTriggerPath, mockTriggerContent); await writeFile(sfdxConfigFile, configJsonString); }); @@ -48,8 +50,8 @@ describe('main', () => { }); after(async () => { - await rm('force-app/main/default/classes/AccountProfile.cls'); - await rm('packaged/triggers/AccountTrigger.trigger'); + await rm(baselineClassPath); + await rm(baselineTriggerPath); await rm('force-app', { recursive: true }); await rm('packaged', { recursive: true }); await rm(testXmlPath1); diff --git a/test/coverage_baseline.xml b/test/coverage_baseline.xml index 8a1a438..f1e856d 100644 --- a/test/coverage_baseline.xml +++ b/test/coverage_baseline.xml @@ -3,46 +3,46 @@ + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - + + @@ -55,15 +55,15 @@ - - - - - - - - - - + + + + + + + + + + \ No newline at end of file