├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── coverage.yml │ ├── manual-deprecate-versions.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .husky ├── commit-msg ├── pre-commit └── pre-push ├── .lintstagedrc.cjs ├── .prettierignore ├── .prettierrc.json ├── .sfdevrc.json ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── baselines ├── clover_baseline.xml ├── cobertura_baseline.xml ├── jacoco_baseline.xml ├── json_baseline.json ├── lcov_baseline.info └── sonar_baseline.xml ├── bin ├── dev.cmd ├── dev.js ├── run.cmd └── run.js ├── commitlint.config.cjs ├── defaults ├── salesforce-cli │ └── .apexcodecovtransformer.config.json └── sfdx-hardis │ └── .apexcodecovtransformer.config.json ├── inputs ├── deploy_coverage.json ├── invalid.json └── test_coverage.json ├── jest.config.js ├── messages └── transformer.transform.md ├── package.json ├── samples ├── classes │ ├── AccountHandler.cls │ └── AccountProfile.cls └── triggers │ └── AccountTrigger.trigger ├── src ├── commands │ └── acc-transformer │ │ └── transform.ts ├── handlers │ ├── clover.ts │ ├── cobertura.ts │ ├── getHandler.ts │ ├── istanbulJson.ts │ ├── jacoco.ts │ ├── lcov.ts │ └── sonar.ts ├── hooks │ └── postrun.ts ├── index.ts ├── transformers │ ├── coverageTransformer.ts │ └── reportGenerator.ts └── utils │ ├── constants.ts │ ├── findFilePath.ts │ ├── getConcurrencyThreshold.ts │ ├── getPackageDirectories.ts │ ├── getRepoRoot.ts │ ├── getTotalLines.ts │ ├── normalizePathToUnix.ts │ ├── setCoverageDataType.ts │ ├── setCoveredLines.ts │ └── types.ts ├── test ├── .eslintrc.cjs ├── commands │ └── acc-transformer │ │ ├── transform.nut.ts │ │ └── transform.test.ts ├── tsconfig.json ├── units │ ├── handler.test.ts │ ├── repoRoot.test.ts │ ├── setCoveredLines.test.ts │ └── typeGuards.test.ts └── utils │ ├── baselineCompare.ts │ ├── normalizeCoverageReport.ts │ ├── testCleanup.ts │ ├── testConstants.ts │ └── testSetup.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.cjs/ 2 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['eslint-config-salesforce-typescript', 'plugin:sf-plugin/recommended'], 3 | root: true, 4 | rules: { 5 | header: 'off', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'monthly' 7 | versioning-strategy: 'increase' 8 | labels: 9 | - 'dependencies' 10 | open-pull-requests-limit: 5 11 | pull-request-branch-name: 12 | separator: '-' 13 | commit-message: 14 | # cause a release for non-dev-deps 15 | prefix: fix(deps) 16 | # no release for dev-deps 17 | prefix-development: chore(dev-deps) 18 | ignore: 19 | - dependency-name: '@salesforce/dev-scripts' 20 | - dependency-name: '*' 21 | update-types: ['version-update:semver-major'] 22 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | on: 3 | workflow_call: 4 | push: 5 | branches-ignore: 6 | - main 7 | 8 | permissions: 9 | contents: read 10 | id-token: write # Required for OIDC 11 | 12 | jobs: 13 | coverage: 14 | name: Coverage 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest] 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4.1.1 23 | 24 | - name: Setup Node 25 | uses: actions/setup-node@v4.0.1 26 | with: 27 | node-version: 20 28 | cache: yarn 29 | registry-url: 'https://registry.npmjs.org' 30 | 31 | - name: Install Dependencies 32 | run: yarn install 33 | 34 | - name: Build 35 | run: yarn build 36 | 37 | - name: Test 38 | run: yarn test:only 39 | 40 | - name: Upload coverage 41 | if: runner.os == 'Linux' 42 | uses: qltysh/qlty-action/coverage@v1 43 | with: 44 | oidc: true 45 | files: coverage/lcov.info 46 | continue-on-error: true 47 | -------------------------------------------------------------------------------- /.github/workflows/manual-deprecate-versions.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Deprecate versions 3 | 4 | on: 5 | workflow_dispatch: 6 | inputs: 7 | version-expression: 8 | description: version number (semver format) or range to deprecate 9 | required: true 10 | type: string 11 | rationale: 12 | description: explain why this version is deprecated. No message content will un-deprecate the version 13 | type: string 14 | 15 | 16 | jobs: 17 | deprecate: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout sources 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup node 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version: 20 27 | registry-url: 'https://registry.npmjs.org' 28 | 29 | - name: Change version 30 | run: npm deprecate apex-code-coverage-transformer@$"${{ github.event.inputs.version-expression }}" "${{ github.event.inputs.rationale }}" 31 | env: 32 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | permissions: 7 | contents: write 8 | pull-requests: write 9 | id-token: write # Required for OIDC 10 | 11 | jobs: 12 | test: 13 | uses: ./.github/workflows/coverage.yml 14 | secrets: inherit 15 | release: 16 | name: Release 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Release Please 20 | id: release 21 | uses: googleapis/release-please-action@v4 22 | with: 23 | release-type: node 24 | 25 | - name: Checkout 26 | uses: actions/checkout@v4.1.1 27 | if: ${{ steps.release.outputs.release_created == 'true' }} 28 | 29 | - name: Setup Node 30 | uses: actions/setup-node@v4.0.1 31 | with: 32 | node-version: 20 33 | cache: yarn 34 | registry-url: 'https://registry.npmjs.org' 35 | if: ${{ steps.release.outputs.release_created == 'true' }} 36 | 37 | - name: Install Dependencies 38 | run: yarn install 39 | if: ${{ steps.release.outputs.release_created == 'true' }} 40 | 41 | - name: Build 42 | run: yarn build 43 | if: ${{ steps.release.outputs.release_created == 'true' }} 44 | 45 | - name: Publish to NPM 46 | run: npm publish --access public --tag latest 47 | env: 48 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | if: ${{ steps.release.outputs.release_created == 'true' }} 50 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - main 7 | paths-ignore: 8 | - '**.md' 9 | 10 | jobs: 11 | unit-tests: 12 | uses: salesforcecli/github-workflows/.github/workflows/unitTest.yml@main 13 | nuts: 14 | needs: unit-tests 15 | uses: salesforcecli/github-workflows/.github/workflows/nut.yml@main 16 | secrets: inherit 17 | strategy: 18 | matrix: 19 | os: [windows-latest] 20 | fail-fast: false 21 | with: 22 | os: ${{ matrix.os }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # -- CLEAN 2 | tmp/ 3 | # use yarn by default, so ignore npm 4 | package-lock.json 5 | 6 | .sfdx/ 7 | 8 | # never checkin npm config 9 | .npmrc 10 | 11 | # debug logs 12 | npm-error.log 13 | yarn-error.log 14 | 15 | 16 | # compile source 17 | lib 18 | 19 | # test artifacts 20 | *xunit.xml 21 | *checkstyle.xml 22 | *unitcoverage 23 | .nyc_output 24 | coverage 25 | test_session* 26 | 27 | # generated docs 28 | docs 29 | 30 | # ignore sfdx-trust files 31 | *.tgz 32 | *.sig 33 | package.json.bak. 34 | 35 | # -- CLEAN ALL 36 | *.tsbuildinfo 37 | .eslintcache 38 | .wireit 39 | node_modules 40 | 41 | # -- 42 | # put files here you don't want cleaned with sf-clean 43 | 44 | # os specific files 45 | .DS_Store 46 | .idea 47 | 48 | oclif.manifest.json 49 | 50 | oclif.lock 51 | *.log 52 | stderr*.txt 53 | stdout*.txt 54 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | yarn commitlint --edit -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn lint && yarn pretty-quick --staged && yarn build -------------------------------------------------------------------------------- /.husky/pre-push: -------------------------------------------------------------------------------- 1 | yarn build -------------------------------------------------------------------------------- /.lintstagedrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '**/*.{js,json,md}?(x)': () => 'npm run reformat', 3 | }; 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | baselines/json_baseline.json 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | "@salesforce/prettier-config" 2 | -------------------------------------------------------------------------------- /.sfdevrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "testsPath": "test/**/*.test.ts" 4 | }, 5 | "wireit": { 6 | "test": { 7 | "dependencies": ["test:compile", "test:only", "lint"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "attach", 10 | "name": "Attach", 11 | "port": 9229, 12 | "skipFiles": ["/**"] 13 | }, 14 | { 15 | "name": "Run All Tests", 16 | "type": "node", 17 | "request": "launch", 18 | "program": "${workspaceFolder}/node_modules/mocha/bin/mocha", 19 | "args": ["--inspect", "--colors", "test/**/*.test.ts"], 20 | "env": { 21 | "NODE_ENV": "development", 22 | "SFDX_ENV": "development" 23 | }, 24 | "sourceMaps": true, 25 | "smartStep": true, 26 | "internalConsoleOptions": "openOnSessionStart", 27 | "preLaunchTask": "Compile tests" 28 | }, 29 | { 30 | "type": "node", 31 | "request": "launch", 32 | "name": "Run Current Test", 33 | "program": "${workspaceFolder}/node_modules/mocha/bin/mocha", 34 | "args": ["--inspect", "--colors", "${file}"], 35 | "env": { 36 | "NODE_ENV": "development", 37 | "SFDX_ENV": "development" 38 | }, 39 | "sourceMaps": true, 40 | "smartStep": true, 41 | "internalConsoleOptions": "openOnSessionStart", 42 | "preLaunchTask": "Compile tests" 43 | } 44 | ] 45 | } 46 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.svn": true, 5 | "**/.hg": true, 6 | "**/CVS": true, 7 | "**/.DS_Store": true 8 | }, 9 | "search.exclude": { 10 | "**/lib": true, 11 | "**/bin": true 12 | }, 13 | "editor.tabSize": 2, 14 | "editor.formatOnSave": true, 15 | "rewrap.wrappingColumn": 80 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "problemMatcher": "$tsc", 4 | "tasks": [ 5 | { 6 | "label": "Compile tests", 7 | "group": { 8 | "kind": "build", 9 | "isDefault": true 10 | }, 11 | "command": "yarn", 12 | "type": "shell", 13 | "presentation": { 14 | "focus": false, 15 | "panel": "dedicated" 16 | }, 17 | "args": ["run", "pretest"], 18 | "isBackground": false 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Changelog 5 | 6 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 7 | 8 | ## [2.13.3](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.13.2...v2.13.3) (2025-07-14) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * upgrade node requirement to 20 ([#207](https://github.com/mcarvin8/apex-code-coverage-transformer/issues/207)) ([7507e18](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/7507e18a25a7b3dd00a0db95e602414a7e2c1b8b)) 14 | 15 | ## [2.13.2](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.13.1...v2.13.2) (2025-07-01) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **deps:** bump @oclif/core from 4.3.0 to 4.4.0 ([0fa1a48](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/0fa1a4885bd69dc61da539f62c6122fab75288e4)) 21 | * upgrade @salesforce/core from 8.11.4 to 8.12.0 ([5b5bee2](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/5b5bee20e3c658457b28c5ed2c4ee4d3a783f3ad)) 22 | 23 | ## [2.13.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.13.0...v2.13.1) (2025-06-24) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * set format amount in main function ([#195](https://github.com/mcarvin8/apex-code-coverage-transformer/issues/195)) ([b63c2a2](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/b63c2a2c6fada03986b34cf62145b126147c3f99)) 29 | 30 | ## [2.13.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.12.1...v2.13.0) (2025-06-24) 31 | 32 | 33 | ### Features 34 | 35 | * allow multiple output formats ([#193](https://github.com/mcarvin8/apex-code-coverage-transformer/issues/193)) ([58c024d](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/58c024dff5e5bdba983ba55ce15ca0d7c25b4a98)) 36 | 37 | ## [2.12.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.12.0...v2.12.1) (2025-06-22) 38 | 39 | 40 | ### Bug Fixes 41 | 42 | * upgrade @salesforce/sf-plugins-core from 12.2.1 to 12.2.2 ([#191](https://github.com/mcarvin8/apex-code-coverage-transformer/issues/191)) ([0fd939d](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/0fd939d8e2977d2f8f90238e339b5116f573affa)) 43 | 44 | ## [2.12.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.8...v2.12.0) (2025-06-17) 45 | 46 | 47 | ### Features 48 | 49 | * add istanbul json format ([#188](https://github.com/mcarvin8/apex-code-coverage-transformer/issues/188)) ([9ace6a7](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/9ace6a752be3d10ee846a8f055f1231559eeb695)) 50 | 51 | ## [2.11.8](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.7...v2.11.8) (2025-06-05) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * move code from command into function ([#184](https://github.com/mcarvin8/apex-code-coverage-transformer/issues/184)) ([1d9beea](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/1d9beeadb6c4489427d40f9e1f54baaf2d13bad0)) 57 | 58 | ## [2.11.7](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.6...v2.11.7) (2025-06-03) 59 | 60 | 61 | ### Bug Fixes 62 | 63 | * **deps:** bump @salesforce/core from 8.10.1 to 8.11.4 ([#178](https://github.com/mcarvin8/apex-code-coverage-transformer/issues/178)) ([a8fbd0c](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/a8fbd0c07816d31fa907c8623b18a98447173152)) 64 | 65 | ## [2.11.6](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.5...v2.11.6) (2025-05-28) 66 | 67 | 68 | ### Bug Fixes 69 | 70 | * move read and write file from command to functions ([#176](https://github.com/mcarvin8/apex-code-coverage-transformer/issues/176)) ([f698080](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/f6980808ab168a670719c77d0cdfeb7326d3b7bc)) 71 | 72 | ## [2.11.5](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.4...v2.11.5) (2025-05-26) 73 | 74 | 75 | ### Bug Fixes 76 | 77 | * correct complex binary expressions in type guards ([#174](https://github.com/mcarvin8/apex-code-coverage-transformer/issues/174)) ([291672b](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/291672bc6a148252b1c8c40e4de8865adae5ab37)) 78 | 79 | ## [2.11.4](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.3...v2.11.4) (2025-05-26) 80 | 81 | 82 | ### Bug Fixes 83 | 84 | * merge coverage transformer functions ([5eadaac](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/5eadaacff9e20cc72e330a3dcd44aa05dd6bf5a4)) 85 | 86 | ## [2.11.3](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.2...v2.11.3) (2025-05-22) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * improve code quality on functions ([604300b](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/604300bf8605576a6cfc8f760f88365250627d31)) 92 | * reduce complexity when setting coverage data type ([51d1f20](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/51d1f205aa8dbb6635e6b302cc0050dfd3b03b84)) 93 | 94 | ## [2.11.2](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.1...v2.11.2) (2025-05-06) 95 | 96 | 97 | ### Bug Fixes 98 | 99 | * **deps:** bump @oclif/core from 4.2.10 to 4.3.0 ([e02c2c4](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/e02c2c4e85636a1d64da1783d39db3a46b118bc2)) 100 | 101 | ## [2.11.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.0...v2.11.1) (2025-03-25) 102 | 103 | 104 | ### Bug Fixes 105 | 106 | * **deps:** bump @salesforce/core from 8.8.5 to 8.8.6 ([3cdbe52](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/3cdbe52bfee45d34d8d355f61451529f9fadaeb3)) 107 | * **deps:** bump @salesforce/sf-plugins-core from 12.2.0 to 12.2.1 ([60d3f72](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/60d3f72719fad96d46a55c02e616ab81e2f8d686)) 108 | 109 | ## [2.11.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.10.0...v2.11.0) (2025-03-07) 110 | 111 | 112 | ### Features 113 | 114 | * include ignore package directories flag in hook ([a7294a4](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/a7294a47a85141d4b1d0e21854ac14d4f19b9e14)) 115 | 116 | 117 | ### Bug Fixes 118 | 119 | * **deps:** bump @salesforce/sf-plugins-core from 12.1.4 to 12.2.0 ([b96e83c](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/b96e83cc28c5f3ae82422c069fa157caa5e805b1)) 120 | 121 | ## [2.10.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.9.0...v2.10.0) (2025-02-26) 122 | 123 | 124 | ### Features 125 | 126 | * dynamically determine cobertura package names from file paths ([cbf63df](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/cbf63dfc44ec79c9ec4e58bdc68bb9458f925e4f)) 127 | 128 | 129 | ### Bug Fixes 130 | 131 | * **deps:** bump @oclif/core from 4.2.6 to 4.2.8 ([b838b3f](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/b838b3f6a47eb8fee04d6d8a1321fc82c949cfe4)) 132 | * **deps:** bump @salesforce/core from 8.8.2 to 8.8.3 ([c395e9c](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/c395e9cf8e38fdd606b594589376b9670e7f220c)) 133 | 134 | ## [2.9.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.8.7...v2.9.0) (2025-02-16) 135 | 136 | 137 | ### Features 138 | 139 | * add ignore package directories flag ([3666c59](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/3666c59bdd675c1610c2409bddbd11fe4ed03719)) 140 | 141 | ## [2.8.7](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.8.6...v2.8.7) (2025-02-13) 142 | 143 | 144 | ### Bug Fixes 145 | 146 | * **deps:** bump @oclif/core from 4.2.5 to 4.2.6 ([b1988e7](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/b1988e7975c907c94a69e580778b8ded326619ba)) 147 | * **deps:** bump @salesforce/sf-plugins-core from 12.1.3 to 12.1.4 ([cce162f](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/cce162f9c9db5b5353c2301c160c70a717198cc9)) 148 | * fix jacoco source file paths ([1a0af9f](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/1a0af9fb6825e21e9b481e06faaa8bf0c8740dc2)) 149 | 150 | ## [2.8.6](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.8.5...v2.8.6) (2025-02-08) 151 | 152 | 153 | ### Bug Fixes 154 | 155 | * remove jacoco class and add custom header ([6c87aff](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/6c87aff315bac4388dbcde7a499d470604fad65e)) 156 | 157 | ## [2.8.5](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.8.4...v2.8.5) (2025-02-08) 158 | 159 | 160 | ### Bug Fixes 161 | 162 | * move class counter jacoco ([610e73d](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/610e73d49c7bdca7d7cdd120643c535994cbfe99)) 163 | 164 | ## [2.8.4](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.8.3...v2.8.4) (2025-02-08) 165 | 166 | 167 | ### Bug Fixes 168 | 169 | * jacoco structuring per package directory ([5efc698](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/5efc69853d6f5498e0a20bb3f94b3e4068b3d528)) 170 | 171 | ## [2.8.3](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.8.2...v2.8.3) (2025-02-08) 172 | 173 | 174 | ### Bug Fixes 175 | 176 | * add package counter for jacoco ([88b620a](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/88b620a27f5e12a9f30a9369e4920880d1f924da)) 177 | 178 | ## [2.8.2](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.8.1...v2.8.2) (2025-02-07) 179 | 180 | 181 | ### Bug Fixes 182 | 183 | * include file path in jacoco sourcefile ([c3df27e](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/c3df27ed3544bd7145267b3cdde617f2719bd034)) 184 | 185 | ## [2.8.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.8.0...v2.8.1) (2025-02-07) 186 | 187 | 188 | ### Bug Fixes 189 | 190 | * jacoco sourcefile structure ([0a1275f](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/0a1275f2d6ef5732c06568d183e4aa8e5def8951)) 191 | 192 | ## [2.8.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.7.2...v2.8.0) (2025-02-07) 193 | 194 | 195 | ### Features 196 | 197 | * add jacoco format ([ecd74f6](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/ecd74f6cc79663b0a0a1bae024620eb69426de60)) 198 | 199 | ## [2.7.2](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.7.1...v2.7.2) (2025-02-02) 200 | 201 | 202 | ### Bug Fixes 203 | 204 | * **deps:** bump @salesforce/sf-plugins-core from 12.1.1 to 12.1.3 ([28f0eeb](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/28f0eeb83ef1855bc0fd5f4c5fb186a6aaeaa5a9)) 205 | 206 | ## [2.7.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.7.0...v2.7.1) (2025-01-16) 207 | 208 | 209 | ### Bug Fixes 210 | 211 | * sorting on each coverage format handler ([b7b9244](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/b7b9244dddc54c1c96b88fafa81ba561c946c0ac)) 212 | 213 | ## [2.7.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.6.1...v2.7.0) (2025-01-15) 214 | 215 | 216 | ### Features 217 | 218 | * add async processing and sort files in report ([ef0c011](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/ef0c011895f378e7aedc5c6568b9b9ec0e9574bf)) 219 | 220 | ## [2.6.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.6.0...v2.6.1) (2025-01-15) 221 | 222 | 223 | ### Bug Fixes 224 | 225 | * fix default test command path in doc and provide defaults ([c8dc711](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/c8dc71177d3ed9cae8dc7a41b7843fdc9d09ad5d)) 226 | 227 | ## [2.6.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.5.2...v2.6.0) (2025-01-12) 228 | 229 | 230 | ### Features 231 | 232 | * allow hook to run after hardis commands ([962847d](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/962847d640237bd8c6c16f7319e2756b005713ea)) 233 | 234 | ## [2.5.2](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.5.1...v2.5.2) (2025-01-10) 235 | 236 | 237 | ### Bug Fixes 238 | 239 | * confirm deploy report lines are sorted before covered line adjustment ([9ffb7f5](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/9ffb7f55343c45b876006015ebc11818c63174ce)) 240 | * only set uncovered and covered lines when needed ([45f77ae](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/45f77aed39fbb1cac8329c0502cc9de8253be666)) 241 | * warn instead of fail when json is not found ([7445801](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/744580187ca9ed67964f13e80f26a207d45a62bf)) 242 | 243 | ## [2.5.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.5.0...v2.5.1) (2025-01-09) 244 | 245 | 246 | ### Bug Fixes 247 | 248 | * set covered lines before processing format ([d96bd88](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/d96bd88b341dbb8abe49ece21252e998abae617b)) 249 | 250 | ## [2.5.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.4.1...v2.5.0) (2025-01-08) 251 | 252 | 253 | ### Features 254 | 255 | * add support for lcovonly info format ([a663cb2](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/a663cb2db49e62a8c6cf278fe6a7e28c9e0a94c6)) 256 | * rename xml flag to output-report ([bb25a47](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/bb25a478e285dc7de612368c51243e0525a71b02)) 257 | 258 | ## [2.4.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.4.0...v2.4.1) (2024-12-22) 259 | 260 | 261 | ### Bug Fixes 262 | 263 | * fix clover file level metrics in deploy reports ([cddded4](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/cddded4f996771af1fafa07213f7e9866f4c1191)) 264 | 265 | ## [2.4.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.3.0...v2.4.0) (2024-12-21) 266 | 267 | 268 | ### Features 269 | 270 | * add support for clover format ([00ffa74](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/00ffa742455609028f36d48f9a4cabc86aa32ecb)) 271 | 272 | ## [2.3.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.2.1...v2.3.0) (2024-12-17) 273 | 274 | 275 | ### Features 276 | 277 | * add support for cobertura format ([41a3b25](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/41a3b2583ce1014ccd04f935c5c8b87831bee2e7)) 278 | 279 | ## [2.2.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.2.0...v2.2.1) (2024-12-14) 280 | 281 | 282 | ### Bug Fixes 283 | 284 | * upgrade all dependencies ([7addc5d](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/7addc5dc6ec72f75a26bc1306ae42eee74daea25)) 285 | 286 | ## [2.2.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.1.0...v2.2.0) (2024-10-28) 287 | 288 | 289 | ### Features 290 | 291 | * remove dependency on git repos ([c2bc72d](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/c2bc72d72de00da8b52f3f1ae0a6b8805316b20f)) 292 | 293 | ## [2.1.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.0.0...v2.1.0) (2024-10-22) 294 | 295 | 296 | ### Features 297 | 298 | * remove command flag by adding type guard functions ([be380bf](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/be380bf2400cb217a3769d6272668d6ba277a6a4)) 299 | 300 | ## [2.0.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.7.6...v2.0.0) (2024-10-21) 301 | 302 | 303 | ### ⚠ BREAKING CHANGES 304 | 305 | * shorten command to acc-transformer 306 | 307 | ### Features 308 | 309 | * shorten command to acc-transformer ([686cdc6](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/686cdc6960f8f73e7796e6b97928a8294a6b450b)) 310 | 311 | ## [1.7.6](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.7.5...v1.7.6) (2024-07-28) 312 | 313 | 314 | ### Bug Fixes 315 | 316 | * refactor fs import ([d246c28](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/d246c28f70f5cd2ac40110b2d376ad0bac59384d)) 317 | 318 | ## [1.7.5](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.7.4...v1.7.5) (2024-07-28) 319 | 320 | 321 | ### Bug Fixes 322 | 323 | * switch to isomorphic-git ([2da1fcc](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/2da1fcc2f55cf76296d1b7cd033daedba1bf496d)) 324 | 325 | ## [1.7.4](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.7.3...v1.7.4) (2024-06-10) 326 | 327 | ### Bug Fixes 328 | 329 | - include `apex get test` command in the hook ([34718f0](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/34718f0c146dbf51efe04c81d994c0c724e84a0c)) 330 | - update the hook to allow 2 different JSON paths based on command ([f6159be](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/f6159be3fe12801a00c06f44a5304cab2d02697e)) 331 | 332 | ## [1.7.3](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.7.2...v1.7.3) (2024-05-15) 333 | 334 | ### Bug Fixes 335 | 336 | - fix handling of an empty JSON from the test run command ([48ef6df](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/48ef6df77eda762006a78e1d2eb66936d57ee418)) 337 | - warn instead of fail when no files in the JSON are processed ([7809843](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/780984332c279ed7142695cbf262e67f69e916c0)) 338 | 339 | ## [1.7.2](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.7.1...v1.7.2) (2024-05-10) 340 | 341 | ### Bug Fixes 342 | 343 | - add support for coverage JSONs created by `sf apex run test` ([5f48b77](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/5f48b777f1ccd003d650c50ef87a0b24e2b4a73f)) 344 | 345 | ## [1.7.2-beta.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.7.1...v1.7.2-beta.1) (2024-05-09) 346 | 347 | ### Bug Fixes 348 | 349 | - add support for coverage JSONs created by `sf apex run test` ([5f48b77](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/5f48b777f1ccd003d650c50ef87a0b24e2b4a73f)) 350 | 351 | ## [1.7.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.7.0...v1.7.1) (2024-04-30) 352 | 353 | ### Bug Fixes 354 | 355 | - fix no-map replacement for windows-style paths ([e6d4fef](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/e6d4fef16266a8075c01601b3f5a838a058c5fa2)) 356 | 357 | # [1.7.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.8...v1.7.0) (2024-04-30) 358 | 359 | ### Features 360 | 361 | - add a post run hook for post deployments ([894a4cd](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/894a4cdb14f6367b3e9248e757b1463e3134ae83)) 362 | 363 | ## [1.6.8](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.7...v1.6.8) (2024-04-23) 364 | 365 | ### Bug Fixes 366 | 367 | - get package directories and repo root before looping through files ([b630002](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/b6300020d650e02928c3b5700b05aa0c4bda050e)) 368 | 369 | ## [1.6.7](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.6...v1.6.7) (2024-04-23) 370 | 371 | ### Bug Fixes 372 | 373 | - remove `--sfdx-configuration` flag and get `sfdx-project.json` path using `simple-git` ([87d92f3](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/87d92f39fdd4f404887dc2c931940ea3221d7606)) 374 | 375 | ## [1.6.6](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.5...v1.6.6) (2024-04-22) 376 | 377 | ### Bug Fixes 378 | 379 | - fix path resolution when running in non-root directories ([c16fe7d](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/c16fe7d09898b857c68c2cc73a1ac2bcc8665f1e)) 380 | 381 | ## [1.6.5](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.4...v1.6.5) (2024-04-18) 382 | 383 | ### Bug Fixes 384 | 385 | - build XML using xmlbuilder2 and normalize path building using posix ([c6d6d94](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/c6d6d944e2675eb6be0dd90e972d67e6311f6738)) 386 | 387 | ## [1.6.4](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.3...v1.6.4) (2024-04-09) 388 | 389 | ### Bug Fixes 390 | 391 | - switch to promises/async and refactor imports ([2577692](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/2577692672aeb9271151f67e8347ef8a09a07b37)) 392 | 393 | ## [1.6.3](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.2...v1.6.3) (2024-03-27) 394 | 395 | ### Bug Fixes 396 | 397 | - remove flow logic ([dd3db2a](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/dd3db2ab60614dd21bdc844b78c8e882814c43a8)) 398 | 399 | ## [1.6.2](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.1...v1.6.2) (2024-03-26) 400 | 401 | ### Bug Fixes 402 | 403 | - warn if a file isn't found and fail if no files are found ([6b9e8ea](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/6b9e8ea80f9101429f82eed7fb3539c71dc613f4)) 404 | 405 | ## [1.6.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.0...v1.6.1) (2024-03-22) 406 | 407 | ### Bug Fixes 408 | 409 | - search the directories recursively without hard-coding the sub-folder names ([8880ab3](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/8880ab3e3c9097b8e7927230395eae32560ae55a)) 410 | 411 | ## [1.6.1-beta.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.0...v1.6.1-beta.1) (2024-03-22) 412 | 413 | ### Bug Fixes 414 | 415 | - search the directories recursively without hard-coding the sub-folder names ([8880ab3](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/8880ab3e3c9097b8e7927230395eae32560ae55a)) 416 | 417 | # [1.6.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.5.0...v1.6.0) (2024-03-21) 418 | 419 | ### Features 420 | 421 | - add `covered` lines, renumbering out-of-range lines numbers ([1733b09](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/1733b09063eac28f0e627e66080dcd24d7c74bf9)) 422 | 423 | # [1.6.0-beta.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.5.0...v1.6.0-beta.1) (2024-03-20) 424 | 425 | ### Features 426 | 427 | - add `covered` lines, renumbering out-of-range lines numbers ([1733b09](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/1733b09063eac28f0e627e66080dcd24d7c74bf9)) 428 | 429 | # [1.5.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.4.0...v1.5.0) (2024-03-20) 430 | 431 | ### Features 432 | 433 | - support multiple package directories via the sfdx-project.json ([52c1a12](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/52c1a12ff5fbfb10c215a41e010ee7fc6c0370de)) 434 | 435 | # [1.4.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.3.1...v1.4.0) (2024-02-27) 436 | 437 | ### Features 438 | 439 | - if coverage JSON includes file extensions, use that to determine paths ([efc1fa6](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/efc1fa61ce21cff394bbc696afce88c4d57894ea)) 440 | 441 | ## [1.3.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.3.0...v1.3.1) (2024-02-26) 442 | 443 | ### Bug Fixes 444 | 445 | - dx-directory should be an existing directory and fix flag in messages ([38fb20b](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/38fb20b8a107c203ba78266cb05d133805135ce4)) 446 | 447 | # [1.3.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.2.0...v1.3.0) (2024-02-07) 448 | 449 | ### Features 450 | 451 | - add support for flows ([6bf0da1](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/6bf0da14a39871dc3b7d50565416c2d24fba7524)) 452 | 453 | # [1.2.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.1.1...v1.2.0) (2024-02-07) 454 | 455 | ### Features 456 | 457 | - check if file name is an apex class or apex trigger using the dx directory flag ([215e41e](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/215e41eab0c41e2861d86370b0bddae2b2e487f0)) 458 | 459 | ## [1.1.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.1.0...v1.1.1) (2024-02-07) 460 | 461 | ### Bug Fixes 462 | 463 | - resolve path to xml ([cc75e96](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/cc75e96ef26120f86cff8588256e4f55e79d5473)) 464 | 465 | # [1.1.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.0.0...v1.1.0) (2024-02-07) 466 | 467 | ### Features 468 | 469 | - update json flag name to ensure it's unique from the global flag and import path module ([d03c567](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/d03c567a7549e5ada291d82525c78e19a1b8fcba)) 470 | 471 | # 1.0.0 (2024-02-07) 472 | 473 | ### Features 474 | 475 | - init release ([504b4cf](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/504b4cfb028fc14241b892e1cc872adadec736d7)) 476 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome! If you would like to contribute, please fork the repository, make your changes, and submit a pull request. 4 | 5 | ## Requirements 6 | 7 | - Node >= 20.0.0 8 | - yarn 9 | 10 | ## Installation 11 | 12 | ### 1) Fork the repository 13 | 14 | ### 2) Install Dependencies 15 | 16 | This will install all the tools needed to contribute 17 | 18 | ```bash 19 | yarn 20 | ``` 21 | 22 | ### 3) Build application 23 | 24 | ```bash 25 | yarn build 26 | ``` 27 | 28 | Rebuild every time you made a change in the source and you need to test locally 29 | 30 | ## Testing 31 | 32 | When developing, run the provided unit tests for new additions. New additions must meet the jest code coverage requirements. 33 | 34 | ```bash 35 | # run unit tests 36 | yarn test:only 37 | ``` 38 | 39 | To run the non-unit test, ensure you re-build the application and then run: 40 | 41 | ```bash 42 | # run non-unit tests 43 | yarn test:nuts 44 | ``` 45 | 46 | ## Adding Coverage Formats 47 | 48 | To add new coverage formats to the transformer: 49 | 50 | 1. Add the format flag value to `formatOptions` in `src/utils/constants.ts`. 51 | 2. Add new coverage types to `src/utils/types.ts` including a `{format}CoverageObject` type. Add the new `{format}CoverageObject` type to the `CoverageHandler` type under `finalize`. 52 | 53 | ```typescript 54 | export type CoverageHandler = { 55 | processFile(filePath: string, fileName: string, lines: Record): void; 56 | finalize(): SonarCoverageObject | CoberturaCoverageObject | CloverCoverageObject | LcovCoverageObject; 57 | }; 58 | ``` 59 | 60 | 3. Create a new coverage handler class in `src/handlers` with a `processFile` and `finalize` function. 61 | 1. The `finalize` function should sort items in the coverage object before returning. 62 | 4. Add the new coverage handler class to `src/handlers/getHandler.ts`. 63 | 5. Add the new `{format}CoverageObject` type to `src/transformers/reportGenerator.ts` and add anything needed to create the final report for that format, including updating the report extension in the `getExtensionForFormat` function. 64 | 6. The unit and non-unit tests will automatically run the new coverage format after it's added to the `formatOptions` constant. You will need to run the unit test suite once to generate the baseline report for the new format. 65 | 1. Add the newly generated baseline to the `baselines` folder named `{format}_baseline.{ext}` 66 | 2. Create a new test constant with the baseline path in `test/utils/testConstants.ts` 67 | 3. Add the new baseline constant to the `baselineMap` in `test/utils/baselineCompare.ts` 68 | 3. If needed, update the `test/commands/acc-transformer/normalizeCoverageReport.ts` to remove timestamps if the new format report has timestamps, i.e. Cobertura and Clover. 69 | 4. Re-run the unit test and confirm all tests pass, including the baseline compare test. 70 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Matthew Carvin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `apex-code-coverage-transformer` 2 | 3 | [![NPM](https://img.shields.io/npm/v/apex-code-coverage-transformer.svg?label=apex-code-coverage-transformer)](https://www.npmjs.com/package/apex-code-coverage-transformer) 4 | [![Downloads/week](https://img.shields.io/npm/dw/apex-code-coverage-transformer.svg)](https://npmjs.org/package/apex-code-coverage-transformer) 5 | [![License](https://img.shields.io/badge/License-MIT-yellow.svg)](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/LICENSE.md) 6 | [![Maintainability](https://qlty.sh/badges/11057a07-84da-41af-91fb-b3476e404242/maintainability.svg)](https://qlty.sh/gh/mcarvin8/projects/apex-code-coverage-transformer) 7 | [![Code Coverage](https://qlty.sh/badges/11057a07-84da-41af-91fb-b3476e404242/test_coverage.svg)](https://qlty.sh/gh/mcarvin8/projects/apex-code-coverage-transformer) 8 | [![Known Vulnerabilities](https://snyk.io//test/github/mcarvin8/apex-code-coverage-transformer/badge.svg?targetFile=package.json)](https://snyk.io//test/github/mcarvin8/apex-code-coverage-transformer?targetFile=package.json) 9 | 10 | 11 |
12 | Table of Contents 13 | 14 | - [Install](#install) 15 | - [Usage](#usage) 16 | - [Salesforce CLI](#salesforce-cli) 17 | - [SFDX Hardis](#sfdx-hardis) 18 | - [Fixes and Enhancements](#fixes-and-enhancements) 19 | - [Command](#command) 20 | - [`sf acc-transformer transform`](#sf-acc-transformer-transform) 21 | - [Coverage Report Formats](#coverage-report-formats) 22 | - [Hook](#hook) 23 | - [Troubleshooting](#troubleshooting) 24 | - [Issues](#issues) 25 | - [Contributing](#contributing) 26 | - [License](#license) 27 |
28 | 29 | Transform the Salesforce Apex code coverage JSON files created during deployments and test runs into other [formats](#coverage-report-formats) accepted by SonarQube, GitHub, GitLab, Azure, Bitbucket, etc. 30 | 31 | > If there's a coverage format not yet supported by this plugin, feel free to provide a pull request or issue for the coverage format. 32 | 33 | ## Install 34 | 35 | ```bash 36 | sf plugins install apex-code-coverage-transformer@x.y.z 37 | ``` 38 | 39 | ## Usage 40 | 41 | This plugin is designed for users deploying Apex or running Apex tests within Salesforce DX projects (`sfdx-project.json`). It transforms Salesforce CLI JSON coverage reports into formats recognized by external tools. 42 | 43 | The plugin ensures that coverage data is only reported for files found in your package directories, preventing mismatches in tools like SonarQube. If Apex files are missing from your project (i.e. Apex from managed or unlocked packages), they will be excluded from the transformed report with a [warning](#troubleshooting). 44 | 45 | To automate coverage transformation after deployments or test executions, see [Hook](#hook). 46 | 47 | ### Salesforce CLI 48 | 49 | > This plugin will only support the "json" coverage format from the Salesforce CLI. Do not use other coverage formats from the Salesforce CLI. 50 | 51 | To create the code coverage JSON when deploying or validating, append `--coverage-formatters json --results-dir "coverage"` to the `sf project deploy` command. This will create a coverage JSON in this relative path - `coverage/coverage/coverage.json`. 52 | 53 | ``` 54 | sf project deploy [start/validate/report/resume] --coverage-formatters json --results-dir "coverage" 55 | ``` 56 | 57 | To create the code coverage JSON when running tests directly in the org, append `--code-coverage --output-dir "coverage"` to the `sf apex run test` or `sf apex get test` command. This will create the code coverage JSON in this relative path - `coverage/test-result-codecoverage.json` 58 | 59 | ``` 60 | sf apex run test --code-coverage --output-dir "coverage" 61 | sf apex get test --test-run-id --code-coverage --output-dir "coverage" 62 | ``` 63 | 64 | ### SFDX Hardis 65 | 66 | This plugin can be used after running the below [sfdx-hardis](https://github.com/hardisgroupcom/sfdx-hardis) commands: 67 | 68 | - `sf hardis project deploy smart` (only if `COVERAGE_FORMATTER_JSON=true` environment variable is defined) 69 | - `sf hardis org test apex` 70 | 71 | Both hardis commands will create the code coverage JSON to transform here: `hardis-report/apex-coverage-results.json`. 72 | 73 | ## Fixes and Enhancements 74 | 75 | - **Maps Apex file names** in the original coverage report (e.g., `no-map/AccountTriggerHandler`) to their corresponding relative file paths in the Salesforce DX project (e.g., `force-app/main/default/classes/AccountTriggerHandler.cls`). 76 | - **Normalizes coverage reports** across both deploy and test commands, improving compatibility with external tools. 77 | - **Adds additional coverage formats** not available in the default Salesforce CLI deploy and test commands. 78 | - **"Fixes" inaccuracies** in Salesforce CLI deploy command coverage reports, such as out-of-range covered lines (e.g., line 100 reported as "covered" in a 98-line Apex class) and incorrect total line counts (e.g., 120 lines reported for a 100-line Apex class). 79 | - To address these inaccuracies, the plugin includes a **re-numbering function** that only applies to deploy coverage reports. This function reassigns out-of-range `covered` lines to unused lines, ensuring reports are accepted by external tools. 80 | - The `uncovered` lines are always correctly reported by the deploy command. 81 | - Once Salesforce resolves the issue with the API that affects deploy command coverage reports, the re-numbering function will be removed in a future **breaking** release. 82 | - See issues [5511](https://github.com/forcedotcom/salesforcedx-vscode/issues/5511) and [1568](https://github.com/forcedotcom/cli/issues/1568) for more details. 83 | - **Note**: This does not affect coverage reports generated by the Salesforce CLI test commands. 84 | 85 | ## Command 86 | 87 | The `apex-code-coverage-transformer` has 1 command: 88 | 89 | - `sf acc-transformer transform` 90 | 91 | ## `sf acc-transformer transform` 92 | 93 | ``` 94 | USAGE 95 | $ sf acc-transformer transform -j [-r ] [-f ] [-i ] [--json] 96 | 97 | FLAGS 98 | -j, --coverage-json= Path to the code coverage JSON file created by the Salesforce CLI deploy or test command. 99 | -r, --output-report= Path to the code coverage file that will be created by this plugin. 100 | [default: "coverage.[xml/info]"] 101 | -f, --format= Output format for the code coverage format. 102 | Can be declared multiple times. 103 | If declared multiple times, the output report will have the format appended to the file-name, i.e. `coverage-sonar.xml` 104 | [default: "sonar"] 105 | -i, --ignore-package-directory= Package directory to ignore when looking for matching files in the coverage report. 106 | Should be as they appear in the "sfdx-project.json". 107 | Can be declared multiple times. 108 | 109 | GLOBAL FLAGS 110 | --json Format output as json. 111 | 112 | EXAMPLES 113 | Transform the JSON into Sonar format: 114 | 115 | $ sf acc-transformer transform -j "coverage.json" -r "coverage.xml" -f "sonar" 116 | 117 | Transform the JSON into Cobertura format: 118 | 119 | $ sf acc-transformer transform -j "coverage.json" -r "coverage.xml" -f "cobertura" 120 | 121 | Transform the JSON into Clover format: 122 | 123 | $ sf acc-transformer transform -j "coverage.json" -r "coverage.xml" -f "clover" 124 | 125 | Transform the JSON into LCovOnly format: 126 | 127 | $ sf acc-transformer transform -j "coverage.json" -r "coverage.info" -f "lcovonly" 128 | 129 | Transform the JSON into Sonar format, ignoring Apex in the "force-app" directory: 130 | 131 | $ sf acc-transformer transform -j "coverage.json" -i "force-app" 132 | ``` 133 | 134 | ## Coverage Report Formats 135 | 136 | The `-f`/`--format` flag allows you to specify the format of the transformed coverage report. 137 | 138 | You can provide multiple `--format` flags in a single command to create multiple reports. If multiple `--format` flags are provided, each output report will have the format appended to the name. For example, if `--output-report` is set `coverage.xml` and you supply `--format sonar --format cobertura` to the command, the output reports will be `coverage-sonar.xml` and `coverage-cobertura.xml`. 139 | 140 | | Flag Option | Description | 141 | | ---------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | 142 | | [sonar](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/baselines/sonar_baseline.xml) | Generates a SonarQube-compatible coverage report. This is the default option. | 143 | | [clover](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/baselines/clover_baseline.xml) | Produces a Clover XML report format, commonly used with Atlassian tools. | 144 | | [lcovonly](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/baselines/lcov_baseline.info) | Outputs coverage data in LCOV format, useful for integrating with LCOV-based tools. | 145 | | [cobertura](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/baselines/cobertura_baseline.xml) | Creates a Cobertura XML report, a widely used format for coverage reporting. | 146 | | [jacoco](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/baselines/jacoco_baseline.xml) | Creates a JaCoCo XML report, the standard for Java projects. | 147 | | [json](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/baselines/json_baseline.json) | Generates a Istanbul JSON report compatible with Node.js tooling and coverage visualizers. | 148 | 149 | ## Hook 150 | 151 | To enable automatic transformation after the below `sf` commands complete, create a `.apexcodecovtransformer.config.json` in your project’s root directory. 152 | 153 | - `sf project deploy [start/validate/report/resume]` 154 | - `sf apex run test` 155 | - `sf apex get test` 156 | - `sf hardis project deploy smart` 157 | - only if `sfdx-hardis` is installed 158 | - `COVERAGE_FORMATTER_JSON=true` must be set in the environment variables 159 | - `sf hardis org test apex` 160 | - only if `sfdx-hardis` is installed 161 | 162 | You can copy & update the sample [Salesforce CLI .apexcodecovtransformer.config.json](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/defaults/salesforce-cli/.apexcodecovtransformer.config.json), which assumes you are running the Salesforce CLI commands and specifying the `--results-dir`/`--output-dir` directory as "coverage". 163 | 164 | You can copy & update the sample [SFDX Hardis .apexcodecovtransformer.config.json](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/defaults/sfdx-hardis/.apexcodecovtransformer.config.json), which assumes you are running the SFDX Hardis commands. 165 | 166 | **`.apexcodecovtransformer.config.json` structure** 167 | 168 | | JSON Key | Required | Description | 169 | | -------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 170 | | `deployCoverageJsonPath` | Yes (for deploy command) | Code coverage JSON created by the Salesforce CLI deploy commands. | 171 | | `testCoverageJsonPath` | Yes (for test command) | Code coverage JSON created by the Salesforce CLI test commands. | 172 | | `outputReportPath` | No (defaults to `coverage.[xml/info]`) | Transformed code coverage report path. | 173 | | `format` | No (defaults to `sonar`) | Transformed code coverage report [format(s)](#coverage-report-formats). If you're providing multiple formats, provide a comma-separated list, i.e. `sonar,cobertura,jacoco` | 174 | | `ignorePackageDirectories` | No | Comma-separated string of package directories to ignore when looking for matching Apex files. | 175 | 176 | ## Troubleshooting 177 | 178 | If a file listed in the coverage JSON cannot be found in any package directory, a warning is displayed, and the file will not be included in the transformed report: 179 | 180 | ``` 181 | Warning: The file name AccountTrigger was not found in any package directory. 182 | ``` 183 | 184 | If **none** of the files in the coverage JSON are found in a package directory, the plugin will print an additional warning, and the generated report will be empty: 185 | 186 | ``` 187 | Warning: The file name AccountTrigger was not found in any package directory. 188 | Warning: The file name AccountProfile was not found in any package directory. 189 | Warning: None of the files listed in the coverage JSON were processed. The coverage report will be empty. 190 | ``` 191 | 192 | Salesforce CLI generates code coverage JSONs in two different structures (deploy and test command formats). If the provided coverage JSON does not match one of these expected structures, the plugin will fail with: 193 | 194 | ``` 195 | Error (1): The provided JSON does not match a known coverage data format from the Salesforce deploy or test command. 196 | ``` 197 | 198 | If `sfdx-project.json` file is missing from the project root, the plugin will fail with: 199 | 200 | ``` 201 | Error (1): sfdx-project.json not found in any parent directory. 202 | ``` 203 | 204 | If a package directory listed in `sfdx-project.json` cannot be found, the plugin will encounter a **ENOENT** error: 205 | 206 | ``` 207 | Error (1): ENOENT: no such file or directory: {packageDir} 208 | ``` 209 | 210 | ## Issues 211 | 212 | If you encounter any issues or would like to suggest features, please create an [issue](https://github.com/mcarvin8/apex-code-coverage-transformer/issues). 213 | 214 | ## Contributing 215 | 216 | Contributions are welcome! See [Contributing](https://github.com/mcarvin8/apex-code-coverage-transformer/blob/main/CONTRIBUTING.md). 217 | 218 | ## License 219 | 220 | This project is licensed under the MIT license. Please see the [LICENSE](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/LICENSE.md) file for details. 221 | -------------------------------------------------------------------------------- /baselines/clover_baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /baselines/cobertura_baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | . 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /baselines/jacoco_baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /baselines/json_baseline.json: -------------------------------------------------------------------------------- 1 | { 2 | "force-app/main/default/classes/AccountProfile.cls": { 3 | "path": "force-app/main/default/classes/AccountProfile.cls", 4 | "statementMap": { 5 | "52": { 6 | "start": { 7 | "line": 52, 8 | "column": 0 9 | }, 10 | "end": { 11 | "line": 52, 12 | "column": 0 13 | } 14 | }, 15 | "53": { 16 | "start": { 17 | "line": 53, 18 | "column": 0 19 | }, 20 | "end": { 21 | "line": 53, 22 | "column": 0 23 | } 24 | }, 25 | "54": { 26 | "start": { 27 | "line": 54, 28 | "column": 0 29 | }, 30 | "end": { 31 | "line": 54, 32 | "column": 0 33 | } 34 | }, 35 | "55": { 36 | "start": { 37 | "line": 55, 38 | "column": 0 39 | }, 40 | "end": { 41 | "line": 55, 42 | "column": 0 43 | } 44 | }, 45 | "56": { 46 | "start": { 47 | "line": 56, 48 | "column": 0 49 | }, 50 | "end": { 51 | "line": 56, 52 | "column": 0 53 | } 54 | }, 55 | "57": { 56 | "start": { 57 | "line": 57, 58 | "column": 0 59 | }, 60 | "end": { 61 | "line": 57, 62 | "column": 0 63 | } 64 | }, 65 | "58": { 66 | "start": { 67 | "line": 58, 68 | "column": 0 69 | }, 70 | "end": { 71 | "line": 58, 72 | "column": 0 73 | } 74 | }, 75 | "59": { 76 | "start": { 77 | "line": 59, 78 | "column": 0 79 | }, 80 | "end": { 81 | "line": 59, 82 | "column": 0 83 | } 84 | }, 85 | "60": { 86 | "start": { 87 | "line": 60, 88 | "column": 0 89 | }, 90 | "end": { 91 | "line": 60, 92 | "column": 0 93 | } 94 | }, 95 | "61": { 96 | "start": { 97 | "line": 61, 98 | "column": 0 99 | }, 100 | "end": { 101 | "line": 61, 102 | "column": 0 103 | } 104 | }, 105 | "62": { 106 | "start": { 107 | "line": 62, 108 | "column": 0 109 | }, 110 | "end": { 111 | "line": 62, 112 | "column": 0 113 | } 114 | }, 115 | "63": { 116 | "start": { 117 | "line": 63, 118 | "column": 0 119 | }, 120 | "end": { 121 | "line": 63, 122 | "column": 0 123 | } 124 | }, 125 | "64": { 126 | "start": { 127 | "line": 64, 128 | "column": 0 129 | }, 130 | "end": { 131 | "line": 64, 132 | "column": 0 133 | } 134 | }, 135 | "65": { 136 | "start": { 137 | "line": 65, 138 | "column": 0 139 | }, 140 | "end": { 141 | "line": 65, 142 | "column": 0 143 | } 144 | }, 145 | "66": { 146 | "start": { 147 | "line": 66, 148 | "column": 0 149 | }, 150 | "end": { 151 | "line": 66, 152 | "column": 0 153 | } 154 | }, 155 | "67": { 156 | "start": { 157 | "line": 67, 158 | "column": 0 159 | }, 160 | "end": { 161 | "line": 67, 162 | "column": 0 163 | } 164 | }, 165 | "68": { 166 | "start": { 167 | "line": 68, 168 | "column": 0 169 | }, 170 | "end": { 171 | "line": 68, 172 | "column": 0 173 | } 174 | }, 175 | "69": { 176 | "start": { 177 | "line": 69, 178 | "column": 0 179 | }, 180 | "end": { 181 | "line": 69, 182 | "column": 0 183 | } 184 | }, 185 | "70": { 186 | "start": { 187 | "line": 70, 188 | "column": 0 189 | }, 190 | "end": { 191 | "line": 70, 192 | "column": 0 193 | } 194 | }, 195 | "71": { 196 | "start": { 197 | "line": 71, 198 | "column": 0 199 | }, 200 | "end": { 201 | "line": 71, 202 | "column": 0 203 | } 204 | }, 205 | "72": { 206 | "start": { 207 | "line": 72, 208 | "column": 0 209 | }, 210 | "end": { 211 | "line": 72, 212 | "column": 0 213 | } 214 | }, 215 | "73": { 216 | "start": { 217 | "line": 73, 218 | "column": 0 219 | }, 220 | "end": { 221 | "line": 73, 222 | "column": 0 223 | } 224 | }, 225 | "74": { 226 | "start": { 227 | "line": 74, 228 | "column": 0 229 | }, 230 | "end": { 231 | "line": 74, 232 | "column": 0 233 | } 234 | }, 235 | "75": { 236 | "start": { 237 | "line": 75, 238 | "column": 0 239 | }, 240 | "end": { 241 | "line": 75, 242 | "column": 0 243 | } 244 | }, 245 | "76": { 246 | "start": { 247 | "line": 76, 248 | "column": 0 249 | }, 250 | "end": { 251 | "line": 76, 252 | "column": 0 253 | } 254 | }, 255 | "77": { 256 | "start": { 257 | "line": 77, 258 | "column": 0 259 | }, 260 | "end": { 261 | "line": 77, 262 | "column": 0 263 | } 264 | }, 265 | "78": { 266 | "start": { 267 | "line": 78, 268 | "column": 0 269 | }, 270 | "end": { 271 | "line": 78, 272 | "column": 0 273 | } 274 | }, 275 | "79": { 276 | "start": { 277 | "line": 79, 278 | "column": 0 279 | }, 280 | "end": { 281 | "line": 79, 282 | "column": 0 283 | } 284 | }, 285 | "80": { 286 | "start": { 287 | "line": 80, 288 | "column": 0 289 | }, 290 | "end": { 291 | "line": 80, 292 | "column": 0 293 | } 294 | }, 295 | "81": { 296 | "start": { 297 | "line": 81, 298 | "column": 0 299 | }, 300 | "end": { 301 | "line": 81, 302 | "column": 0 303 | } 304 | }, 305 | "82": { 306 | "start": { 307 | "line": 82, 308 | "column": 0 309 | }, 310 | "end": { 311 | "line": 82, 312 | "column": 0 313 | } 314 | } 315 | }, 316 | "fnMap": {}, 317 | "branchMap": {}, 318 | "s": { 319 | "52": 0, 320 | "53": 0, 321 | "54": 1, 322 | "55": 1, 323 | "56": 1, 324 | "57": 1, 325 | "58": 1, 326 | "59": 0, 327 | "60": 0, 328 | "61": 1, 329 | "62": 1, 330 | "63": 1, 331 | "64": 1, 332 | "65": 1, 333 | "66": 1, 334 | "67": 1, 335 | "68": 1, 336 | "69": 1, 337 | "70": 1, 338 | "71": 1, 339 | "72": 1, 340 | "73": 1, 341 | "74": 1, 342 | "75": 1, 343 | "76": 1, 344 | "77": 1, 345 | "78": 1, 346 | "79": 1, 347 | "80": 1, 348 | "81": 1, 349 | "82": 1 350 | }, 351 | "f": {}, 352 | "b": {}, 353 | "l": { 354 | "52": 0, 355 | "53": 0, 356 | "54": 1, 357 | "55": 1, 358 | "56": 1, 359 | "57": 1, 360 | "58": 1, 361 | "59": 0, 362 | "60": 0, 363 | "61": 1, 364 | "62": 1, 365 | "63": 1, 366 | "64": 1, 367 | "65": 1, 368 | "66": 1, 369 | "67": 1, 370 | "68": 1, 371 | "69": 1, 372 | "70": 1, 373 | "71": 1, 374 | "72": 1, 375 | "73": 1, 376 | "74": 1, 377 | "75": 1, 378 | "76": 1, 379 | "77": 1, 380 | "78": 1, 381 | "79": 1, 382 | "80": 1, 383 | "81": 1, 384 | "82": 1 385 | } 386 | }, 387 | "packaged/triggers/AccountTrigger.trigger": { 388 | "path": "packaged/triggers/AccountTrigger.trigger", 389 | "statementMap": { 390 | "52": { 391 | "start": { 392 | "line": 52, 393 | "column": 0 394 | }, 395 | "end": { 396 | "line": 52, 397 | "column": 0 398 | } 399 | }, 400 | "53": { 401 | "start": { 402 | "line": 53, 403 | "column": 0 404 | }, 405 | "end": { 406 | "line": 53, 407 | "column": 0 408 | } 409 | }, 410 | "54": { 411 | "start": { 412 | "line": 54, 413 | "column": 0 414 | }, 415 | "end": { 416 | "line": 54, 417 | "column": 0 418 | } 419 | }, 420 | "55": { 421 | "start": { 422 | "line": 55, 423 | "column": 0 424 | }, 425 | "end": { 426 | "line": 55, 427 | "column": 0 428 | } 429 | }, 430 | "56": { 431 | "start": { 432 | "line": 56, 433 | "column": 0 434 | }, 435 | "end": { 436 | "line": 56, 437 | "column": 0 438 | } 439 | }, 440 | "57": { 441 | "start": { 442 | "line": 57, 443 | "column": 0 444 | }, 445 | "end": { 446 | "line": 57, 447 | "column": 0 448 | } 449 | }, 450 | "58": { 451 | "start": { 452 | "line": 58, 453 | "column": 0 454 | }, 455 | "end": { 456 | "line": 58, 457 | "column": 0 458 | } 459 | }, 460 | "59": { 461 | "start": { 462 | "line": 59, 463 | "column": 0 464 | }, 465 | "end": { 466 | "line": 59, 467 | "column": 0 468 | } 469 | }, 470 | "60": { 471 | "start": { 472 | "line": 60, 473 | "column": 0 474 | }, 475 | "end": { 476 | "line": 60, 477 | "column": 0 478 | } 479 | }, 480 | "61": { 481 | "start": { 482 | "line": 61, 483 | "column": 0 484 | }, 485 | "end": { 486 | "line": 61, 487 | "column": 0 488 | } 489 | }, 490 | "62": { 491 | "start": { 492 | "line": 62, 493 | "column": 0 494 | }, 495 | "end": { 496 | "line": 62, 497 | "column": 0 498 | } 499 | }, 500 | "63": { 501 | "start": { 502 | "line": 63, 503 | "column": 0 504 | }, 505 | "end": { 506 | "line": 63, 507 | "column": 0 508 | } 509 | }, 510 | "64": { 511 | "start": { 512 | "line": 64, 513 | "column": 0 514 | }, 515 | "end": { 516 | "line": 64, 517 | "column": 0 518 | } 519 | }, 520 | "65": { 521 | "start": { 522 | "line": 65, 523 | "column": 0 524 | }, 525 | "end": { 526 | "line": 65, 527 | "column": 0 528 | } 529 | }, 530 | "66": { 531 | "start": { 532 | "line": 66, 533 | "column": 0 534 | }, 535 | "end": { 536 | "line": 66, 537 | "column": 0 538 | } 539 | }, 540 | "67": { 541 | "start": { 542 | "line": 67, 543 | "column": 0 544 | }, 545 | "end": { 546 | "line": 67, 547 | "column": 0 548 | } 549 | }, 550 | "68": { 551 | "start": { 552 | "line": 68, 553 | "column": 0 554 | }, 555 | "end": { 556 | "line": 68, 557 | "column": 0 558 | } 559 | }, 560 | "69": { 561 | "start": { 562 | "line": 69, 563 | "column": 0 564 | }, 565 | "end": { 566 | "line": 69, 567 | "column": 0 568 | } 569 | }, 570 | "70": { 571 | "start": { 572 | "line": 70, 573 | "column": 0 574 | }, 575 | "end": { 576 | "line": 70, 577 | "column": 0 578 | } 579 | }, 580 | "71": { 581 | "start": { 582 | "line": 71, 583 | "column": 0 584 | }, 585 | "end": { 586 | "line": 71, 587 | "column": 0 588 | } 589 | }, 590 | "72": { 591 | "start": { 592 | "line": 72, 593 | "column": 0 594 | }, 595 | "end": { 596 | "line": 72, 597 | "column": 0 598 | } 599 | }, 600 | "73": { 601 | "start": { 602 | "line": 73, 603 | "column": 0 604 | }, 605 | "end": { 606 | "line": 73, 607 | "column": 0 608 | } 609 | }, 610 | "74": { 611 | "start": { 612 | "line": 74, 613 | "column": 0 614 | }, 615 | "end": { 616 | "line": 74, 617 | "column": 0 618 | } 619 | }, 620 | "75": { 621 | "start": { 622 | "line": 75, 623 | "column": 0 624 | }, 625 | "end": { 626 | "line": 75, 627 | "column": 0 628 | } 629 | }, 630 | "76": { 631 | "start": { 632 | "line": 76, 633 | "column": 0 634 | }, 635 | "end": { 636 | "line": 76, 637 | "column": 0 638 | } 639 | }, 640 | "77": { 641 | "start": { 642 | "line": 77, 643 | "column": 0 644 | }, 645 | "end": { 646 | "line": 77, 647 | "column": 0 648 | } 649 | }, 650 | "78": { 651 | "start": { 652 | "line": 78, 653 | "column": 0 654 | }, 655 | "end": { 656 | "line": 78, 657 | "column": 0 658 | } 659 | }, 660 | "79": { 661 | "start": { 662 | "line": 79, 663 | "column": 0 664 | }, 665 | "end": { 666 | "line": 79, 667 | "column": 0 668 | } 669 | }, 670 | "80": { 671 | "start": { 672 | "line": 80, 673 | "column": 0 674 | }, 675 | "end": { 676 | "line": 80, 677 | "column": 0 678 | } 679 | }, 680 | "81": { 681 | "start": { 682 | "line": 81, 683 | "column": 0 684 | }, 685 | "end": { 686 | "line": 81, 687 | "column": 0 688 | } 689 | }, 690 | "82": { 691 | "start": { 692 | "line": 82, 693 | "column": 0 694 | }, 695 | "end": { 696 | "line": 82, 697 | "column": 0 698 | } 699 | } 700 | }, 701 | "fnMap": {}, 702 | "branchMap": {}, 703 | "s": { 704 | "52": 0, 705 | "53": 0, 706 | "54": 1, 707 | "55": 1, 708 | "56": 1, 709 | "57": 1, 710 | "58": 1, 711 | "59": 0, 712 | "60": 0, 713 | "61": 1, 714 | "62": 1, 715 | "63": 1, 716 | "64": 1, 717 | "65": 1, 718 | "66": 1, 719 | "67": 1, 720 | "68": 1, 721 | "69": 1, 722 | "70": 1, 723 | "71": 1, 724 | "72": 1, 725 | "73": 1, 726 | "74": 1, 727 | "75": 1, 728 | "76": 1, 729 | "77": 1, 730 | "78": 1, 731 | "79": 1, 732 | "80": 1, 733 | "81": 1, 734 | "82": 1 735 | }, 736 | "f": {}, 737 | "b": {}, 738 | "l": { 739 | "52": 0, 740 | "53": 0, 741 | "54": 1, 742 | "55": 1, 743 | "56": 1, 744 | "57": 1, 745 | "58": 1, 746 | "59": 0, 747 | "60": 0, 748 | "61": 1, 749 | "62": 1, 750 | "63": 1, 751 | "64": 1, 752 | "65": 1, 753 | "66": 1, 754 | "67": 1, 755 | "68": 1, 756 | "69": 1, 757 | "70": 1, 758 | "71": 1, 759 | "72": 1, 760 | "73": 1, 761 | "74": 1, 762 | "75": 1, 763 | "76": 1, 764 | "77": 1, 765 | "78": 1, 766 | "79": 1, 767 | "80": 1, 768 | "81": 1, 769 | "82": 1 770 | } 771 | } 772 | } -------------------------------------------------------------------------------- /baselines/lcov_baseline.info: -------------------------------------------------------------------------------- 1 | TN: 2 | SF:force-app/main/default/classes/AccountProfile.cls 3 | FNF:0 4 | FNH:0 5 | DA:52,0 6 | DA:53,0 7 | DA:54,1 8 | DA:55,1 9 | DA:56,1 10 | DA:57,1 11 | DA:58,1 12 | DA:59,0 13 | DA:60,0 14 | DA:61,1 15 | DA:62,1 16 | DA:63,1 17 | DA:64,1 18 | DA:65,1 19 | DA:66,1 20 | DA:67,1 21 | DA:68,1 22 | DA:69,1 23 | DA:70,1 24 | DA:71,1 25 | DA:72,1 26 | DA:73,1 27 | DA:74,1 28 | DA:75,1 29 | DA:76,1 30 | DA:77,1 31 | DA:78,1 32 | DA:79,1 33 | DA:80,1 34 | DA:81,1 35 | DA:82,1 36 | LF:31 37 | LH:27 38 | BRF:0 39 | BRH:0 40 | end_of_record 41 | TN: 42 | SF:packaged/triggers/AccountTrigger.trigger 43 | FNF:0 44 | FNH:0 45 | DA:52,0 46 | DA:53,0 47 | DA:54,1 48 | DA:55,1 49 | DA:56,1 50 | DA:57,1 51 | DA:58,1 52 | DA:59,0 53 | DA:60,0 54 | DA:61,1 55 | DA:62,1 56 | DA:63,1 57 | DA:64,1 58 | DA:65,1 59 | DA:66,1 60 | DA:67,1 61 | DA:68,1 62 | DA:69,1 63 | DA:70,1 64 | DA:71,1 65 | DA:72,1 66 | DA:73,1 67 | DA:74,1 68 | DA:75,1 69 | DA:76,1 70 | DA:77,1 71 | DA:78,1 72 | DA:79,1 73 | DA:80,1 74 | DA:81,1 75 | DA:82,1 76 | LF:31 77 | LH:27 78 | BRF:0 79 | BRH:0 80 | end_of_record -------------------------------------------------------------------------------- /baselines/sonar_baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node --loader ts-node/esm --no-warnings=ExperimentalWarning "%~dp0\dev" %* 4 | -------------------------------------------------------------------------------- /bin/dev.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S node --loader ts-node/esm --no-warnings=ExperimentalWarning 2 | // eslint-disable-next-line node/shebang 3 | async function main() { 4 | const { execute } = await import('@oclif/core'); 5 | await execute({ development: true, dir: import.meta.url }); 6 | } 7 | 8 | await main(); 9 | -------------------------------------------------------------------------------- /bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /bin/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // eslint-disable-next-line node/shebang 4 | async function main() { 5 | const { execute } = await import('@oclif/core'); 6 | await execute({ dir: import.meta.url }); 7 | } 8 | 9 | await main(); 10 | -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /defaults/salesforce-cli/.apexcodecovtransformer.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "deployCoverageJsonPath": "coverage/coverage/coverage.json", 3 | "testCoverageJsonPath": "coverage/test-result-codecoverage.json", 4 | "outputReportPath": "coverage.xml", 5 | "format": "sonar" 6 | } 7 | -------------------------------------------------------------------------------- /defaults/sfdx-hardis/.apexcodecovtransformer.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "deployCoverageJsonPath": "hardis-report/apex-coverage-results.json", 3 | "testCoverageJsonPath": "hardis-report/apex-coverage-results.json", 4 | "outputReportPath": "coverage.xml", 5 | "format": "sonar" 6 | } 7 | -------------------------------------------------------------------------------- /inputs/deploy_coverage.json: -------------------------------------------------------------------------------- 1 | { 2 | "no-map/AccountTrigger": { 3 | "fnMap": {}, 4 | "branchMap": {}, 5 | "path": "no-map/AccountTrigger", 6 | "f": {}, 7 | "b": {}, 8 | "s": { 9 | "52": 0, 10 | "53": 0, 11 | "54": 1, 12 | "55": 1, 13 | "56": 1, 14 | "57": 1, 15 | "58": 1, 16 | "59": 0, 17 | "60": 0, 18 | "61": 1, 19 | "62": 1, 20 | "63": 1, 21 | "64": 1, 22 | "65": 1, 23 | "66": 1, 24 | "67": 1, 25 | "68": 1, 26 | "69": 1, 27 | "70": 1, 28 | "71": 1, 29 | "72": 1, 30 | "73": 1, 31 | "74": 1, 32 | "75": 1, 33 | "76": 1, 34 | "77": 1, 35 | "78": 1, 36 | "79": 1, 37 | "80": 1, 38 | "81": 1, 39 | "82": 1 40 | }, 41 | "statementMap": { 42 | "52": { "start": { "line": 52, "column": 0 }, "end": { "line": 52, "column": 0 } }, 43 | "53": { "start": { "line": 53, "column": 0 }, "end": { "line": 53, "column": 0 } }, 44 | "54": { "start": { "line": 54, "column": 0 }, "end": { "line": 54, "column": 0 } }, 45 | "55": { "start": { "line": 55, "column": 0 }, "end": { "line": 55, "column": 0 } }, 46 | "56": { "start": { "line": 56, "column": 0 }, "end": { "line": 56, "column": 0 } }, 47 | "57": { "start": { "line": 57, "column": 0 }, "end": { "line": 57, "column": 0 } }, 48 | "58": { "start": { "line": 58, "column": 0 }, "end": { "line": 58, "column": 0 } }, 49 | "59": { "start": { "line": 59, "column": 0 }, "end": { "line": 59, "column": 0 } }, 50 | "60": { "start": { "line": 60, "column": 0 }, "end": { "line": 60, "column": 0 } }, 51 | "61": { "start": { "line": 61, "column": 0 }, "end": { "line": 61, "column": 0 } }, 52 | "62": { "start": { "line": 62, "column": 0 }, "end": { "line": 62, "column": 0 } }, 53 | "63": { "start": { "line": 63, "column": 0 }, "end": { "line": 63, "column": 0 } }, 54 | "64": { "start": { "line": 64, "column": 0 }, "end": { "line": 64, "column": 0 } }, 55 | "65": { "start": { "line": 65, "column": 0 }, "end": { "line": 65, "column": 0 } }, 56 | "66": { "start": { "line": 66, "column": 0 }, "end": { "line": 66, "column": 0 } }, 57 | "67": { "start": { "line": 67, "column": 0 }, "end": { "line": 67, "column": 0 } }, 58 | "68": { "start": { "line": 68, "column": 0 }, "end": { "line": 68, "column": 0 } }, 59 | "69": { "start": { "line": 69, "column": 0 }, "end": { "line": 69, "column": 0 } }, 60 | "70": { "start": { "line": 70, "column": 0 }, "end": { "line": 70, "column": 0 } }, 61 | "71": { "start": { "line": 71, "column": 0 }, "end": { "line": 71, "column": 0 } }, 62 | "72": { "start": { "line": 72, "column": 0 }, "end": { "line": 72, "column": 0 } }, 63 | "73": { "start": { "line": 73, "column": 0 }, "end": { "line": 73, "column": 0 } }, 64 | "74": { "start": { "line": 74, "column": 0 }, "end": { "line": 74, "column": 0 } }, 65 | "75": { "start": { "line": 75, "column": 0 }, "end": { "line": 75, "column": 0 } }, 66 | "76": { "start": { "line": 76, "column": 0 }, "end": { "line": 76, "column": 0 } }, 67 | "77": { "start": { "line": 77, "column": 0 }, "end": { "line": 77, "column": 0 } }, 68 | "78": { "start": { "line": 78, "column": 0 }, "end": { "line": 78, "column": 0 } }, 69 | "79": { "start": { "line": 79, "column": 0 }, "end": { "line": 79, "column": 0 } }, 70 | "80": { "start": { "line": 80, "column": 0 }, "end": { "line": 80, "column": 0 } }, 71 | "81": { "start": { "line": 81, "column": 0 }, "end": { "line": 81, "column": 0 } }, 72 | "82": { "start": { "line": 82, "column": 0 }, "end": { "line": 82, "column": 0 } } 73 | } 74 | }, 75 | "no-map/AccountProfile": { 76 | "fnMap": {}, 77 | "branchMap": {}, 78 | "path": "no-map/AccountProfile", 79 | "f": {}, 80 | "b": {}, 81 | "s": { 82 | "52": 0, 83 | "53": 0, 84 | "54": 1, 85 | "55": 1, 86 | "56": 1, 87 | "57": 1, 88 | "58": 1, 89 | "59": 0, 90 | "60": 0, 91 | "61": 1, 92 | "62": 1, 93 | "63": 1, 94 | "64": 1, 95 | "65": 1, 96 | "66": 1, 97 | "67": 1, 98 | "68": 1, 99 | "69": 1, 100 | "70": 1, 101 | "71": 1, 102 | "72": 1, 103 | "73": 1, 104 | "74": 1, 105 | "75": 1, 106 | "76": 1, 107 | "77": 1, 108 | "78": 1, 109 | "79": 1, 110 | "80": 1, 111 | "81": 1, 112 | "82": 1 113 | }, 114 | "statementMap": { 115 | "52": { "start": { "line": 52, "column": 0 }, "end": { "line": 52, "column": 0 } }, 116 | "53": { "start": { "line": 53, "column": 0 }, "end": { "line": 53, "column": 0 } }, 117 | "54": { "start": { "line": 54, "column": 0 }, "end": { "line": 54, "column": 0 } }, 118 | "55": { "start": { "line": 55, "column": 0 }, "end": { "line": 55, "column": 0 } }, 119 | "56": { "start": { "line": 56, "column": 0 }, "end": { "line": 56, "column": 0 } }, 120 | "57": { "start": { "line": 57, "column": 0 }, "end": { "line": 57, "column": 0 } }, 121 | "58": { "start": { "line": 58, "column": 0 }, "end": { "line": 58, "column": 0 } }, 122 | "59": { "start": { "line": 59, "column": 0 }, "end": { "line": 59, "column": 0 } }, 123 | "60": { "start": { "line": 60, "column": 0 }, "end": { "line": 60, "column": 0 } }, 124 | "61": { "start": { "line": 61, "column": 0 }, "end": { "line": 61, "column": 0 } }, 125 | "62": { "start": { "line": 62, "column": 0 }, "end": { "line": 62, "column": 0 } }, 126 | "63": { "start": { "line": 63, "column": 0 }, "end": { "line": 63, "column": 0 } }, 127 | "64": { "start": { "line": 64, "column": 0 }, "end": { "line": 64, "column": 0 } }, 128 | "65": { "start": { "line": 65, "column": 0 }, "end": { "line": 65, "column": 0 } }, 129 | "66": { "start": { "line": 66, "column": 0 }, "end": { "line": 66, "column": 0 } }, 130 | "67": { "start": { "line": 67, "column": 0 }, "end": { "line": 67, "column": 0 } }, 131 | "68": { "start": { "line": 68, "column": 0 }, "end": { "line": 68, "column": 0 } }, 132 | "69": { "start": { "line": 69, "column": 0 }, "end": { "line": 69, "column": 0 } }, 133 | "70": { "start": { "line": 70, "column": 0 }, "end": { "line": 70, "column": 0 } }, 134 | "71": { "start": { "line": 71, "column": 0 }, "end": { "line": 71, "column": 0 } }, 135 | "72": { "start": { "line": 72, "column": 0 }, "end": { "line": 72, "column": 0 } }, 136 | "73": { "start": { "line": 73, "column": 0 }, "end": { "line": 73, "column": 0 } }, 137 | "74": { "start": { "line": 74, "column": 0 }, "end": { "line": 74, "column": 0 } }, 138 | "75": { "start": { "line": 75, "column": 0 }, "end": { "line": 75, "column": 0 } }, 139 | "76": { "start": { "line": 76, "column": 0 }, "end": { "line": 76, "column": 0 } }, 140 | "77": { "start": { "line": 77, "column": 0 }, "end": { "line": 77, "column": 0 } }, 141 | "78": { "start": { "line": 78, "column": 0 }, "end": { "line": 78, "column": 0 } }, 142 | "79": { "start": { "line": 79, "column": 0 }, "end": { "line": 79, "column": 0 } }, 143 | "80": { "start": { "line": 80, "column": 0 }, "end": { "line": 80, "column": 0 } }, 144 | "81": { "start": { "line": 81, "column": 0 }, "end": { "line": 81, "column": 0 } }, 145 | "82": { "start": { "line": 82, "column": 0 }, "end": { "line": 82, "column": 0 } } 146 | } 147 | }, 148 | "no-map/AccountHandler": { 149 | "fnMap": {}, 150 | "branchMap": {}, 151 | "path": "no-map/AccountHandler", 152 | "f": {}, 153 | "b": {}, 154 | "s": { 155 | "52": 0, 156 | "53": 0, 157 | "54": 1, 158 | "55": 1, 159 | "56": 1, 160 | "57": 1, 161 | "58": 1, 162 | "59": 0, 163 | "60": 0, 164 | "61": 1, 165 | "62": 1, 166 | "63": 1, 167 | "64": 1, 168 | "65": 1, 169 | "66": 1, 170 | "67": 1, 171 | "68": 1, 172 | "69": 1, 173 | "70": 1, 174 | "71": 1, 175 | "72": 1, 176 | "73": 1, 177 | "74": 1, 178 | "75": 1, 179 | "76": 1, 180 | "77": 1, 181 | "78": 1, 182 | "79": 1, 183 | "80": 1, 184 | "81": 1, 185 | "82": 1 186 | }, 187 | "statementMap": { 188 | "52": { "start": { "line": 52, "column": 0 }, "end": { "line": 52, "column": 0 } }, 189 | "53": { "start": { "line": 53, "column": 0 }, "end": { "line": 53, "column": 0 } }, 190 | "54": { "start": { "line": 54, "column": 0 }, "end": { "line": 54, "column": 0 } }, 191 | "55": { "start": { "line": 55, "column": 0 }, "end": { "line": 55, "column": 0 } }, 192 | "56": { "start": { "line": 56, "column": 0 }, "end": { "line": 56, "column": 0 } }, 193 | "57": { "start": { "line": 57, "column": 0 }, "end": { "line": 57, "column": 0 } }, 194 | "58": { "start": { "line": 58, "column": 0 }, "end": { "line": 58, "column": 0 } }, 195 | "59": { "start": { "line": 59, "column": 0 }, "end": { "line": 59, "column": 0 } }, 196 | "60": { "start": { "line": 60, "column": 0 }, "end": { "line": 60, "column": 0 } }, 197 | "61": { "start": { "line": 61, "column": 0 }, "end": { "line": 61, "column": 0 } }, 198 | "62": { "start": { "line": 62, "column": 0 }, "end": { "line": 62, "column": 0 } }, 199 | "63": { "start": { "line": 63, "column": 0 }, "end": { "line": 63, "column": 0 } }, 200 | "64": { "start": { "line": 64, "column": 0 }, "end": { "line": 64, "column": 0 } }, 201 | "65": { "start": { "line": 65, "column": 0 }, "end": { "line": 65, "column": 0 } }, 202 | "66": { "start": { "line": 66, "column": 0 }, "end": { "line": 66, "column": 0 } }, 203 | "67": { "start": { "line": 67, "column": 0 }, "end": { "line": 67, "column": 0 } }, 204 | "68": { "start": { "line": 68, "column": 0 }, "end": { "line": 68, "column": 0 } }, 205 | "69": { "start": { "line": 69, "column": 0 }, "end": { "line": 69, "column": 0 } }, 206 | "70": { "start": { "line": 70, "column": 0 }, "end": { "line": 70, "column": 0 } }, 207 | "71": { "start": { "line": 71, "column": 0 }, "end": { "line": 71, "column": 0 } }, 208 | "72": { "start": { "line": 72, "column": 0 }, "end": { "line": 72, "column": 0 } }, 209 | "73": { "start": { "line": 73, "column": 0 }, "end": { "line": 73, "column": 0 } }, 210 | "74": { "start": { "line": 74, "column": 0 }, "end": { "line": 74, "column": 0 } }, 211 | "75": { "start": { "line": 75, "column": 0 }, "end": { "line": 75, "column": 0 } }, 212 | "76": { "start": { "line": 76, "column": 0 }, "end": { "line": 76, "column": 0 } }, 213 | "77": { "start": { "line": 77, "column": 0 }, "end": { "line": 77, "column": 0 } }, 214 | "78": { "start": { "line": 78, "column": 0 }, "end": { "line": 78, "column": 0 } }, 215 | "79": { "start": { "line": 79, "column": 0 }, "end": { "line": 79, "column": 0 } }, 216 | "80": { "start": { "line": 80, "column": 0 }, "end": { "line": 80, "column": 0 } }, 217 | "81": { "start": { "line": 81, "column": 0 }, "end": { "line": 81, "column": 0 } }, 218 | "82": { "start": { "line": 82, "column": 0 }, "end": { "line": 82, "column": 0 } } 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /inputs/invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "no-map/AccountTrigger.trigger": { 3 | "path": "no-map/AccountTrigger.trigger" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /inputs/test_coverage.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "01p9X00000DKqDQQA1", 4 | "name": "AccountProfile", 5 | "totalLines": 31, 6 | "lines": { 7 | "52": 0, 8 | "53": 0, 9 | "54": 1, 10 | "55": 1, 11 | "56": 1, 12 | "57": 1, 13 | "58": 1, 14 | "59": 0, 15 | "60": 0, 16 | "61": 1, 17 | "62": 1, 18 | "63": 1, 19 | "64": 1, 20 | "65": 1, 21 | "66": 1, 22 | "67": 1, 23 | "68": 1, 24 | "69": 1, 25 | "70": 1, 26 | "71": 1, 27 | "72": 1, 28 | "73": 1, 29 | "74": 1, 30 | "75": 1, 31 | "76": 1, 32 | "77": 1, 33 | "78": 1, 34 | "79": 1, 35 | "80": 1, 36 | "81": 1, 37 | "82": 1 38 | }, 39 | "totalCovered": 27, 40 | "coveredPercent": 87 41 | }, 42 | { 43 | "id": "01p9X00000DKqCiQAL", 44 | "name": "AccountTrigger", 45 | "totalLines": 31, 46 | "lines": { 47 | "52": 0, 48 | "53": 0, 49 | "54": 1, 50 | "55": 1, 51 | "56": 1, 52 | "57": 1, 53 | "58": 1, 54 | "59": 0, 55 | "60": 0, 56 | "61": 1, 57 | "62": 1, 58 | "63": 1, 59 | "64": 1, 60 | "65": 1, 61 | "66": 1, 62 | "67": 1, 63 | "68": 1, 64 | "69": 1, 65 | "70": 1, 66 | "71": 1, 67 | "72": 1, 68 | "73": 1, 69 | "74": 1, 70 | "75": 1, 71 | "76": 1, 72 | "77": 1, 73 | "78": 1, 74 | "79": 1, 75 | "80": 1, 76 | "81": 1, 77 | "82": 1 78 | }, 79 | "totalCovered": 27, 80 | "coveredPercent": 87 81 | } 82 | ] 83 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | export default { 5 | automock: false, 6 | clearMocks: true, 7 | coverageDirectory: 'coverage', 8 | coverageReporters: ['text', 'lcov'], 9 | coverageThreshold: { 10 | global: { 11 | branches: 90, 12 | functions: 90, 13 | lines: 95, 14 | statements: 95, 15 | }, 16 | }, 17 | extensionsToTreatAsEsm: ['.ts'], 18 | moduleNameMapper: { 19 | '(.+)\\.js': '$1', 20 | '^lodash-es$': 'lodash', 21 | }, 22 | testEnvironment: 'node', 23 | testMatch: ['**/test/**/*.test.ts'], 24 | transform: { 25 | '\\.[jt]sx?$': [ 26 | 'ts-jest', 27 | { 28 | tsconfig: './tsconfig.json', 29 | }, 30 | ], 31 | }, 32 | // An array of regexp pattern strings used to skip coverage collection 33 | coveragePathIgnorePatterns: ['/node_modules/', '/test/utils/', '/coverage/'], 34 | }; 35 | -------------------------------------------------------------------------------- /messages/transformer.transform.md: -------------------------------------------------------------------------------- 1 | # summary 2 | 3 | Transform Salesforce Apex code coverage JSONs created during deployments and test runs into other formats accepted by SonarQube, GitHub, GitLab, Azure, Bitbucket, etc. 4 | 5 | # description 6 | 7 | Transform Salesforce Apex code coverage JSONs created during deployments and test runs into other formats accepted by SonarQube, GitHub, GitLab, Azure, Bitbucket, etc. 8 | 9 | # examples 10 | 11 | - `sf acc-transformer transform -j "coverage.json" -r "coverage.xml" -f "sonar"` 12 | - `sf acc-transformer transform -j "coverage.json" -r "coverage.xml" -f "cobertura"` 13 | - `sf acc-transformer transform -j "coverage.json" -r "coverage.xml" -f "clover"` 14 | - `sf acc-transformer transform -j "coverage.json" -r "coverage.info" -f "lcovonly"` 15 | - `sf acc-transformer transform -j "coverage.json" -i "force-app"` 16 | 17 | # flags.coverage-json.summary 18 | 19 | Path to the code coverage JSON file created by the Salesforce CLI deploy or test command. 20 | 21 | # flags.output-report.summary 22 | 23 | Path to the code coverage file that will be created by this plugin. 24 | 25 | # flags.format.summary 26 | 27 | Output format for the coverage report. 28 | 29 | # flags.ignore-package-directory.summary 30 | 31 | Ignore a package directory when looking for matching files in the coverage report. 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apex-code-coverage-transformer", 3 | "description": "Transform Salesforce Apex code coverage JSONs into other formats accepted by SonarQube, GitHub, GitLab, Azure, Bitbucket, etc.", 4 | "version": "2.13.3", 5 | "dependencies": { 6 | "@oclif/core": "^4.4.0", 7 | "@salesforce/core": "^8.12.0", 8 | "@salesforce/sf-plugins-core": "^12.2.2", 9 | "async": "^3.2.6", 10 | "xmlbuilder2": "^3.1.1" 11 | }, 12 | "devDependencies": { 13 | "@commitlint/cli": "^19.8.1", 14 | "@commitlint/config-conventional": "^19.8.1", 15 | "@oclif/plugin-command-snapshot": "^5.2.40", 16 | "@salesforce/cli-plugins-testkit": "^5.3.39", 17 | "@salesforce/dev-scripts": "^10.2.11", 18 | "@types/async": "^3.2.24", 19 | "@types/jest": "^29.5.14", 20 | "@types/node": "18", 21 | "eslint-plugin-sf-plugin": "^1.20.26", 22 | "husky": "^9.1.7", 23 | "jest": "^29.7.0", 24 | "oclif": "^4.17.46", 25 | "shx": "0.4.0", 26 | "ts-jest": "^29.4.0", 27 | "ts-jest-mock-import-meta": "^1.3.0", 28 | "ts-node": "^10.9.2", 29 | "typescript": "^5.8.3", 30 | "wireit": "^0.14.12" 31 | }, 32 | "engines": { 33 | "node": ">=20.0.0" 34 | }, 35 | "files": [ 36 | "/lib", 37 | "/messages", 38 | "/oclif.manifest.json", 39 | "/oclif.lock", 40 | "/CHANGELOG.md" 41 | ], 42 | "keywords": [ 43 | "force", 44 | "salesforce", 45 | "salesforcedx", 46 | "sf", 47 | "sf-plugin", 48 | "sfdx", 49 | "sfdx-plugin", 50 | "xml", 51 | "json", 52 | "sonarqube", 53 | "apex", 54 | "coverage", 55 | "git", 56 | "cobertura", 57 | "clover", 58 | "converter", 59 | "transformer", 60 | "code", 61 | "quality", 62 | "validation", 63 | "deployment", 64 | "gitlab", 65 | "github", 66 | "azure", 67 | "bitbucket", 68 | "jacoco", 69 | "lcov", 70 | "json2xml", 71 | "istanbul" 72 | ], 73 | "license": "MIT", 74 | "oclif": { 75 | "commands": "./lib/commands", 76 | "bin": "sf", 77 | "topicSeparator": " ", 78 | "topics": { 79 | "acc-transformer": { 80 | "description": "description for acc-transformer" 81 | } 82 | }, 83 | "hooks": { 84 | "postrun": "./lib/hooks/postrun" 85 | }, 86 | "devPlugins": [ 87 | "@oclif/plugin-help" 88 | ], 89 | "flexibleTaxonomy": true 90 | }, 91 | "scripts": { 92 | "command-docs": "oclif readme", 93 | "build": "tsc -b", 94 | "clean": "sf-clean", 95 | "clean-all": "sf-clean all", 96 | "clean:lib": "shx rm -rf lib && shx rm -rf coverage && shx rm -rf .nyc_output && shx rm -f oclif.manifest.json oclif.lock", 97 | "compile": "wireit", 98 | "docs": "sf-docs", 99 | "format": "sf-format", 100 | "lint": "wireit", 101 | "postpack": "shx rm -f oclif.manifest.json oclif.lock", 102 | "prepack": "sf-prepack", 103 | "prepare": "husky install", 104 | "test": "wireit", 105 | "test:nuts": "oclif manifest && jest --testMatch \"**/*.nut.ts\"", 106 | "test:only": "wireit", 107 | "version": "oclif readme" 108 | }, 109 | "publishConfig": { 110 | "access": "public" 111 | }, 112 | "wireit": { 113 | "build": { 114 | "dependencies": [ 115 | "compile", 116 | "lint" 117 | ] 118 | }, 119 | "compile": { 120 | "command": "tsc -p . --pretty --incremental", 121 | "files": [ 122 | "src/**/*.ts", 123 | "**/tsconfig.json", 124 | "messages/**" 125 | ], 126 | "output": [ 127 | "lib/**", 128 | "*.tsbuildinfo" 129 | ], 130 | "clean": "if-file-deleted" 131 | }, 132 | "format": { 133 | "command": "prettier --write \"+(src|test|schemas)/**/*.+(ts|js|json)|command-snapshot.json\"", 134 | "files": [ 135 | "src/**/*.ts", 136 | "test/**/*.ts", 137 | "schemas/**/*.json", 138 | "command-snapshot.json", 139 | ".prettier*" 140 | ], 141 | "output": [] 142 | }, 143 | "lint": { 144 | "command": "eslint src test --color --cache --cache-location .eslintcache", 145 | "files": [ 146 | "src/**/*.ts", 147 | "test/**/*.ts", 148 | "messages/**", 149 | "**/.eslint*", 150 | "**/tsconfig.json" 151 | ], 152 | "output": [] 153 | }, 154 | "test:compile": { 155 | "command": "tsc -p \"./test\" --pretty", 156 | "files": [ 157 | "test/**/*.ts", 158 | "**/tsconfig.json" 159 | ], 160 | "output": [] 161 | }, 162 | "test": { 163 | "dependencies": [ 164 | "test:compile", 165 | "test:only", 166 | "lint" 167 | ] 168 | }, 169 | "test:only": { 170 | "command": "jest --coverage", 171 | "env": { 172 | "FORCE_COLOR": "2" 173 | }, 174 | "files": [ 175 | "test/**/*.ts", 176 | "src/**/*.ts", 177 | "**/tsconfig.json", 178 | ".mocha*", 179 | "!*.nut.ts", 180 | ".nycrc" 181 | ], 182 | "output": [] 183 | }, 184 | "test:command-reference": { 185 | "command": "\"./bin/dev\" commandreference:generate --erroronwarnings", 186 | "files": [ 187 | "src/**/*.ts", 188 | "messages/**", 189 | "package.json" 190 | ], 191 | "output": [ 192 | "tmp/root" 193 | ] 194 | }, 195 | "test:deprecation-policy": { 196 | "command": "\"./bin/dev\" snapshot:compare", 197 | "files": [ 198 | "src/**/*.ts" 199 | ], 200 | "output": [], 201 | "dependencies": [ 202 | "compile" 203 | ] 204 | }, 205 | "test:json-schema": { 206 | "command": "\"./bin/dev\" schema:compare", 207 | "files": [ 208 | "src/**/*.ts", 209 | "schemas" 210 | ], 211 | "output": [] 212 | } 213 | }, 214 | "exports": "./lib/index.js", 215 | "type": "module", 216 | "author": "Matt Carvin", 217 | "repository": { 218 | "type": "git", 219 | "url": "git+https://github.com/mcarvin8/apex-code-coverage-transformer.git" 220 | }, 221 | "bugs": { 222 | "url": "https://github.com/mcarvin8/apex-code-coverage-transformer/issues" 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /samples/classes/AccountHandler.cls: -------------------------------------------------------------------------------- 1 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 2 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 3 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 4 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 5 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 6 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 7 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 8 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 9 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 10 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 11 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 12 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 13 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 14 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 15 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 16 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 17 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 18 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 19 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 20 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 21 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 22 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 23 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 24 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 25 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 26 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 27 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 28 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 29 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 30 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 31 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 32 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 33 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 34 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 35 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 36 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 37 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 38 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 39 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 40 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 41 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 42 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 43 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 44 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 45 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 46 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 47 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 48 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 49 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 50 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 51 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 52 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 53 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 54 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 55 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 56 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 57 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 58 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 59 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 60 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 61 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 62 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 63 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 64 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 65 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 66 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 67 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 68 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 69 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 70 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 71 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 72 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 73 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 74 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 75 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 76 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 77 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 78 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 79 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 80 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 81 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 82 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 83 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 84 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 85 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 86 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 87 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 88 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 89 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 90 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 91 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 92 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 93 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 94 | -------------------------------------------------------------------------------- /samples/classes/AccountProfile.cls: -------------------------------------------------------------------------------- 1 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 2 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 3 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 4 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 5 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 6 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 7 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 8 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 9 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 10 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 11 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 12 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 13 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 14 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 15 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 16 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 17 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 18 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 19 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 20 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 21 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 22 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 23 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 24 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 25 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 26 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 27 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 28 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 29 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 30 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 31 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 32 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 33 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 34 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 35 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 36 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 37 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 38 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 39 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 40 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 41 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 42 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 43 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 44 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 45 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 46 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 47 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 48 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 49 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 50 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 51 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 52 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 53 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 54 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 55 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 56 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 57 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 58 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 59 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 60 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 61 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 62 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 63 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 64 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 65 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 66 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 67 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 68 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 69 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 70 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 71 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 72 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 73 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 74 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 75 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 76 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 77 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 78 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 79 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 80 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 81 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 82 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 83 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 84 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 85 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 86 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 87 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 88 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 89 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 90 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 91 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 92 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 93 | // This is a sample Apex class that needs to have as many lines as the coverage reports under test have 94 | -------------------------------------------------------------------------------- /samples/triggers/AccountTrigger.trigger: -------------------------------------------------------------------------------- 1 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 2 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 3 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 4 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 5 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 6 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 7 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 8 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 9 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 10 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 11 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 12 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 13 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 14 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 15 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 16 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 17 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 18 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 19 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 20 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 21 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 22 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 23 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 24 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 25 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 26 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 27 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 28 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 29 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 30 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 31 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 32 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 33 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 34 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 35 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 36 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 37 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 38 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 39 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 40 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 41 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 42 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 43 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 44 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 45 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 46 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 47 | 48 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 49 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 50 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 51 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 52 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 53 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 54 | 55 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have// This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 56 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 57 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 58 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 59 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 60 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 61 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 62 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 63 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 64 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 65 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 66 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 67 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 68 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 69 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 70 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 71 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 72 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 73 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 74 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 75 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 76 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 77 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 78 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 79 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 80 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 81 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 82 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 83 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 84 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 85 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 86 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have// This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 87 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 88 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 89 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 90 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 91 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 92 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 93 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 94 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 95 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have// This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 96 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 97 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 98 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 99 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 100 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 101 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 102 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 103 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 104 | // This is a sample Apex trigger that needs to have as many lines as the coverage reports under test have 105 | -------------------------------------------------------------------------------- /src/commands/acc-transformer/transform.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { SfCommand, Flags } from '@salesforce/sf-plugins-core'; 4 | import { Messages } from '@salesforce/core'; 5 | import { TransformerTransformResult } from '../../utils/types.js'; 6 | import { transformCoverageReport } from '../../transformers/coverageTransformer.js'; 7 | import { formatOptions } from '../../utils/constants.js'; 8 | 9 | Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); 10 | const messages = Messages.loadMessages('apex-code-coverage-transformer', 'transformer.transform'); 11 | 12 | export default class TransformerTransform extends SfCommand { 13 | public static override readonly summary = messages.getMessage('summary'); 14 | public static override readonly description = messages.getMessage('description'); 15 | public static override readonly examples = messages.getMessages('examples'); 16 | 17 | public static override readonly flags = { 18 | 'coverage-json': Flags.file({ 19 | summary: messages.getMessage('flags.coverage-json.summary'), 20 | char: 'j', 21 | required: true, 22 | }), 23 | 'output-report': Flags.file({ 24 | summary: messages.getMessage('flags.output-report.summary'), 25 | char: 'r', 26 | required: true, 27 | default: 'coverage.xml', 28 | }), 29 | format: Flags.string({ 30 | summary: messages.getMessage('flags.format.summary'), 31 | char: 'f', 32 | required: false, 33 | multiple: true, 34 | options: formatOptions, 35 | }), 36 | 'ignore-package-directory': Flags.directory({ 37 | summary: messages.getMessage('flags.ignore-package-directory.summary'), 38 | char: 'i', 39 | required: false, 40 | multiple: true, 41 | }), 42 | }; 43 | 44 | public async run(): Promise { 45 | const { flags } = await this.parse(TransformerTransform); 46 | const warnings: string[] = []; 47 | 48 | const result = await transformCoverageReport( 49 | flags['coverage-json'], 50 | flags['output-report'], 51 | flags['format'] ?? ['sonar'], 52 | flags['ignore-package-directory'] ?? [] 53 | ); 54 | warnings.push(...result.warnings); 55 | const finalPath = result.finalPaths; 56 | 57 | if (warnings.length > 0) { 58 | warnings.forEach((warning) => { 59 | this.warn(warning); 60 | }); 61 | } 62 | 63 | this.log(`The coverage report has been written to: ${finalPath.join(', ')}`); 64 | return { path: finalPath }; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/handlers/clover.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { CloverCoverageObject, CloverFile, CoverageHandler } from '../utils/types.js'; 4 | 5 | export class CloverCoverageHandler implements CoverageHandler { 6 | private readonly coverageObj: CloverCoverageObject; 7 | 8 | public constructor() { 9 | this.coverageObj = { 10 | coverage: { 11 | '@generated': Date.now(), 12 | '@clover': '3.2.0', 13 | project: { 14 | '@timestamp': Date.now(), 15 | '@name': 'All files', 16 | metrics: { 17 | '@statements': 0, 18 | '@coveredstatements': 0, 19 | '@conditionals': 0, 20 | '@coveredconditionals': 0, 21 | '@methods': 0, 22 | '@coveredmethods': 0, 23 | '@elements': 0, 24 | '@coveredelements': 0, 25 | '@complexity': 0, 26 | '@loc': 0, 27 | '@ncloc': 0, 28 | '@packages': 1, 29 | '@files': 0, 30 | '@classes': 0, 31 | }, 32 | file: [], 33 | }, 34 | }, 35 | }; 36 | } 37 | 38 | public processFile(filePath: string, fileName: string, lines: Record): void { 39 | const uncoveredLines = Object.keys(lines) 40 | .filter((lineNumber) => lines[lineNumber] === 0) 41 | .map(Number); 42 | const coveredLines = Object.keys(lines) 43 | .filter((lineNumber) => lines[lineNumber] === 1) 44 | .map(Number); 45 | 46 | const fileObj: CloverFile = { 47 | '@name': fileName, 48 | '@path': filePath, 49 | metrics: { 50 | '@statements': uncoveredLines.length + coveredLines.length, 51 | '@coveredstatements': coveredLines.length, 52 | '@conditionals': 0, 53 | '@coveredconditionals': 0, 54 | '@methods': 0, 55 | '@coveredmethods': 0, 56 | }, 57 | line: [], 58 | }; 59 | for (const [lineNumber, isCovered] of Object.entries(lines)) { 60 | fileObj.line.push({ 61 | '@num': Number(lineNumber), 62 | '@count': isCovered === 1 ? 1 : 0, 63 | '@type': 'stmt', 64 | }); 65 | } 66 | this.coverageObj.coverage.project.file.push(fileObj); 67 | const projectMetrics = this.coverageObj.coverage.project.metrics; 68 | 69 | projectMetrics['@statements'] += uncoveredLines.length + coveredLines.length; 70 | projectMetrics['@coveredstatements'] += coveredLines.length; 71 | projectMetrics['@elements'] += uncoveredLines.length + coveredLines.length; 72 | projectMetrics['@coveredelements'] += coveredLines.length; 73 | projectMetrics['@files'] += 1; 74 | projectMetrics['@classes'] += 1; 75 | projectMetrics['@loc'] += uncoveredLines.length + coveredLines.length; 76 | projectMetrics['@ncloc'] += uncoveredLines.length + coveredLines.length; 77 | } 78 | 79 | public finalize(): CloverCoverageObject { 80 | if (this.coverageObj.coverage?.project?.file) { 81 | this.coverageObj.coverage.project.file.sort((a, b) => a['@path'].localeCompare(b['@path'])); 82 | } 83 | return this.coverageObj; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/handlers/cobertura.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { CoberturaCoverageObject, CoberturaPackage, CoberturaClass, CoverageHandler } from '../utils/types.js'; 4 | 5 | export class CoberturaCoverageHandler implements CoverageHandler { 6 | private readonly coverageObj: CoberturaCoverageObject; 7 | private packageMap: Map; 8 | 9 | public constructor() { 10 | this.coverageObj = { 11 | coverage: { 12 | '@lines-valid': 0, 13 | '@lines-covered': 0, 14 | '@line-rate': 0, 15 | '@branches-valid': 0, 16 | '@branches-covered': 0, 17 | '@branch-rate': 1, 18 | '@timestamp': Date.now(), 19 | '@complexity': 0, 20 | '@version': '0.1', 21 | sources: { source: ['.'] }, 22 | packages: { package: [] }, 23 | }, 24 | }; 25 | this.packageMap = new Map(); 26 | } 27 | 28 | public processFile(filePath: string, fileName: string, lines: Record): void { 29 | const packageName = filePath.split('/')[0]; // Extract root directory as package name 30 | 31 | if (!this.packageMap.has(packageName)) { 32 | this.packageMap.set(packageName, { 33 | '@name': packageName, 34 | '@line-rate': 0, 35 | '@branch-rate': 1, 36 | classes: { class: [] }, 37 | }); 38 | } 39 | 40 | const packageObj = this.packageMap.get(packageName)!; 41 | 42 | const classObj: CoberturaClass = { 43 | '@name': fileName, 44 | '@filename': filePath, 45 | '@line-rate': '0', 46 | '@branch-rate': '1', 47 | methods: {}, 48 | lines: { line: [] }, 49 | }; 50 | 51 | const uncoveredLines = Object.keys(lines) 52 | .filter((lineNumber) => lines[lineNumber] === 0) 53 | .map(Number); 54 | const coveredLines = Object.keys(lines) 55 | .filter((lineNumber) => lines[lineNumber] === 1) 56 | .map(Number); 57 | 58 | const totalLines = uncoveredLines.length + coveredLines.length; 59 | const coveredLineCount = coveredLines.length; 60 | 61 | for (const [lineNumber, isCovered] of Object.entries(lines)) { 62 | classObj.lines.line.push({ 63 | '@number': Number(lineNumber), 64 | '@hits': isCovered === 1 ? 1 : 0, 65 | '@branch': 'false', 66 | }); 67 | } 68 | 69 | if (totalLines > 0) { 70 | classObj['@line-rate'] = (coveredLineCount / totalLines).toFixed(4); 71 | } 72 | 73 | this.coverageObj.coverage['@lines-valid'] += totalLines; 74 | this.coverageObj.coverage['@lines-covered'] += coveredLineCount; 75 | 76 | packageObj.classes.class.push(classObj); 77 | this.packageMap.set(packageName, packageObj); 78 | } 79 | 80 | public finalize(): CoberturaCoverageObject { 81 | this.coverageObj.coverage.packages.package = Array.from(this.packageMap.values()); 82 | 83 | for (const pkg of this.coverageObj.coverage.packages.package) { 84 | const totalLines = pkg.classes.class.reduce( 85 | (sum, cls) => sum + parseFloat(cls['@line-rate']) * cls.lines.line.length, 86 | 0 87 | ); 88 | const totalClasses = pkg.classes.class.reduce((sum, cls) => sum + cls.lines.line.length, 0); 89 | 90 | pkg['@line-rate'] = parseFloat((totalLines / totalClasses).toFixed(4)); 91 | } 92 | 93 | this.coverageObj.coverage['@line-rate'] = parseFloat( 94 | (this.coverageObj.coverage['@lines-covered'] / this.coverageObj.coverage['@lines-valid']).toFixed(4) 95 | ); 96 | 97 | this.coverageObj.coverage.packages.package.sort((a, b) => a['@name'].localeCompare(b['@name'])); 98 | for (const pkg of this.coverageObj.coverage.packages.package) { 99 | if (pkg.classes?.class) { 100 | pkg.classes.class.sort((a, b) => a['@filename'].localeCompare(b['@filename'])); 101 | } 102 | } 103 | 104 | return this.coverageObj; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/handlers/getHandler.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { CoverageHandler } from '../utils/types.js'; 4 | import { CloverCoverageHandler } from './clover.js'; 5 | import { CoberturaCoverageHandler } from './cobertura.js'; 6 | import { SonarCoverageHandler } from './sonar.js'; 7 | import { LcovCoverageHandler } from './lcov.js'; 8 | import { JaCoCoCoverageHandler } from './jacoco.js'; 9 | import { IstanbulCoverageHandler } from './istanbulJson.js'; 10 | 11 | export function getCoverageHandler(format: string): CoverageHandler { 12 | const handlers: Record = { 13 | sonar: new SonarCoverageHandler(), 14 | cobertura: new CoberturaCoverageHandler(), 15 | clover: new CloverCoverageHandler(), 16 | lcovonly: new LcovCoverageHandler(), 17 | jacoco: new JaCoCoCoverageHandler(), 18 | json: new IstanbulCoverageHandler(), 19 | }; 20 | 21 | const handler = handlers[format]; 22 | if (!handler) { 23 | throw new Error(`Unsupported format: ${format}`); 24 | } 25 | return handler; 26 | } 27 | -------------------------------------------------------------------------------- /src/handlers/istanbulJson.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { 4 | IstanbulCoverageMap, 5 | IstanbulCoverageFile, 6 | IstanbulCoverageObject, 7 | CoverageHandler, 8 | SourceRange, 9 | } from '../utils/types.js'; 10 | 11 | export class IstanbulCoverageHandler implements CoverageHandler { 12 | private coverageMap: IstanbulCoverageMap = {}; 13 | 14 | public processFile(filePath: string, fileName: string, lines: Record): void { 15 | const statementMap: Record = {}; 16 | const s: Record = {}; 17 | const lineCoverage: Record = {}; 18 | 19 | for (const [lineNumber, hits] of Object.entries(lines)) { 20 | const line = Number(lineNumber); 21 | lineCoverage[lineNumber] = hits; 22 | statementMap[lineNumber] = { 23 | start: { line, column: 0 }, 24 | end: { line, column: 0 }, 25 | }; 26 | s[lineNumber] = hits; 27 | } 28 | 29 | const coverageFile: IstanbulCoverageFile = { 30 | path: filePath, 31 | statementMap, 32 | fnMap: {}, 33 | branchMap: {}, 34 | s, 35 | f: {}, 36 | b: {}, 37 | l: lineCoverage, 38 | }; 39 | 40 | this.coverageMap[filePath] = coverageFile; 41 | } 42 | 43 | public finalize(): IstanbulCoverageObject { 44 | return this.coverageMap; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/handlers/jacoco.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { JaCoCoCoverageObject, JaCoCoPackage, JaCoCoSourceFile, JaCoCoLine, CoverageHandler } from '../utils/types.js'; 4 | 5 | export class JaCoCoCoverageHandler implements CoverageHandler { 6 | private readonly coverageObj: JaCoCoCoverageObject; 7 | private packageMap: Record; 8 | 9 | public constructor() { 10 | this.coverageObj = { 11 | report: { 12 | '@name': 'JaCoCo', 13 | package: [], 14 | counter: [], 15 | }, 16 | }; 17 | this.packageMap = {}; // Stores packages by directory 18 | } 19 | 20 | public processFile(filePath: string, fileName: string, lines: Record): void { 21 | const pathParts = filePath.split('/'); 22 | const fileNamewithExt = pathParts.pop()!; 23 | const packageName = pathParts.join('/'); 24 | 25 | const packageObj = this.getOrCreatePackage(packageName); 26 | 27 | // Ensure source file only contains the filename, not the full path 28 | const sourceFileObj: JaCoCoSourceFile = { 29 | '@name': fileNamewithExt, 30 | line: [], 31 | counter: [], 32 | }; 33 | 34 | let coveredLines = 0; 35 | let totalLines = 0; 36 | 37 | for (const [lineNumber, isCovered] of Object.entries(lines)) { 38 | totalLines++; 39 | if (isCovered === 1) coveredLines++; 40 | 41 | const lineObj: JaCoCoLine = { 42 | '@nr': Number(lineNumber), 43 | '@mi': isCovered === 0 ? 1 : 0, 44 | '@ci': isCovered === 1 ? 1 : 0, 45 | '@mb': 0, 46 | '@cb': 0, 47 | }; 48 | sourceFileObj.line.push(lineObj); 49 | } 50 | 51 | // Add line coverage counter for the source file 52 | sourceFileObj.counter.push({ 53 | '@type': 'LINE', 54 | '@missed': totalLines - coveredLines, 55 | '@covered': coveredLines, 56 | }); 57 | 58 | packageObj.sourcefile.push(sourceFileObj); 59 | } 60 | 61 | public finalize(): JaCoCoCoverageObject { 62 | let overallCovered = 0; 63 | let overallMissed = 0; 64 | 65 | for (const packageObj of Object.values(this.packageMap)) { 66 | packageObj.sourcefile.sort((a, b) => a['@name'].localeCompare(b['@name'])); 67 | 68 | let packageCovered = 0; 69 | let packageMissed = 0; 70 | 71 | for (const sf of packageObj.sourcefile) { 72 | packageCovered += sf.counter[0]['@covered']; 73 | packageMissed += sf.counter[0]['@missed']; 74 | } 75 | 76 | packageObj.counter.push({ 77 | '@type': 'LINE', 78 | '@missed': packageMissed, 79 | '@covered': packageCovered, 80 | }); 81 | 82 | overallCovered += packageCovered; 83 | overallMissed += packageMissed; 84 | } 85 | 86 | this.coverageObj.report.counter.push({ 87 | '@type': 'LINE', 88 | '@missed': overallMissed, 89 | '@covered': overallCovered, 90 | }); 91 | 92 | return this.coverageObj; 93 | } 94 | 95 | private getOrCreatePackage(packageName: string): JaCoCoPackage { 96 | if (!this.packageMap[packageName]) { 97 | this.packageMap[packageName] = { 98 | '@name': packageName, 99 | sourcefile: [], 100 | counter: [], 101 | }; 102 | this.coverageObj.report.package.push(this.packageMap[packageName]); 103 | } 104 | return this.packageMap[packageName]; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/handlers/lcov.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { CoverageHandler, LcovCoverageObject, LcovFile } from '../utils/types.js'; 4 | 5 | export class LcovCoverageHandler implements CoverageHandler { 6 | private readonly coverageObj: LcovCoverageObject; 7 | 8 | public constructor() { 9 | this.coverageObj = { files: [] }; 10 | } 11 | 12 | public processFile(filePath: string, fileName: string, lines: Record): void { 13 | const uncoveredLines = Object.keys(lines) 14 | .filter((lineNumber) => lines[lineNumber] === 0) 15 | .map(Number); 16 | const coveredLines = Object.keys(lines) 17 | .filter((lineNumber) => lines[lineNumber] === 1) 18 | .map(Number); 19 | 20 | const lcovFile: LcovFile = { 21 | sourceFile: filePath, 22 | lines: [], 23 | totalLines: uncoveredLines.length + coveredLines.length, 24 | coveredLines: coveredLines.length, 25 | }; 26 | 27 | for (const [lineNumber, isCovered] of Object.entries(lines)) { 28 | lcovFile.lines.push({ 29 | lineNumber: Number(lineNumber), 30 | hitCount: isCovered === 1 ? 1 : 0, 31 | }); 32 | } 33 | 34 | this.coverageObj.files.push(lcovFile); 35 | } 36 | 37 | public finalize(): LcovCoverageObject { 38 | if ('files' in this.coverageObj && Array.isArray(this.coverageObj.files)) { 39 | this.coverageObj.files.sort((a, b) => a.sourceFile.localeCompare(b.sourceFile)); 40 | } 41 | return this.coverageObj; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/handlers/sonar.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { SonarCoverageObject, SonarClass, CoverageHandler } from '../utils/types.js'; 4 | 5 | export class SonarCoverageHandler implements CoverageHandler { 6 | private readonly coverageObj: SonarCoverageObject; 7 | 8 | public constructor() { 9 | this.coverageObj = { coverage: { '@version': '1', file: [] } }; 10 | } 11 | 12 | public processFile(filePath: string, _fileName: string, lines: Record): void { 13 | const fileObj: SonarClass = { 14 | '@path': filePath, 15 | lineToCover: [], 16 | }; 17 | for (const [lineNumberString, value] of Object.entries(lines)) { 18 | const covered = value === 1 ? 'true' : 'false'; 19 | fileObj.lineToCover.push({ 20 | '@lineNumber': Number(lineNumberString), 21 | '@covered': covered, 22 | }); 23 | } 24 | 25 | this.coverageObj.coverage.file.push(fileObj); 26 | } 27 | 28 | public finalize(): SonarCoverageObject { 29 | if (this.coverageObj.coverage?.file) { 30 | this.coverageObj.coverage.file.sort((a, b) => a['@path'].localeCompare(b['@path'])); 31 | } 32 | return this.coverageObj; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/hooks/postrun.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { existsSync } from 'node:fs'; 4 | import { readFile } from 'node:fs/promises'; 5 | import { resolve } from 'node:path'; 6 | import { Hook } from '@oclif/core'; 7 | 8 | import TransformerTransform from '../commands/acc-transformer/transform.js'; 9 | import { HookFile } from '../utils/types.js'; 10 | import { getRepoRoot } from '../utils/getRepoRoot.js'; 11 | 12 | export const postrun: Hook<'postrun'> = async function (options) { 13 | let commandType: string; 14 | let coverageJson: string; 15 | if ( 16 | [ 17 | 'project:deploy:validate', 18 | 'project:deploy:start', 19 | 'project:deploy:report', 20 | 'project:deploy:resume', 21 | 'hardis:project:deploy:smart', 22 | ].includes(options.Command.id) 23 | ) { 24 | commandType = 'deploy'; 25 | } else if (['apex:run:test', 'apex:get:test', 'hardis:org:test:apex'].includes(options.Command.id)) { 26 | commandType = 'test'; 27 | } else { 28 | return; 29 | } 30 | let configFile: HookFile; 31 | const { repoRoot } = await getRepoRoot(); 32 | if (!repoRoot) { 33 | return; 34 | } 35 | const configPath = resolve(repoRoot, '.apexcodecovtransformer.config.json'); 36 | 37 | try { 38 | const jsonString: string = await readFile(configPath, 'utf-8'); 39 | configFile = JSON.parse(jsonString) as HookFile; 40 | } catch (error) { 41 | return; 42 | } 43 | 44 | const outputReport: string = configFile.outputReportPath || 'coverage.xml'; 45 | const coverageFormat: string = configFile.format || 'sonar'; 46 | const ignorePackageDirs: string = configFile.ignorePackageDirectories || ''; 47 | 48 | if (commandType === 'deploy') { 49 | coverageJson = configFile.deployCoverageJsonPath || '.'; 50 | } else { 51 | coverageJson = configFile.testCoverageJsonPath || '.'; 52 | } 53 | 54 | if (coverageJson.trim() === '.') { 55 | return; 56 | } 57 | 58 | const coverageJsonPath = resolve(coverageJson); 59 | const outputReportPath = resolve(outputReport); 60 | 61 | if (!existsSync(coverageJsonPath)) { 62 | return; 63 | } 64 | 65 | const commandArgs: string[] = []; 66 | commandArgs.push('--coverage-json'); 67 | commandArgs.push(coverageJsonPath); 68 | commandArgs.push('--output-report'); 69 | commandArgs.push(outputReportPath); 70 | if (coverageFormat.trim() !== '') { 71 | const formatArray: string[] = coverageFormat.split(','); 72 | for (const format of formatArray) { 73 | const sanitizedFormat = format.replace(/,/g, ''); 74 | commandArgs.push('--format'); 75 | commandArgs.push(sanitizedFormat); 76 | } 77 | } 78 | if (ignorePackageDirs.trim() !== '') { 79 | const ignorePackageDirArray: string[] = ignorePackageDirs.split(','); 80 | for (const dirs of ignorePackageDirArray) { 81 | const sanitizedDir = dirs.replace(/,/g, ''); 82 | commandArgs.push('--ignore-package-directory'); 83 | commandArgs.push(sanitizedDir); 84 | } 85 | } 86 | await TransformerTransform.run(commandArgs); 87 | }; 88 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /src/transformers/coverageTransformer.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | import { readFile } from 'node:fs/promises'; 3 | import { mapLimit } from 'async'; 4 | 5 | import { getCoverageHandler } from '../handlers/getHandler.js'; 6 | import { DeployCoverageData, TestCoverageData, CoverageProcessingContext } from '../utils/types.js'; 7 | import { getPackageDirectories } from '../utils/getPackageDirectories.js'; 8 | import { findFilePath } from '../utils/findFilePath.js'; 9 | import { setCoveredLines } from '../utils/setCoveredLines.js'; 10 | import { getConcurrencyThreshold } from '../utils/getConcurrencyThreshold.js'; 11 | import { checkCoverageDataType } from '../utils/setCoverageDataType.js'; 12 | import { generateAndWriteReport } from './reportGenerator.js'; 13 | 14 | type CoverageInput = DeployCoverageData | TestCoverageData[]; 15 | 16 | export async function transformCoverageReport( 17 | jsonFilePath: string, 18 | outputReportPath: string, 19 | formats: string[], 20 | ignoreDirs: string[] 21 | ): Promise<{ finalPaths: string[]; warnings: string[] }> { 22 | const warnings: string[] = []; 23 | const finalPaths: string[] = []; 24 | const formatAmount: number = formats.length; 25 | let filesProcessed = 0; 26 | 27 | const jsonData = await tryReadJson(jsonFilePath, warnings); 28 | if (!jsonData) return { finalPaths: [outputReportPath], warnings }; 29 | 30 | const parsedData = JSON.parse(jsonData) as CoverageInput; 31 | const { repoRoot, packageDirectories } = await getPackageDirectories(ignoreDirs); 32 | const handlers = createHandlers(formats); 33 | const commandType = checkCoverageDataType(parsedData); 34 | const concurrencyLimit = getConcurrencyThreshold(); 35 | 36 | const context: CoverageProcessingContext = { 37 | handlers, 38 | packageDirs: packageDirectories, 39 | repoRoot, 40 | concurrencyLimit, 41 | warnings, 42 | }; 43 | 44 | if (commandType === 'DeployCoverageData') { 45 | filesProcessed = await processDeployCoverage(parsedData as DeployCoverageData, context); 46 | } else if (commandType === 'TestCoverageData') { 47 | filesProcessed = await processTestCoverage(parsedData as TestCoverageData[], context); 48 | } else { 49 | throw new Error( 50 | 'The provided JSON does not match a known coverage data format from the Salesforce deploy or test command.' 51 | ); 52 | } 53 | 54 | if (filesProcessed === 0) { 55 | warnings.push('None of the files listed in the coverage JSON were processed. The coverage report will be empty.'); 56 | } 57 | 58 | for (const [format, handler] of handlers.entries()) { 59 | const coverageObj = handler.finalize(); 60 | const finalPath = await generateAndWriteReport(outputReportPath, coverageObj, format, formatAmount); 61 | finalPaths.push(finalPath); 62 | } 63 | 64 | return { finalPaths, warnings }; 65 | } 66 | 67 | async function tryReadJson(path: string, warnings: string[]): Promise { 68 | try { 69 | return await readFile(path, 'utf-8'); 70 | } catch { 71 | warnings.push(`Failed to read ${path}. Confirm file exists.`); 72 | return null; 73 | } 74 | } 75 | 76 | function createHandlers(formats: string[]): Map> { 77 | const handlers = new Map>(); 78 | for (const format of formats) { 79 | handlers.set(format, getCoverageHandler(format)); 80 | } 81 | return handlers; 82 | } 83 | 84 | async function processDeployCoverage(data: DeployCoverageData, context: CoverageProcessingContext): Promise { 85 | let processed = 0; 86 | await mapLimit(Object.keys(data), context.concurrencyLimit, async (fileName: string) => { 87 | const fileInfo = data[fileName]; 88 | const formattedName = fileName.replace(/no-map[\\/]+/, ''); 89 | const path = await findFilePath(formattedName, context.packageDirs, context.repoRoot); 90 | 91 | if (!path) { 92 | context.warnings.push(`The file name ${formattedName} was not found in any package directory.`); 93 | return; 94 | } 95 | 96 | fileInfo.s = await setCoveredLines(path, context.repoRoot, fileInfo.s); 97 | for (const handler of context.handlers.values()) { 98 | handler.processFile(path, formattedName, fileInfo.s); 99 | } 100 | processed++; 101 | }); 102 | return processed; 103 | } 104 | 105 | async function processTestCoverage(data: TestCoverageData[], context: CoverageProcessingContext): Promise { 106 | let processed = 0; 107 | await mapLimit(data, context.concurrencyLimit, async (entry: TestCoverageData) => { 108 | const formattedName = entry.name.replace(/no-map[\\/]+/, ''); 109 | const path = await findFilePath(formattedName, context.packageDirs, context.repoRoot); 110 | 111 | if (!path) { 112 | context.warnings.push(`The file name ${formattedName} was not found in any package directory.`); 113 | return; 114 | } 115 | 116 | for (const handler of context.handlers.values()) { 117 | handler.processFile(path, formattedName, entry.lines); 118 | } 119 | processed++; 120 | }); 121 | return processed; 122 | } 123 | -------------------------------------------------------------------------------- /src/transformers/reportGenerator.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'node:fs/promises'; 2 | import { extname, basename, dirname, join } from 'node:path'; 3 | import { create } from 'xmlbuilder2'; 4 | 5 | import { 6 | SonarCoverageObject, 7 | CoberturaCoverageObject, 8 | CloverCoverageObject, 9 | LcovCoverageObject, 10 | JaCoCoCoverageObject, 11 | IstanbulCoverageObject, 12 | } from '../utils/types.js'; 13 | 14 | export async function generateAndWriteReport( 15 | outputPath: string, 16 | coverageObj: 17 | | SonarCoverageObject 18 | | CoberturaCoverageObject 19 | | CloverCoverageObject 20 | | LcovCoverageObject 21 | | JaCoCoCoverageObject 22 | | IstanbulCoverageObject, 23 | format: string, 24 | formatAmount: number 25 | ): Promise { 26 | const content = generateReportContent(coverageObj, format); 27 | const extension = getExtensionForFormat(format); 28 | 29 | const base = basename(outputPath, extname(outputPath)); // e.g., 'coverage' 30 | const dir = dirname(outputPath); 31 | 32 | const suffix = formatAmount > 1 ? `-${format}` : ''; 33 | const filePath = join(dir, `${base}${suffix}${extension}`); 34 | 35 | await writeFile(filePath, content, 'utf-8'); 36 | return filePath; 37 | } 38 | 39 | function generateReportContent( 40 | coverageObj: 41 | | SonarCoverageObject 42 | | CoberturaCoverageObject 43 | | CloverCoverageObject 44 | | LcovCoverageObject 45 | | JaCoCoCoverageObject 46 | | IstanbulCoverageObject, 47 | format: string 48 | ): string { 49 | if (format === 'lcovonly' && isLcovCoverageObject(coverageObj)) { 50 | return generateLcov(coverageObj); 51 | } 52 | 53 | if (format === 'json') { 54 | return JSON.stringify(coverageObj, null, 2); 55 | } 56 | 57 | const isHeadless = ['cobertura', 'clover', 'jacoco'].includes(format); 58 | const xml = create(coverageObj).end({ prettyPrint: true, indent: ' ', headless: isHeadless }); 59 | 60 | return prependXmlHeader(xml, format); 61 | } 62 | 63 | function generateLcov(coverageObj: LcovCoverageObject): string { 64 | return coverageObj.files 65 | .map((file) => { 66 | const lineData = file.lines.map((line) => `DA:${line.lineNumber},${line.hitCount}`).join('\n'); 67 | return [ 68 | 'TN:', 69 | `SF:${file.sourceFile}`, 70 | 'FNF:0', 71 | 'FNH:0', 72 | lineData, 73 | `LF:${file.totalLines}`, 74 | `LH:${file.coveredLines}`, 75 | 'BRF:0', 76 | 'BRH:0', 77 | 'end_of_record', 78 | ].join('\n'); 79 | }) 80 | .join('\n'); 81 | } 82 | 83 | function prependXmlHeader(xml: string, format: string): string { 84 | switch (format) { 85 | case 'cobertura': 86 | return `\n\n${xml}`; 87 | case 'clover': 88 | return `\n${xml}`; 89 | case 'jacoco': 90 | return `\n\n${xml}`; 91 | default: 92 | return xml; 93 | } 94 | } 95 | 96 | export function getExtensionForFormat(format: string): string { 97 | if (format === 'lcovonly') return '.info'; 98 | if (format === 'json') return '.json'; 99 | return '.xml'; 100 | } 101 | 102 | function isLcovCoverageObject(obj: unknown): obj is LcovCoverageObject { 103 | return typeof obj === 'object' && obj !== null && 'files' in obj; 104 | } 105 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const formatOptions: string[] = ['sonar', 'cobertura', 'clover', 'lcovonly', 'jacoco', 'json']; 2 | -------------------------------------------------------------------------------- /src/utils/findFilePath.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-disable no-await-in-loop */ 3 | 4 | import { readdir, stat } from 'node:fs/promises'; 5 | import { join, relative } from 'node:path'; 6 | import { normalizePathToUnix } from './normalizePathToUnix.js'; 7 | 8 | export async function findFilePath( 9 | fileName: string, 10 | packageDirectories: string[], 11 | repoRoot: string 12 | ): Promise { 13 | for (const directory of packageDirectories) { 14 | const result = await resolveFilePath(fileName, directory, repoRoot); 15 | if (result) { 16 | return normalizePathToUnix(result); 17 | } 18 | } 19 | return undefined; 20 | } 21 | 22 | async function resolveFilePath(fileName: string, dxDirectory: string, repoRoot: string): Promise { 23 | const extensionsToTry = getExtensionsToTry(fileName); 24 | 25 | for (const name of extensionsToTry) { 26 | const absolutePath = await searchRecursively(name, dxDirectory); 27 | if (absolutePath) { 28 | return relative(repoRoot, absolutePath); 29 | } 30 | } 31 | 32 | return undefined; 33 | } 34 | 35 | function getExtensionsToTry(fileName: string): string[] { 36 | return ['cls', 'trigger'].map((ext) => `${fileName}.${ext}`); 37 | } 38 | 39 | async function searchRecursively(fileName: string, directory: string): Promise { 40 | const entries = await readdir(directory); 41 | 42 | for (const entry of entries) { 43 | const fullPath = join(directory, entry); 44 | const stats = await stat(fullPath); 45 | 46 | if (stats.isDirectory()) { 47 | const nestedResult = await searchRecursively(fileName, fullPath); 48 | if (nestedResult) return nestedResult; 49 | } else if (entry === fileName) { 50 | return fullPath; 51 | } 52 | } 53 | 54 | return undefined; 55 | } 56 | -------------------------------------------------------------------------------- /src/utils/getConcurrencyThreshold.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { availableParallelism } from 'node:os'; 4 | 5 | export function getConcurrencyThreshold(): number { 6 | const AVAILABLE_PARALLELISM: number = availableParallelism(); 7 | 8 | return Math.min(AVAILABLE_PARALLELISM, 6); 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/getPackageDirectories.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { readFile } from 'node:fs/promises'; 4 | import { resolve } from 'node:path'; 5 | 6 | import { SfdxProject } from './types.js'; 7 | import { getRepoRoot } from './getRepoRoot.js'; 8 | 9 | export async function getPackageDirectories( 10 | ignoreDirectories: string[] 11 | ): Promise<{ repoRoot: string; packageDirectories: string[] }> { 12 | const { repoRoot, dxConfigFilePath } = (await getRepoRoot()) as { 13 | repoRoot: string; 14 | dxConfigFilePath: string; 15 | }; 16 | 17 | const sfdxProjectRaw: string = await readFile(dxConfigFilePath, 'utf-8'); 18 | const sfdxProject: SfdxProject = JSON.parse(sfdxProjectRaw) as SfdxProject; 19 | 20 | const packageDirectories = sfdxProject.packageDirectories 21 | .filter((directory) => !ignoreDirectories.includes(directory.path)) // Ignore exact folder names 22 | .map((directory) => resolve(repoRoot, directory.path)); 23 | 24 | return { repoRoot, packageDirectories }; 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/getRepoRoot.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { access } from 'node:fs/promises'; 3 | import { join, dirname } from 'node:path'; 4 | 5 | async function findRepoRoot(dir: string): Promise<{ repoRoot: string; dxConfigFilePath: string }> { 6 | const filePath = join(dir, 'sfdx-project.json'); 7 | try { 8 | // Check if sfdx-project.json exists in the current directory 9 | await access(filePath); 10 | return { repoRoot: dir, dxConfigFilePath: filePath }; 11 | } catch { 12 | const parentDir = dirname(dir); 13 | if (dir === parentDir) { 14 | // Reached the root without finding the file, throw an error 15 | throw new Error('sfdx-project.json not found in any parent directory.'); 16 | } 17 | // Recursively search in the parent directory 18 | return findRepoRoot(parentDir); 19 | } 20 | } 21 | 22 | export async function getRepoRoot(): Promise<{ repoRoot: string | undefined; dxConfigFilePath: string | undefined }> { 23 | const currentDir = process.cwd(); 24 | return findRepoRoot(currentDir); 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/getTotalLines.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { readFile } from 'node:fs/promises'; 4 | 5 | export async function getTotalLines(filePath: string): Promise { 6 | const fileContent = await readFile(filePath, 'utf8'); 7 | return fileContent.split(/\r\n|\r|\n/).length; 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/normalizePathToUnix.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | export function normalizePathToUnix(path: string): string { 4 | return path.replace(/\\/g, '/'); 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/setCoverageDataType.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { DeployCoverageData, TestCoverageData } from './types.js'; 4 | 5 | function isObject(val: unknown): val is Record { 6 | return typeof val === 'object' && val !== null; 7 | } 8 | 9 | function isValidPosition(pos: unknown): boolean { 10 | return isObject(pos) && typeof pos.line === 'number' && typeof pos.column === 'number'; 11 | } 12 | 13 | function isValidStatementMap(statementMap: unknown): boolean { 14 | if (!isObject(statementMap)) return false; 15 | 16 | return Object.values(statementMap).every((statement) => { 17 | if (!isObject(statement)) return false; 18 | const { start, end } = statement as { start: unknown; end: unknown }; 19 | return isValidPosition(start) && isValidPosition(end); 20 | }); 21 | } 22 | 23 | function isValidDeployItem(item: unknown): boolean { 24 | if (!isObject(item)) return false; 25 | 26 | const { path, fnMap, branchMap, f, b, s, statementMap } = item; 27 | 28 | const checks = [ 29 | typeof path === 'string', 30 | isObject(fnMap), 31 | isObject(branchMap), 32 | isObject(f), 33 | isObject(b), 34 | isObject(s), 35 | isValidStatementMap(statementMap), 36 | ]; 37 | 38 | return checks.every(Boolean); 39 | } 40 | 41 | function isDeployCoverageData(data: unknown): data is DeployCoverageData { 42 | if (!isObject(data)) return false; 43 | return Object.entries(data).every(([, item]) => isValidDeployItem(item)); 44 | } 45 | 46 | function isSingleTestCoverageData(data: unknown): data is TestCoverageData { 47 | if (!isObject(data)) return false; 48 | 49 | const { id, name, totalLines, lines, totalCovered, coveredPercent } = data; 50 | 51 | const checks = [ 52 | typeof id === 'string', 53 | typeof name === 'string', 54 | typeof totalLines === 'number', 55 | typeof totalCovered === 'number', 56 | typeof coveredPercent === 'number', 57 | isObject(lines), 58 | isObject(lines) && Object.values(lines).every((line) => typeof line === 'number'), 59 | ]; 60 | 61 | return checks.every(Boolean); 62 | } 63 | 64 | function isTestCoverageDataArray(data: unknown): data is TestCoverageData[] { 65 | return Array.isArray(data) && data.every(isSingleTestCoverageData); 66 | } 67 | 68 | export function checkCoverageDataType( 69 | data: DeployCoverageData | TestCoverageData[] 70 | ): 'DeployCoverageData' | 'TestCoverageData' | 'Unknown' { 71 | if (isDeployCoverageData(data)) return 'DeployCoverageData'; 72 | if (isTestCoverageDataArray(data)) return 'TestCoverageData'; 73 | return 'Unknown'; 74 | } 75 | -------------------------------------------------------------------------------- /src/utils/setCoveredLines.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { join } from 'node:path'; 4 | import { getTotalLines } from './getTotalLines.js'; 5 | 6 | export async function setCoveredLines( 7 | filePath: string, 8 | repoRoot: string, 9 | lines: Record 10 | ): Promise> { 11 | const totalLines = await getTotalLines(join(repoRoot, filePath)); 12 | const updatedLines: Record = {}; 13 | const usedLines = new Set(); 14 | 15 | const sortedLines = Object.entries(lines).sort(([lineA], [lineB]) => parseInt(lineA, 10) - parseInt(lineB, 10)); 16 | 17 | for (const [line, status] of sortedLines) { 18 | const lineNumber = parseInt(line, 10); 19 | 20 | if (status === 1 && lineNumber > totalLines) { 21 | for (let randomLine = 1; randomLine <= totalLines; randomLine++) { 22 | if (!usedLines.has(randomLine)) { 23 | updatedLines[randomLine.toString()] = status; 24 | usedLines.add(randomLine); 25 | break; 26 | } 27 | } 28 | } else { 29 | updatedLines[line] = status; 30 | usedLines.add(lineNumber); 31 | } 32 | } 33 | 34 | return updatedLines; 35 | } 36 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { getCoverageHandler } from '../handlers/getHandler.js'; 3 | 4 | export type TransformerTransformResult = { 5 | path: string[]; 6 | }; 7 | 8 | export type DeployCoverageData = { 9 | [className: string]: { 10 | fnMap: Record; 11 | branchMap: Record; 12 | path: string; 13 | f: Record; 14 | b: Record; 15 | s: Record; 16 | statementMap: Record< 17 | string, 18 | { 19 | start: { line: number; column: number }; 20 | end: { line: number; column: number }; 21 | } 22 | >; 23 | }; 24 | }; 25 | 26 | export type TestCoverageData = { 27 | id: string; 28 | name: string; 29 | totalLines: number; 30 | lines: Record; 31 | totalCovered: number; 32 | coveredPercent: number; 33 | }; 34 | 35 | export type SfdxProject = { 36 | packageDirectories: Array<{ path: string }>; 37 | }; 38 | 39 | export type CoverageProcessingContext = { 40 | handlers: Map>; 41 | packageDirs: string[]; 42 | repoRoot: string; 43 | concurrencyLimit: number; 44 | warnings: string[]; 45 | }; 46 | 47 | type SonarLine = { 48 | '@lineNumber': number; 49 | '@covered': string; 50 | }; 51 | 52 | export type SonarClass = { 53 | '@path': string; 54 | lineToCover: SonarLine[]; 55 | }; 56 | 57 | export type SonarCoverageObject = { 58 | coverage: { 59 | file: SonarClass[]; 60 | '@version': string; 61 | }; 62 | }; 63 | 64 | export type HookFile = { 65 | deployCoverageJsonPath: string; 66 | testCoverageJsonPath: string; 67 | outputReportPath: string; 68 | format: string; 69 | ignorePackageDirectories: string; 70 | }; 71 | 72 | export type CoberturaLine = { 73 | '@number': number; 74 | '@hits': number; 75 | '@branch': string; 76 | }; 77 | 78 | export type CoberturaClass = { 79 | '@name': string; 80 | '@filename': string; 81 | '@line-rate': string; 82 | '@branch-rate': string; 83 | methods: Record; 84 | lines: { 85 | line: CoberturaLine[]; 86 | }; 87 | }; 88 | 89 | export type CoberturaPackage = { 90 | '@name': string; 91 | '@line-rate': number; 92 | '@branch-rate': number; 93 | classes: { 94 | class: CoberturaClass[]; 95 | }; 96 | }; 97 | 98 | export type CoberturaCoverageObject = { 99 | coverage: { 100 | '@lines-valid': number; 101 | '@lines-covered': number; 102 | '@line-rate': number; 103 | '@branches-valid': number; 104 | '@branches-covered': number; 105 | '@branch-rate': number | string; 106 | '@timestamp': number; 107 | '@complexity': number; 108 | '@version': string; 109 | sources: { 110 | source: string[]; 111 | }; 112 | packages: { 113 | package: CoberturaPackage[]; 114 | }; 115 | }; 116 | }; 117 | 118 | export type CloverLine = { 119 | '@num': number; 120 | '@count': number; 121 | '@type': string; 122 | }; 123 | 124 | export type CloverFile = { 125 | '@name': string; 126 | '@path': string; 127 | metrics: { 128 | '@statements': number; 129 | '@coveredstatements': number; 130 | '@conditionals': number; 131 | '@coveredconditionals': number; 132 | '@methods': number; 133 | '@coveredmethods': number; 134 | }; 135 | line: CloverLine[]; 136 | }; 137 | 138 | type CloverProjectMetrics = { 139 | '@statements': number; 140 | '@coveredstatements': number; 141 | '@conditionals': number; 142 | '@coveredconditionals': number; 143 | '@methods': number; 144 | '@coveredmethods': number; 145 | '@elements': number; 146 | '@coveredelements': number; 147 | '@complexity': number; 148 | '@loc': number; 149 | '@ncloc': number; 150 | '@packages': number; 151 | '@files': number; 152 | '@classes': number; 153 | }; 154 | 155 | type CloverProject = { 156 | '@timestamp': number; 157 | '@name': string; 158 | metrics: CloverProjectMetrics; 159 | file: CloverFile[]; 160 | }; 161 | 162 | export type CloverCoverageObject = { 163 | coverage: { 164 | '@generated': number; 165 | '@clover': string; 166 | project: CloverProject; 167 | }; 168 | }; 169 | 170 | export type CoverageHandler = { 171 | processFile(filePath: string, fileName: string, lines: Record): void; 172 | finalize(): 173 | | SonarCoverageObject 174 | | CoberturaCoverageObject 175 | | CloverCoverageObject 176 | | LcovCoverageObject 177 | | JaCoCoCoverageObject 178 | | IstanbulCoverageObject; 179 | }; 180 | 181 | type LcovLine = { 182 | lineNumber: number; 183 | hitCount: number; 184 | }; 185 | 186 | export type LcovFile = { 187 | sourceFile: string; 188 | lines: LcovLine[]; 189 | totalLines: number; 190 | coveredLines: number; 191 | }; 192 | 193 | export type LcovCoverageObject = { 194 | files: LcovFile[]; 195 | }; 196 | 197 | export type JaCoCoCoverageObject = { 198 | report: { 199 | '@name': string; 200 | package: JaCoCoPackage[]; 201 | counter: JaCoCoCounter[]; 202 | }; 203 | }; 204 | 205 | export type JaCoCoPackage = { 206 | '@name': string; 207 | sourcefile: JaCoCoSourceFile[]; 208 | counter: JaCoCoCounter[]; 209 | }; 210 | 211 | export type JaCoCoSourceFile = { 212 | '@name': string; 213 | line: JaCoCoLine[]; 214 | counter: JaCoCoCounter[]; 215 | }; 216 | 217 | export type JaCoCoLine = { 218 | '@nr': number; // Line number 219 | '@mi': number; // Missed (0 = not covered, 1 = covered) 220 | '@ci': number; // Covered (1 = covered, 0 = missed) 221 | '@mb'?: number; // Missed Branch (optional, can be adjusted if needed) 222 | '@cb'?: number; // Covered Branch (optional, can be adjusted if needed) 223 | }; 224 | 225 | export type JaCoCoCounter = { 226 | '@type': 'INSTRUCTION' | 'BRANCH' | 'LINE' | 'METHOD' | 'CLASS' | 'PACKAGE'; 227 | '@missed': number; 228 | '@covered': number; 229 | }; 230 | 231 | export type IstanbulCoverageMap = { 232 | [filePath: string]: IstanbulCoverageFile; 233 | }; 234 | 235 | export type IstanbulCoverageFile = { 236 | path: string; 237 | statementMap: Record; 238 | fnMap: Record; 239 | branchMap: Record; 240 | s: Record; // statement hits 241 | f: Record; // function hits 242 | b: Record; // branch hits 243 | l: Record; // line hits 244 | }; 245 | 246 | export type SourcePosition = { 247 | line: number; 248 | column: number; 249 | }; 250 | 251 | export type SourceRange = { 252 | start: SourcePosition; 253 | end: SourcePosition; 254 | }; 255 | 256 | export type FunctionMapping = { 257 | name: string; 258 | decl: SourceRange; 259 | loc: SourceRange; 260 | line: number; 261 | }; 262 | 263 | export type BranchMapping = { 264 | loc: SourceRange; 265 | type: string; 266 | locations: SourceRange[]; 267 | line: number; 268 | }; 269 | 270 | export type IstanbulCoverageObject = IstanbulCoverageMap; // alias for clarity 271 | -------------------------------------------------------------------------------- /test/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: '../.eslintrc.cjs', 3 | // Allow describe and it 4 | env: { mocha: true }, 5 | rules: { 6 | // Allow assert style expressions. i.e. expect(true).to.be.true 7 | 'no-unused-expressions': 'off', 8 | 9 | // It is common for tests to stub out method. 10 | 11 | // Return types are defined by the source code. Allows for quick overwrites. 12 | '@typescript-eslint/explicit-function-return-type': 'off', 13 | // Mocked out the methods that shouldn't do anything in the tests. 14 | '@typescript-eslint/no-empty-function': 'off', 15 | // Easily return a promise in a mocked method. 16 | '@typescript-eslint/require-await': 'off', 17 | header: 'off', 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /test/commands/acc-transformer/transform.nut.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { describe, it, expect } from '@jest/globals'; 4 | 5 | import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; 6 | import { formatOptions } from '../../../src/utils/constants.js'; 7 | import { inputJsons, invalidJson } from '../../utils/testConstants.js'; 8 | import { getExtensionForFormat } from '../../../src/transformers/reportGenerator.js'; 9 | import { compareToBaselines } from '../../utils/baselineCompare.js'; 10 | import { postTestCleanup } from '../../utils/testCleanup.js'; 11 | import { preTestSetup } from '../../utils/testSetup.js'; 12 | 13 | describe('acc-transformer transform NUTs', () => { 14 | let session: TestSession; 15 | const formatString = formatOptions.map((f) => `--format ${f}`).join(' '); 16 | 17 | beforeAll(async () => { 18 | session = await TestSession.create({ devhubAuthStrategy: 'NONE' }); 19 | await preTestSetup(); 20 | }); 21 | 22 | afterAll(async () => { 23 | await session?.clean(); 24 | await postTestCleanup(); 25 | }); 26 | 27 | inputJsons.forEach(({ label, path }) => { 28 | it(`transforms the ${label} command JSON file into all formats`, async () => { 29 | const command = `acc-transformer transform --coverage-json "${path}" --output-report "${label}.xml" ${formatString} -i "samples"`; 30 | const output = execCmd(command, { ensureExitCode: 0 }).shellOutput.stdout; 31 | 32 | const expectedOutput = 33 | 'The coverage report has been written to: ' + 34 | formatOptions 35 | .map((f) => { 36 | const ext = getExtensionForFormat(f); 37 | return `${label}-${f}${ext}`; 38 | }) 39 | .join(', '); 40 | 41 | expect(output.replace('\n', '')).toStrictEqual(expectedOutput); 42 | }); 43 | }); 44 | 45 | it('confirm the reports created are the same as the baselines.', async () => { 46 | await compareToBaselines(); 47 | }); 48 | 49 | it('confirms a failure on an invalid JSON file.', async () => { 50 | const command = `acc-transformer transform --coverage-json "${invalidJson}"`; 51 | const error = execCmd(command, { ensureExitCode: 1 }).shellOutput.stderr; 52 | 53 | expect(error.replace('\n', '')).toContain( 54 | 'The provided JSON does not match a known coverage data format from the Salesforce deploy or test command.' 55 | ); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/commands/acc-transformer/transform.test.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import { describe, it, expect } from '@jest/globals'; 3 | 4 | import { TestContext } from '@salesforce/core/testSetup'; 5 | import { transformCoverageReport } from '../../../src/transformers/coverageTransformer.js'; 6 | import { formatOptions } from '../../../src/utils/constants.js'; 7 | import { inputJsons, invalidJson, deployCoverage, testCoverage } from '../../utils/testConstants.js'; 8 | import { compareToBaselines } from '../../utils/baselineCompare.js'; 9 | import { postTestCleanup } from '../../utils/testCleanup.js'; 10 | import { preTestSetup } from '../../utils/testSetup.js'; 11 | 12 | describe('acc-transformer transform unit tests', () => { 13 | const $$ = new TestContext(); 14 | 15 | beforeAll(async () => { 16 | await preTestSetup(); 17 | }); 18 | 19 | afterEach(() => { 20 | $$.restore(); 21 | }); 22 | 23 | afterAll(async () => { 24 | await postTestCleanup(); 25 | }); 26 | 27 | inputJsons.forEach(({ label, path }) => { 28 | it(`transforms the ${label} command JSON file into all output formats`, async () => { 29 | await transformCoverageReport(path, `${label}.xml`, formatOptions, ['samples']); 30 | }); 31 | }); 32 | it('confirm the reports created are the same as the baselines.', async () => { 33 | await compareToBaselines(); 34 | }); 35 | it('confirms a failure on an invalid JSON file.', async () => { 36 | try { 37 | await transformCoverageReport(invalidJson, 'coverage.xml', ['sonar'], []); 38 | throw new Error('Command did not fail as expected'); 39 | } catch (error) { 40 | if (error instanceof Error) { 41 | expect(error.message).toContain( 42 | 'The provided JSON does not match a known coverage data format from the Salesforce deploy or test command.' 43 | ); 44 | } else { 45 | throw new Error('An unknown error type was thrown.'); 46 | } 47 | } 48 | }); 49 | it('confirms a warning with a JSON file that does not exist.', async () => { 50 | const result = await transformCoverageReport('nonexistent.json', 'coverage.xml', ['sonar'], []); 51 | expect(result.warnings).toContain('Failed to read nonexistent.json. Confirm file exists.'); 52 | }); 53 | it('ignore a package directory and produce a warning on the deploy command report.', async () => { 54 | const result = await transformCoverageReport( 55 | deployCoverage, 56 | 'coverage.xml', 57 | ['sonar'], 58 | ['packaged', 'force-app', 'samples'] 59 | ); 60 | expect(result.warnings).toContain('The file name AccountTrigger was not found in any package directory.'); 61 | }); 62 | it('ignore a package directory and produce a warning on the test command report.', async () => { 63 | const result = await transformCoverageReport(testCoverage, 'coverage.xml', ['sonar'], ['packaged', 'samples']); 64 | expect(result.warnings).toContain('The file name AccountTrigger was not found in any package directory.'); 65 | }); 66 | it('create a cobertura report using only 1 package directory', async () => { 67 | await transformCoverageReport(deployCoverage, 'coverage.xml', ['cobertura'], ['packaged', 'force-app']); 68 | }); 69 | it('create a jacoco report using only 1 package directory', async () => { 70 | await transformCoverageReport(deployCoverage, 'coverage.xml', ['jacoco'], ['packaged', 'force-app']); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@salesforce/dev-config/tsconfig-test-strict-esm", 3 | "include": ["./**/*.ts"], 4 | "compilerOptions": { 5 | "skipLibCheck": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/units/handler.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | 'use strict'; 3 | import { describe, it, expect } from '@jest/globals'; 4 | 5 | import { getCoverageHandler } from '../../src/handlers/getHandler.js'; 6 | 7 | describe('coverage handler unit test', () => { 8 | it('confirms a failure with an invalid format.', async () => { 9 | try { 10 | getCoverageHandler('invalid'); 11 | throw new Error('Command did not fail as expected'); 12 | } catch (error) { 13 | if (error instanceof Error) { 14 | expect(error.message).toContain('Unsupported format: invalid'); 15 | } else { 16 | throw new Error('An unknown error type was thrown.'); 17 | } 18 | } 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/units/repoRoot.test.ts: -------------------------------------------------------------------------------- 1 | import { access } from 'node:fs/promises'; 2 | import { getRepoRoot } from '../../src/utils/getRepoRoot.js'; 3 | 4 | jest.mock('node:fs/promises'); 5 | 6 | const accessMock = access as jest.Mock; 7 | 8 | describe('getRepoRoot recursion unit test', () => { 9 | it('recursively searches parent directories and eventually throws', async () => { 10 | // Start in a deeply nested directory 11 | const fakePath = '/a/b/c'; 12 | process.cwd = jest.fn(() => fakePath) as typeof process.cwd; 13 | 14 | // Set up access to fail for /a/b/c, /a/b, /a, / (4 levels) 15 | accessMock.mockImplementation((filePath: string) => { 16 | throw new Error(`File not found at ${filePath}`); 17 | }); 18 | 19 | await expect(getRepoRoot()).rejects.toThrow('sfdx-project.json not found in any parent directory.'); 20 | 21 | // Assert recursion happened 22 | expect(accessMock).toHaveBeenCalledTimes(4); // /a/b/c, /a/b, /a, / 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/units/setCoveredLines.test.ts: -------------------------------------------------------------------------------- 1 | import { setCoveredLines } from '../../src/utils/setCoveredLines.js'; 2 | 3 | // Mock getTotalLines to simulate a short file 4 | jest.mock('../../src/utils/getTotalLines.js', () => ({ 5 | getTotalLines: jest.fn(() => Promise.resolve(3)), // Pretend file has only 3 lines 6 | })); 7 | 8 | describe('setCoveredLines unit test', () => { 9 | it('renumbers out-of-range covered lines into available unused lines', async () => { 10 | const filePath = 'some/file.cls'; 11 | const repoRoot = '/repo'; 12 | 13 | // Line 5 is out of range since getTotalLines returns 3 14 | const inputLines = { 15 | '5': 1, 16 | }; 17 | 18 | const result = await setCoveredLines(filePath, repoRoot, inputLines); 19 | 20 | expect(result).toEqual({ 21 | '1': 1, // Line 5 is remapped to first available line (1) 22 | }); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/units/typeGuards.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from '@jest/globals'; 2 | import { checkCoverageDataType } from '../../src/utils/setCoverageDataType.js'; 3 | import { DeployCoverageData } from '../../src/utils/types.js'; 4 | 5 | describe('coverage type guard unit tests', () => { 6 | it('returns Unknown when a non-object is in the test coverage array', () => { 7 | const data = [123]; // Not an object 8 | 9 | const result = checkCoverageDataType(data as unknown as DeployCoverageData); 10 | expect(result).toStrictEqual('Unknown'); 11 | }); 12 | it('test where a statementMap has a non-object value.', async () => { 13 | const invalidDeployData = { 14 | 'someFile.js': { 15 | path: 'someFile.js', 16 | fnMap: {}, 17 | branchMap: {}, 18 | f: {}, 19 | b: {}, 20 | s: {}, 21 | statementMap: { 22 | someStatement: null, 23 | }, 24 | }, 25 | }; 26 | 27 | const result = checkCoverageDataType(invalidDeployData as unknown as DeployCoverageData); 28 | expect(result).toStrictEqual('Unknown'); 29 | }); 30 | it('returns Unknown when data is not an object', () => { 31 | const result = checkCoverageDataType(42 as unknown as DeployCoverageData); // 👈 non-object input 32 | expect(result).toStrictEqual('Unknown'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /test/utils/baselineCompare.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | import { readFile } from 'node:fs/promises'; 3 | import { resolve } from 'node:path'; 4 | import { strictEqual } from 'node:assert'; 5 | 6 | import { formatOptions } from '../../src/utils/constants.js'; 7 | import { getExtensionForFormat } from '../../src/transformers/reportGenerator.js'; 8 | import { 9 | jacocoBaselinePath, 10 | lcovBaselinePath, 11 | sonarBaselinePath, 12 | cloverBaselinePath, 13 | coberturaBaselinePath, 14 | inputJsons, 15 | jsonBaselinePath, 16 | } from './testConstants.js'; 17 | import { normalizeCoverageReport } from './normalizeCoverageReport.js'; 18 | 19 | export async function compareToBaselines(): Promise { 20 | const baselineMap = { 21 | sonar: sonarBaselinePath, 22 | lcovonly: lcovBaselinePath, 23 | jacoco: jacocoBaselinePath, 24 | cobertura: coberturaBaselinePath, 25 | clover: cloverBaselinePath, 26 | json: jsonBaselinePath, 27 | } as const; 28 | 29 | const normalizationRequired = new Set(['cobertura', 'clover']); 30 | 31 | for (const format of formatOptions as Array) { 32 | for (const { label } of inputJsons) { 33 | const reportExtension = getExtensionForFormat(format); 34 | const outputPath = resolve(`${label}-${format}${reportExtension}`); 35 | const outputContent = await readFile(outputPath, 'utf-8'); 36 | const baselineContent = await readFile(baselineMap[format], 'utf-8'); 37 | 38 | if (normalizationRequired.has(format)) { 39 | strictEqual( 40 | normalizeCoverageReport(outputContent), 41 | normalizeCoverageReport(baselineContent), 42 | `Mismatch between ${outputPath} and ${baselineMap[format]}` 43 | ); 44 | } else { 45 | strictEqual(outputContent, baselineContent, `Mismatch between ${outputPath} and ${baselineMap[format]}`); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/utils/normalizeCoverageReport.ts: -------------------------------------------------------------------------------- 1 | export function normalizeCoverageReport(content: string): string { 2 | return ( 3 | content 4 | // Normalize Cobertura timestamps: timestamp="123456789" 5 | .replace(/timestamp="[\d]+"/g, 'timestamp="NORMALIZED"') 6 | // Normalize Clover timestamps: generated="1234567890123" 7 | .replace(/generated="[\d]+"/g, 'generated="NORMALIZED"') 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /test/utils/testCleanup.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | import { rm } from 'node:fs/promises'; 3 | import { resolve } from 'node:path'; 4 | 5 | import { formatOptions } from '../../src/utils/constants.js'; 6 | import { getExtensionForFormat } from '../../src/transformers/reportGenerator.js'; 7 | import { sfdxConfigFile, inputJsons, defaultPath } from './testConstants.js'; 8 | 9 | export async function postTestCleanup(): Promise { 10 | await rm(sfdxConfigFile); 11 | await rm('force-app/main/default/classes/AccountProfile.cls'); 12 | await rm('packaged/triggers/AccountTrigger.trigger'); 13 | await rm('force-app', { recursive: true }); 14 | await rm('packaged', { recursive: true }); 15 | 16 | const pathsToRemove = formatOptions 17 | .flatMap((format) => 18 | inputJsons.map(({ label }) => { 19 | const reportExtension = getExtensionForFormat(format); 20 | return resolve(`${label}-${format}${reportExtension}`); 21 | }) 22 | ) 23 | .concat(defaultPath); 24 | 25 | for (const path of pathsToRemove) { 26 | await rm(path).catch(() => {}); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /test/utils/testConstants.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path'; 2 | 3 | export const baselineClassPath = resolve('samples/classes/AccountProfile.cls'); 4 | export const baselineTriggerPath = resolve('samples/triggers/AccountTrigger.trigger'); 5 | export const deployCoverage = resolve('inputs/deploy_coverage.json'); 6 | export const testCoverage = resolve('inputs/test_coverage.json'); 7 | export const invalidJson = resolve('inputs/invalid.json'); 8 | export const sonarBaselinePath = resolve('baselines/sonar_baseline.xml'); 9 | export const jacocoBaselinePath = resolve('baselines/jacoco_baseline.xml'); 10 | export const lcovBaselinePath = resolve('baselines/lcov_baseline.info'); 11 | export const coberturaBaselinePath = resolve('baselines/cobertura_baseline.xml'); 12 | export const cloverBaselinePath = resolve('baselines/clover_baseline.xml'); 13 | export const jsonBaselinePath = resolve('baselines/json_baseline.json'); 14 | export const defaultPath = resolve('coverage.xml'); 15 | export const sfdxConfigFile = resolve('sfdx-project.json'); 16 | 17 | const configFile = { 18 | packageDirectories: [{ path: 'force-app', default: true }, { path: 'packaged' }, { path: 'samples' }], 19 | namespace: '', 20 | sfdcLoginUrl: 'https://login.salesforce.com', 21 | sourceApiVersion: '58.0', 22 | }; 23 | export const configJsonString = JSON.stringify(configFile, null, 2); 24 | export const inputJsons = [ 25 | { label: 'deploy', path: deployCoverage }, 26 | { label: 'test', path: testCoverage }, 27 | ] as const; 28 | -------------------------------------------------------------------------------- /test/utils/testSetup.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | import { mkdir, copyFile, writeFile } from 'node:fs/promises'; 3 | 4 | import { sfdxConfigFile, baselineClassPath, baselineTriggerPath, configJsonString } from './testConstants.js'; 5 | 6 | export async function preTestSetup(): Promise { 7 | await mkdir('force-app/main/default/classes', { recursive: true }); 8 | await mkdir('packaged/triggers', { recursive: true }); 9 | await copyFile(baselineClassPath, 'force-app/main/default/classes/AccountProfile.cls'); 10 | await copyFile(baselineTriggerPath, 'packaged/triggers/AccountTrigger.trigger'); 11 | await writeFile(sfdxConfigFile, configJsonString); 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@salesforce/dev-config/tsconfig-strict-esm", 3 | "compilerOptions": { 4 | "outDir": "lib", 5 | "rootDir": "src", 6 | "sourceMap": true, 7 | "types": ["jest", "chai"] 8 | }, 9 | "include": ["./src/**/*.ts"], 10 | "exclude": ["node_modules"] 11 | } 12 | --------------------------------------------------------------------------------