diff --git a/README.md b/README.md index f33d87e..26d1279 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,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 automatically for Salesforce DX 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. - ## Command The `apex-code-coverage-transformer` has 1 command: 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/setCoveredLinesCobertura.ts b/src/helpers/setCoveredLinesCobertura.ts deleted file mode 100644 index 0ecfd60..0000000 --- a/src/helpers/setCoveredLinesCobertura.ts +++ /dev/null @@ -1,45 +0,0 @@ -'use strict'; - -import { join } from 'node:path'; - -import { getTotalLines } from './getTotalLines.js'; -import { CoberturaClass, CoberturaLine } from './types.js'; - -export async function setCoveredLinesCobertura( - coveredLines: number[], - uncoveredLines: number[], - repoRoot: string, - filePath: string, - classObj: CoberturaClass -): 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) - ) { - const randomLine: CoberturaLine = { - '@number': randomLineNumber, - '@hits': 1, - '@branch': 'false', - }; - classObj.lines.line.push(randomLine); - randomLines.push(randomLineNumber); - break; - } - } - } else { - const coveredLineObj: CoberturaLine = { - '@number': coveredLine, - '@hits': 1, - '@branch': 'false', - }; - classObj.lines.line.push(coveredLineObj); - } - } -} diff --git a/src/helpers/setCoveredLinesSonar.ts b/src/helpers/setCoveredLinesSonar.ts deleted file mode 100644 index 72b311a..0000000 --- a/src/helpers/setCoveredLinesSonar.ts +++ /dev/null @@ -1,40 +0,0 @@ -'use strict'; - -import { join } from 'node:path'; - -import { getTotalLines } from './getTotalLines.js'; -import { SonarClass } from './types.js'; - -export async function setCoveredLinesSonar( - coveredLines: number[], - uncoveredLines: number[], - repoRoot: string, - filePath: string, - fileObj: SonarClass -): 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 1c27df0..5e377e1 100644 --- a/src/helpers/transformDeployCoverageReport.ts +++ b/src/helpers/transformDeployCoverageReport.ts @@ -11,8 +11,6 @@ import { } from './types.js'; import { getPackageDirectories } from './getPackageDirectories.js'; import { findFilePath } from './findFilePath.js'; -import { setCoveredLinesSonar } from './setCoveredLinesSonar.js'; -import { setCoveredLinesCobertura } from './setCoveredLinesCobertura.js'; import { normalizePathToUnix } from './normalizePathToUnix.js'; import { generateXml } from './generateXml.js'; @@ -87,20 +85,13 @@ export async function transformDeployCoverageReport( .map(Number); if (format === 'sonar') { - await handleSonarFormat( - relativeFilePath, - uncoveredLines, - coveredLines, - repoRoot, - coverageObj as SonarCoverageObject - ); + handleSonarFormat(relativeFilePath, fileInfo.s, coverageObj as SonarCoverageObject); } else { - await handleCoberturaFormat( + handleCoberturaFormat( relativeFilePath, formattedFileName, uncoveredLines, coveredLines, - repoRoot, coverageObj as CoberturaCoverageObject, packageObj! ); @@ -113,34 +104,32 @@ export async function transformDeployCoverageReport( return { xml, warnings, filesProcessed }; } -async function handleSonarFormat( - filePath: string, - uncoveredLines: number[], - coveredLines: number[], - repoRoot: string, - coverageObj: SonarCoverageObject -): Promise { +function handleSonarFormat(filePath: string, lines: Record, coverageObj: SonarCoverageObject): void { const fileObj: SonarClass = { '@path': normalizePathToUnix(filePath), - lineToCover: uncoveredLines.map((lineNumber) => ({ - '@lineNumber': lineNumber, - '@covered': 'false', - })), + lineToCover: [], }; - await setCoveredLinesSonar(coveredLines, uncoveredLines, repoRoot, filePath, fileObj); + for (const lineNumberString in lines) { + if (!Object.hasOwn(lines, lineNumberString)) continue; + const covered = lines[lineNumberString] === 1 ? 'true' : 'false'; + fileObj.lineToCover.push({ + '@lineNumber': Number(lineNumberString), + '@covered': covered, + }); + } + coverageObj.coverage.file.push(fileObj); } -async function handleCoberturaFormat( +function handleCoberturaFormat( filePath: string, fileName: string, uncoveredLines: number[], coveredLines: number[], - repoRoot: string, coverageObj: CoberturaCoverageObject, packageObj: CoberturaPackage -): Promise { +): void { const classObj: CoberturaClass = { '@name': fileName, '@filename': normalizePathToUnix(filePath), @@ -154,12 +143,15 @@ async function handleCoberturaFormat( '@hits': 0, '@branch': 'false', })), + ...coveredLines.map((lineNumber) => ({ + '@number': lineNumber, + '@hits': 1, + '@branch': 'false', + })), ], }, }; - await setCoveredLinesCobertura(coveredLines, uncoveredLines, repoRoot, filePath, classObj); - coverageObj.coverage['@lines-valid'] += uncoveredLines.length + coveredLines.length; coverageObj.coverage['@lines-covered'] += coveredLines.length; packageObj.classes.class.push(classObj); 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/acc-transformer/transform.nut.ts b/test/commands/acc-transformer/transform.nut.ts index 1181d29..338fca7 100644 --- a/test/commands/acc-transformer/transform.nut.ts +++ b/test/commands/acc-transformer/transform.nut.ts @@ -1,6 +1,6 @@ 'use strict'; -import { copyFile, writeFile, readFile, rm, mkdir } from 'node:fs/promises'; +import { writeFile, readFile, rm, mkdir } from 'node:fs/promises'; import { strictEqual } from 'node:assert'; import { resolve } from 'node:path'; @@ -9,8 +9,10 @@ import { expect } from 'chai'; describe('acc-transformer transform NUTs', () => { let session: TestSession; - 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'); @@ -29,7 +31,6 @@ describe('acc-transformer transform NUTs', () => { packageDirectories: [{ path: 'force-app', default: true }, { path: 'packaged' }], namespace: '', sfdcLoginUrl: 'https://login.salesforce.com', - sourceApiVersion: '58.0', }; const configJsonString = JSON.stringify(configFile, null, 2); @@ -38,15 +39,15 @@ describe('acc-transformer transform NUTs', () => { await writeFile(sfdxConfigFile, configJsonString); 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); }); after(async () => { await session?.clean(); await rm(sfdxConfigFile); - 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(sonarXmlPath1); diff --git a/test/commands/acc-transformer/transform.test.ts b/test/commands/acc-transformer/transform.test.ts index 95cf9d8..2899865 100644 --- a/test/commands/acc-transformer/transform.test.ts +++ b/test/commands/acc-transformer/transform.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/acc-transformer/transfor 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'); @@ -32,16 +34,15 @@ describe('main', () => { packageDirectories: [{ path: 'force-app', default: true }, { path: 'packaged' }], namespace: '', sfdcLoginUrl: 'https://login.salesforce.com', - sourceApiVersion: '58.0', }; const configJsonString = JSON.stringify(configFile, null, 2); before(async () => { + await writeFile(sfdxConfigFile, configJsonString); 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(sfdxConfigFile, configJsonString); + await writeFile(baselineClassPath, mockClassContent); + await writeFile(baselineTriggerPath, mockTriggerContent); }); beforeEach(() => { @@ -54,8 +55,8 @@ describe('main', () => { after(async () => { await rm(sfdxConfigFile); - 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(sonarXmlPath1); diff --git a/test/deploy_coverage_baseline.xml b/test/deploy_coverage_baseline.xml index 8a1a438..f1e856d 100644 --- a/test/deploy_coverage_baseline.xml +++ b/test/deploy_coverage_baseline.xml @@ -3,46 +3,46 @@ + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - + + @@ -55,15 +55,15 @@ - - - - - - - - - - + + + + + + + + + + \ No newline at end of file