From 380ea16ba5d789313f471b813366b3815ee6ad3c Mon Sep 17 00:00:00 2001 From: peternhale Date: Tue, 6 Oct 2026 09:13:02 -0600 Subject: [PATCH] fix: use reported covered lines (W-16286380) --- .gitignore | 2 ++ package.json | 2 +- schemas/project-delete-source.json | 23 ++++++++++++++++ schemas/project-deploy-cancel.json | 23 ++++++++++++++++ schemas/project-deploy-quick.json | 23 ++++++++++++++++ schemas/project-deploy-report.json | 23 ++++++++++++++++ schemas/project-deploy-resume.json | 23 ++++++++++++++++ schemas/project-deploy-start.json | 23 ++++++++++++++++ schemas/project-deploy-validate.json | 23 ++++++++++++++++ src/formatters/deployResultFormatter.ts | 10 +++++-- src/utils/coverage.ts | 16 ++++++++--- test/utils/coverage.test.ts | 35 ++++++++++++++++++++++++- yarn.lock | 8 +++--- 13 files changed, 222 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index fe0ce4c31..3637b330b 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,5 @@ node_modules # ignore generated nut tests test/nuts/generated/ +/.fastcontext +/.sf diff --git a/package.json b/package.json index 23a044d87..a49513fb6 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "@salesforce/kit": "^4.0.0", "@salesforce/plugin-info": "^4.0.10", "@salesforce/sf-plugins-core": "^13.0.4", - "@salesforce/source-deploy-retrieve": "^13.4.3", + "@salesforce/source-deploy-retrieve": "^13.4.4", "@salesforce/source-tracking": "^8.1.1", "@salesforce/ts-types": "^3.2.0", "ansis": "^3.17.0", diff --git a/schemas/project-delete-source.json b/schemas/project-delete-source.json index f73ed2381..db7d82883 100644 --- a/schemas/project-delete-source.json +++ b/schemas/project-delete-source.json @@ -341,6 +341,19 @@ "id": { "type": "string" }, + "locationsCovered": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/definitions/LocationsCovered" + } + }, + { + "$ref": "#/definitions/LocationsCovered" + } + ] + }, "locationsNotCovered": { "anyOf": [ { @@ -370,6 +383,16 @@ "required": ["id", "name", "numLocations", "numLocationsNotCovered", "type"], "additionalProperties": false }, + "LocationsCovered": { + "type": "object", + "properties": { + "line": { + "type": "string" + } + }, + "required": ["line"], + "additionalProperties": false + }, "LocationsNotCovered": { "type": "object", "properties": { diff --git a/schemas/project-deploy-cancel.json b/schemas/project-deploy-cancel.json index f68f19037..d3b4d6657 100644 --- a/schemas/project-deploy-cancel.json +++ b/schemas/project-deploy-cancel.json @@ -389,6 +389,19 @@ "id": { "type": "string" }, + "locationsCovered": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/definitions/LocationsCovered" + } + }, + { + "$ref": "#/definitions/LocationsCovered" + } + ] + }, "locationsNotCovered": { "anyOf": [ { @@ -418,6 +431,16 @@ "required": ["id", "name", "numLocations", "numLocationsNotCovered", "type"], "additionalProperties": false }, + "LocationsCovered": { + "type": "object", + "properties": { + "line": { + "type": "string" + } + }, + "required": ["line"], + "additionalProperties": false + }, "LocationsNotCovered": { "type": "object", "properties": { diff --git a/schemas/project-deploy-quick.json b/schemas/project-deploy-quick.json index f68f19037..d3b4d6657 100644 --- a/schemas/project-deploy-quick.json +++ b/schemas/project-deploy-quick.json @@ -389,6 +389,19 @@ "id": { "type": "string" }, + "locationsCovered": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/definitions/LocationsCovered" + } + }, + { + "$ref": "#/definitions/LocationsCovered" + } + ] + }, "locationsNotCovered": { "anyOf": [ { @@ -418,6 +431,16 @@ "required": ["id", "name", "numLocations", "numLocationsNotCovered", "type"], "additionalProperties": false }, + "LocationsCovered": { + "type": "object", + "properties": { + "line": { + "type": "string" + } + }, + "required": ["line"], + "additionalProperties": false + }, "LocationsNotCovered": { "type": "object", "properties": { diff --git a/schemas/project-deploy-report.json b/schemas/project-deploy-report.json index f68f19037..d3b4d6657 100644 --- a/schemas/project-deploy-report.json +++ b/schemas/project-deploy-report.json @@ -389,6 +389,19 @@ "id": { "type": "string" }, + "locationsCovered": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/definitions/LocationsCovered" + } + }, + { + "$ref": "#/definitions/LocationsCovered" + } + ] + }, "locationsNotCovered": { "anyOf": [ { @@ -418,6 +431,16 @@ "required": ["id", "name", "numLocations", "numLocationsNotCovered", "type"], "additionalProperties": false }, + "LocationsCovered": { + "type": "object", + "properties": { + "line": { + "type": "string" + } + }, + "required": ["line"], + "additionalProperties": false + }, "LocationsNotCovered": { "type": "object", "properties": { diff --git a/schemas/project-deploy-resume.json b/schemas/project-deploy-resume.json index f68f19037..d3b4d6657 100644 --- a/schemas/project-deploy-resume.json +++ b/schemas/project-deploy-resume.json @@ -389,6 +389,19 @@ "id": { "type": "string" }, + "locationsCovered": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/definitions/LocationsCovered" + } + }, + { + "$ref": "#/definitions/LocationsCovered" + } + ] + }, "locationsNotCovered": { "anyOf": [ { @@ -418,6 +431,16 @@ "required": ["id", "name", "numLocations", "numLocationsNotCovered", "type"], "additionalProperties": false }, + "LocationsCovered": { + "type": "object", + "properties": { + "line": { + "type": "string" + } + }, + "required": ["line"], + "additionalProperties": false + }, "LocationsNotCovered": { "type": "object", "properties": { diff --git a/schemas/project-deploy-start.json b/schemas/project-deploy-start.json index f68f19037..d3b4d6657 100644 --- a/schemas/project-deploy-start.json +++ b/schemas/project-deploy-start.json @@ -389,6 +389,19 @@ "id": { "type": "string" }, + "locationsCovered": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/definitions/LocationsCovered" + } + }, + { + "$ref": "#/definitions/LocationsCovered" + } + ] + }, "locationsNotCovered": { "anyOf": [ { @@ -418,6 +431,16 @@ "required": ["id", "name", "numLocations", "numLocationsNotCovered", "type"], "additionalProperties": false }, + "LocationsCovered": { + "type": "object", + "properties": { + "line": { + "type": "string" + } + }, + "required": ["line"], + "additionalProperties": false + }, "LocationsNotCovered": { "type": "object", "properties": { diff --git a/schemas/project-deploy-validate.json b/schemas/project-deploy-validate.json index f68f19037..d3b4d6657 100644 --- a/schemas/project-deploy-validate.json +++ b/schemas/project-deploy-validate.json @@ -389,6 +389,19 @@ "id": { "type": "string" }, + "locationsCovered": { + "anyOf": [ + { + "type": "array", + "items": { + "$ref": "#/definitions/LocationsCovered" + } + }, + { + "$ref": "#/definitions/LocationsCovered" + } + ] + }, "locationsNotCovered": { "anyOf": [ { @@ -418,6 +431,16 @@ "required": ["id", "name", "numLocations", "numLocationsNotCovered", "type"], "additionalProperties": false }, + "LocationsCovered": { + "type": "object", + "properties": { + "line": { + "type": "string" + } + }, + "required": ["line"], + "additionalProperties": false + }, "LocationsNotCovered": { "type": "object", "properties": { diff --git a/src/formatters/deployResultFormatter.ts b/src/formatters/deployResultFormatter.ts index 82158bf20..0707bc9e6 100644 --- a/src/formatters/deployResultFormatter.ts +++ b/src/formatters/deployResultFormatter.ts @@ -247,7 +247,7 @@ export class DeployResultFormatter extends TestResultsFormatter implements Forma ...mapTestResults(ensureArray(runTestResult.failures)), ], codecoverage: ensureArray(runTestResult?.codeCoverage).map((cov): CodeCoverageResult => { - const [uncoveredLines, coveredLines] = generateCoveredLines(cov); + const [uncoveredLines, coveredLines] = generateCoveredLines(cov, this.getApiVersion()); const [numLocationsNum, numLinesUncovered] = getCoverageNumbers(cov); return { @@ -271,7 +271,8 @@ export class DeployResultFormatter extends TestResultsFormatter implements Forma private createCoverageReport(sourceDir: string): void { if (this.resultsDir) { const apexCoverage = transformCoverageToApexCoverage( - ensureArray(this.result.response?.details?.runTestResult?.codeCoverage) + ensureArray(this.result.response?.details?.runTestResult?.codeCoverage), + this.getApiVersion() ); fs.mkdirSync(this.resultsDir, { recursive: true }); const coverageReport = new CoverageReporter(apexCoverage, this.resultsDir, sourceDir, this.coverageOptions); @@ -279,6 +280,11 @@ export class DeployResultFormatter extends TestResultsFormatter implements Forma } } + private getApiVersion(): number | undefined { + const apiVersion = this.flags['target-org']?.getConnection().getApiVersion(); + return apiVersion === undefined ? undefined : Number(apiVersion); + } + private displayReplacements(): void { if (this.verbosity === 'verbose' && this.result.replacements?.size) { const replacements = Array.from(this.result.replacements.entries()).flatMap(([filepath, stringsReplaced]) => diff --git a/src/utils/coverage.ts b/src/utils/coverage.ts index 0969f1b64..c3fb5e11f 100644 --- a/src/utils/coverage.ts +++ b/src/utils/coverage.ts @@ -44,9 +44,14 @@ export const mapTestResults = (testResults: T[]) testTimestamp: '', })); -export const generateCoveredLines = (cov: CodeCoverage): [number[], number[]] => { - const [lineCount] = getCoverageNumbers(cov); +export const generateCoveredLines = (cov: CodeCoverage, apiVersion?: number): [number[], number[]] => { const uncoveredLines = ensureArray(cov.locationsNotCovered).map((location) => parseInt(location.line, 10)); + + if (apiVersion !== undefined && apiVersion >= 68.0 && cov.locationsCovered !== undefined) { + return [uncoveredLines, ensureArray(cov.locationsCovered).map((location) => parseInt(location.line, 10))]; + } + + const [lineCount] = getCoverageNumbers(cov); const minLineNumber = uncoveredLines.length ? Math.min(...uncoveredLines) : 1; const lines = [...Array(lineCount).keys()].map((i) => i + minLineNumber); const coveredLines = lines.filter((line) => !uncoveredLines.includes(line)); @@ -78,10 +83,13 @@ export const getCoverageFormattersOptions = (formatters: string[] = []): Coverag }; }; -export const transformCoverageToApexCoverage = (mdCoverage: CodeCoverage[]): ApexCodeCoverageAggregate => { +export const transformCoverageToApexCoverage = ( + mdCoverage: CodeCoverage[], + apiVersion?: number +): ApexCodeCoverageAggregate => { const apexCoverage = mdCoverage.map((cov): ApexCodeCoverageAggregateRecord => { const [NumLinesCovered, NumLinesUncovered] = getCoverageNumbers(cov); - const [uncoveredLines, coveredLines] = generateCoveredLines(cov); + const [uncoveredLines, coveredLines] = generateCoveredLines(cov, apiVersion); return { ApexClassOrTrigger: { diff --git a/test/utils/coverage.test.ts b/test/utils/coverage.test.ts index d67515614..048e44926 100644 --- a/test/utils/coverage.test.ts +++ b/test/utils/coverage.test.ts @@ -17,7 +17,13 @@ import { expect } from 'chai'; import { ApexTestResultOutcome } from '@salesforce/apex-node'; import { StandardColors } from '@salesforce/sf-plugins-core'; -import { coverageOutput, generateCoveredLines, getCoveragePct, mapTestResults } from '../../src/utils/coverage.js'; +import { + coverageOutput, + generateCoveredLines, + getCoveragePct, + mapTestResults, + transformCoverageToApexCoverage, +} from '../../src/utils/coverage.js'; // methods are mutating the object instead of returning new ones function getSampleTestResult() { @@ -385,6 +391,33 @@ describe('coverage utils', () => { it('will generate covered lines correctly', () => { expect(generateCoveredLines(getSampleTestResult().codeCoverage[0])).to.deep.equal([[12], [13, 14, 15]]); }); + + it('uses locationsCovered at API version 68 or later', () => { + const coverage = { + ...getSampleTestResult().codeCoverage[0], + locationsCovered: [{ line: '4' }, { line: '8' }, { line: '10' }], + }; + + expect(generateCoveredLines(coverage, 68)).to.deep.equal([[12], [4, 8, 10]]); + expect(transformCoverageToApexCoverage([coverage], 68).records[0].Coverage).to.deep.equal({ + coveredLines: [4, 8, 10], + uncoveredLines: [12], + }); + }); + + it('uses inferred covered lines before API version 68', () => { + const coverage = { + ...getSampleTestResult().codeCoverage[0], + locationsCovered: [{ line: '4' }, { line: '8' }, { line: '10' }], + }; + + expect(generateCoveredLines(coverage, 67)).to.deep.equal([[12], [13, 14, 15]]); + }); + + it('uses inferred covered lines when a v68 response omits locationsCovered', () => { + expect(generateCoveredLines(getSampleTestResult().codeCoverage[0], 68)).to.deep.equal([[12], [13, 14, 15]]); + }); + it('rounds 3 uncovered out of 44 to the nearest integer', () => { expect(getCoveragePct(getSampleTestResult().codeCoverage[1])).equal(93); }); diff --git a/yarn.lock b/yarn.lock index 2ccbc4f05..0b6781600 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1507,10 +1507,10 @@ proxy-agent "^6.5.0" yaml "^2.9.0" -"@salesforce/source-deploy-retrieve@^13.2.0", "@salesforce/source-deploy-retrieve@^13.4.3": - version "13.4.3" - resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-13.4.3.tgz#5fb0517f42d37d96a2ce7097cd7afaf5046093dd" - integrity sha512-Oj6tqoJDjV1QySUNPVWpg9eqAySP360z24DP4+bbxvvHqU+QEm08Z1/3eowRXSlwB4mUaJJBbraHTkSCycRRaw== +"@salesforce/source-deploy-retrieve@^13.2.0", "@salesforce/source-deploy-retrieve@^13.4.4": + version "13.4.4" + resolved "https://registry.yarnpkg.com/@salesforce/source-deploy-retrieve/-/source-deploy-retrieve-13.4.4.tgz#6dd851566d79f6bbcadec2f21e95967707e28fa5" + integrity sha512-zx+rUyYFtu7B3hTyXy2muhawTkXpG4eIU/BQaafX/Ku0Lki7fu+Oa0GErQdycMXuGrTyMWhCd8q+Jl3obA2rug== dependencies: "@salesforce/core" "^9.2.2" "@salesforce/kit" "^4.0.0"