├── .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 ├── .mocharc.json ├── .nycrc ├── .prettierrc.json ├── .sfdevrc.json ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── bin ├── dev.cmd ├── dev.js ├── run.cmd └── run.js ├── commitlint.config.cjs ├── defaults ├── salesforce-cli │ └── .apexcodecovtransformer.config.json └── sfdx-hardis │ └── .apexcodecovtransformer.config.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 │ ├── 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 ├── clover_baseline.xml ├── cobertura_baseline.xml ├── commands │ └── acc-transformer │ │ ├── repoRoot.test.ts │ │ ├── setCoveredLines.test.ts │ │ ├── transform.nut.ts │ │ ├── transform.test.ts │ │ └── typeGuards.test.ts ├── deploy_coverage.json ├── invalid.json ├── jacoco_baseline.xml ├── lcov_baseline.info ├── sonar_baseline.xml ├── test_coverage.json ├── tsconfig.json └── 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: 18 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: 18 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: 18 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 | -------------------------------------------------------------------------------- /.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": ["ts-node/register"], 3 | "watch-extensions": "ts", 4 | "recursive": true, 5 | "reporter": "spec", 6 | "timeout": 600000, 7 | "node-option": ["loader=ts-node/esm"] 8 | } 9 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "lines": 75, 4 | "statements": 75, 5 | "functions": 75, 6 | "branches": 75 7 | } 8 | -------------------------------------------------------------------------------- /.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.11.8](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.7...v2.11.8) (2025-06-05) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * 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)) 14 | 15 | ## [2.11.7](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.6...v2.11.7) (2025-06-03) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * **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)) 21 | 22 | ## [2.11.6](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.5...v2.11.6) (2025-05-28) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * 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)) 28 | 29 | ## [2.11.5](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.4...v2.11.5) (2025-05-26) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * 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)) 35 | 36 | ## [2.11.4](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.3...v2.11.4) (2025-05-26) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * merge coverage transformer functions ([5eadaac](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/5eadaacff9e20cc72e330a3dcd44aa05dd6bf5a4)) 42 | 43 | ## [2.11.3](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.2...v2.11.3) (2025-05-22) 44 | 45 | 46 | ### Bug Fixes 47 | 48 | * improve code quality on functions ([604300b](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/604300bf8605576a6cfc8f760f88365250627d31)) 49 | * reduce complexity when setting coverage data type ([51d1f20](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/51d1f205aa8dbb6635e6b302cc0050dfd3b03b84)) 50 | 51 | ## [2.11.2](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.1...v2.11.2) (2025-05-06) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * **deps:** bump @oclif/core from 4.2.10 to 4.3.0 ([e02c2c4](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/e02c2c4e85636a1d64da1783d39db3a46b118bc2)) 57 | 58 | ## [2.11.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.11.0...v2.11.1) (2025-03-25) 59 | 60 | 61 | ### Bug Fixes 62 | 63 | * **deps:** bump @salesforce/core from 8.8.5 to 8.8.6 ([3cdbe52](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/3cdbe52bfee45d34d8d355f61451529f9fadaeb3)) 64 | * **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)) 65 | 66 | ## [2.11.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.10.0...v2.11.0) (2025-03-07) 67 | 68 | 69 | ### Features 70 | 71 | * include ignore package directories flag in hook ([a7294a4](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/a7294a47a85141d4b1d0e21854ac14d4f19b9e14)) 72 | 73 | 74 | ### Bug Fixes 75 | 76 | * **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)) 77 | 78 | ## [2.10.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.9.0...v2.10.0) (2025-02-26) 79 | 80 | 81 | ### Features 82 | 83 | * dynamically determine cobertura package names from file paths ([cbf63df](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/cbf63dfc44ec79c9ec4e58bdc68bb9458f925e4f)) 84 | 85 | 86 | ### Bug Fixes 87 | 88 | * **deps:** bump @oclif/core from 4.2.6 to 4.2.8 ([b838b3f](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/b838b3f6a47eb8fee04d6d8a1321fc82c949cfe4)) 89 | * **deps:** bump @salesforce/core from 8.8.2 to 8.8.3 ([c395e9c](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/c395e9cf8e38fdd606b594589376b9670e7f220c)) 90 | 91 | ## [2.9.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.8.7...v2.9.0) (2025-02-16) 92 | 93 | 94 | ### Features 95 | 96 | * add ignore package directories flag ([3666c59](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/3666c59bdd675c1610c2409bddbd11fe4ed03719)) 97 | 98 | ## [2.8.7](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.8.6...v2.8.7) (2025-02-13) 99 | 100 | 101 | ### Bug Fixes 102 | 103 | * **deps:** bump @oclif/core from 4.2.5 to 4.2.6 ([b1988e7](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/b1988e7975c907c94a69e580778b8ded326619ba)) 104 | * **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)) 105 | * fix jacoco source file paths ([1a0af9f](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/1a0af9fb6825e21e9b481e06faaa8bf0c8740dc2)) 106 | 107 | ## [2.8.6](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.8.5...v2.8.6) (2025-02-08) 108 | 109 | 110 | ### Bug Fixes 111 | 112 | * remove jacoco class and add custom header ([6c87aff](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/6c87aff315bac4388dbcde7a499d470604fad65e)) 113 | 114 | ## [2.8.5](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.8.4...v2.8.5) (2025-02-08) 115 | 116 | 117 | ### Bug Fixes 118 | 119 | * move class counter jacoco ([610e73d](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/610e73d49c7bdca7d7cdd120643c535994cbfe99)) 120 | 121 | ## [2.8.4](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.8.3...v2.8.4) (2025-02-08) 122 | 123 | 124 | ### Bug Fixes 125 | 126 | * jacoco structuring per package directory ([5efc698](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/5efc69853d6f5498e0a20bb3f94b3e4068b3d528)) 127 | 128 | ## [2.8.3](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.8.2...v2.8.3) (2025-02-08) 129 | 130 | 131 | ### Bug Fixes 132 | 133 | * add package counter for jacoco ([88b620a](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/88b620a27f5e12a9f30a9369e4920880d1f924da)) 134 | 135 | ## [2.8.2](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.8.1...v2.8.2) (2025-02-07) 136 | 137 | 138 | ### Bug Fixes 139 | 140 | * include file path in jacoco sourcefile ([c3df27e](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/c3df27ed3544bd7145267b3cdde617f2719bd034)) 141 | 142 | ## [2.8.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.8.0...v2.8.1) (2025-02-07) 143 | 144 | 145 | ### Bug Fixes 146 | 147 | * jacoco sourcefile structure ([0a1275f](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/0a1275f2d6ef5732c06568d183e4aa8e5def8951)) 148 | 149 | ## [2.8.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.7.2...v2.8.0) (2025-02-07) 150 | 151 | 152 | ### Features 153 | 154 | * add jacoco format ([ecd74f6](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/ecd74f6cc79663b0a0a1bae024620eb69426de60)) 155 | 156 | ## [2.7.2](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.7.1...v2.7.2) (2025-02-02) 157 | 158 | 159 | ### Bug Fixes 160 | 161 | * **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)) 162 | 163 | ## [2.7.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.7.0...v2.7.1) (2025-01-16) 164 | 165 | 166 | ### Bug Fixes 167 | 168 | * sorting on each coverage format handler ([b7b9244](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/b7b9244dddc54c1c96b88fafa81ba561c946c0ac)) 169 | 170 | ## [2.7.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.6.1...v2.7.0) (2025-01-15) 171 | 172 | 173 | ### Features 174 | 175 | * add async processing and sort files in report ([ef0c011](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/ef0c011895f378e7aedc5c6568b9b9ec0e9574bf)) 176 | 177 | ## [2.6.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.6.0...v2.6.1) (2025-01-15) 178 | 179 | 180 | ### Bug Fixes 181 | 182 | * fix default test command path in doc and provide defaults ([c8dc711](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/c8dc71177d3ed9cae8dc7a41b7843fdc9d09ad5d)) 183 | 184 | ## [2.6.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.5.2...v2.6.0) (2025-01-12) 185 | 186 | 187 | ### Features 188 | 189 | * allow hook to run after hardis commands ([962847d](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/962847d640237bd8c6c16f7319e2756b005713ea)) 190 | 191 | ## [2.5.2](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.5.1...v2.5.2) (2025-01-10) 192 | 193 | 194 | ### Bug Fixes 195 | 196 | * confirm deploy report lines are sorted before covered line adjustment ([9ffb7f5](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/9ffb7f55343c45b876006015ebc11818c63174ce)) 197 | * only set uncovered and covered lines when needed ([45f77ae](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/45f77aed39fbb1cac8329c0502cc9de8253be666)) 198 | * warn instead of fail when json is not found ([7445801](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/744580187ca9ed67964f13e80f26a207d45a62bf)) 199 | 200 | ## [2.5.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.5.0...v2.5.1) (2025-01-09) 201 | 202 | 203 | ### Bug Fixes 204 | 205 | * set covered lines before processing format ([d96bd88](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/d96bd88b341dbb8abe49ece21252e998abae617b)) 206 | 207 | ## [2.5.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.4.1...v2.5.0) (2025-01-08) 208 | 209 | 210 | ### Features 211 | 212 | * add support for lcovonly info format ([a663cb2](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/a663cb2db49e62a8c6cf278fe6a7e28c9e0a94c6)) 213 | * rename xml flag to output-report ([bb25a47](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/bb25a478e285dc7de612368c51243e0525a71b02)) 214 | 215 | ## [2.4.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.4.0...v2.4.1) (2024-12-22) 216 | 217 | 218 | ### Bug Fixes 219 | 220 | * fix clover file level metrics in deploy reports ([cddded4](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/cddded4f996771af1fafa07213f7e9866f4c1191)) 221 | 222 | ## [2.4.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.3.0...v2.4.0) (2024-12-21) 223 | 224 | 225 | ### Features 226 | 227 | * add support for clover format ([00ffa74](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/00ffa742455609028f36d48f9a4cabc86aa32ecb)) 228 | 229 | ## [2.3.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.2.1...v2.3.0) (2024-12-17) 230 | 231 | 232 | ### Features 233 | 234 | * add support for cobertura format ([41a3b25](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/41a3b2583ce1014ccd04f935c5c8b87831bee2e7)) 235 | 236 | ## [2.2.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.2.0...v2.2.1) (2024-12-14) 237 | 238 | 239 | ### Bug Fixes 240 | 241 | * upgrade all dependencies ([7addc5d](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/7addc5dc6ec72f75a26bc1306ae42eee74daea25)) 242 | 243 | ## [2.2.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.1.0...v2.2.0) (2024-10-28) 244 | 245 | 246 | ### Features 247 | 248 | * remove dependency on git repos ([c2bc72d](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/c2bc72d72de00da8b52f3f1ae0a6b8805316b20f)) 249 | 250 | ## [2.1.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v2.0.0...v2.1.0) (2024-10-22) 251 | 252 | 253 | ### Features 254 | 255 | * remove command flag by adding type guard functions ([be380bf](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/be380bf2400cb217a3769d6272668d6ba277a6a4)) 256 | 257 | ## [2.0.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.7.6...v2.0.0) (2024-10-21) 258 | 259 | 260 | ### ⚠ BREAKING CHANGES 261 | 262 | * shorten command to acc-transformer 263 | 264 | ### Features 265 | 266 | * shorten command to acc-transformer ([686cdc6](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/686cdc6960f8f73e7796e6b97928a8294a6b450b)) 267 | 268 | ## [1.7.6](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.7.5...v1.7.6) (2024-07-28) 269 | 270 | 271 | ### Bug Fixes 272 | 273 | * refactor fs import ([d246c28](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/d246c28f70f5cd2ac40110b2d376ad0bac59384d)) 274 | 275 | ## [1.7.5](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.7.4...v1.7.5) (2024-07-28) 276 | 277 | 278 | ### Bug Fixes 279 | 280 | * switch to isomorphic-git ([2da1fcc](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/2da1fcc2f55cf76296d1b7cd033daedba1bf496d)) 281 | 282 | ## [1.7.4](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.7.3...v1.7.4) (2024-06-10) 283 | 284 | ### Bug Fixes 285 | 286 | - include `apex get test` command in the hook ([34718f0](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/34718f0c146dbf51efe04c81d994c0c724e84a0c)) 287 | - update the hook to allow 2 different JSON paths based on command ([f6159be](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/f6159be3fe12801a00c06f44a5304cab2d02697e)) 288 | 289 | ## [1.7.3](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.7.2...v1.7.3) (2024-05-15) 290 | 291 | ### Bug Fixes 292 | 293 | - fix handling of an empty JSON from the test run command ([48ef6df](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/48ef6df77eda762006a78e1d2eb66936d57ee418)) 294 | - warn instead of fail when no files in the JSON are processed ([7809843](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/780984332c279ed7142695cbf262e67f69e916c0)) 295 | 296 | ## [1.7.2](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.7.1...v1.7.2) (2024-05-10) 297 | 298 | ### Bug Fixes 299 | 300 | - add support for coverage JSONs created by `sf apex run test` ([5f48b77](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/5f48b777f1ccd003d650c50ef87a0b24e2b4a73f)) 301 | 302 | ## [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) 303 | 304 | ### Bug Fixes 305 | 306 | - add support for coverage JSONs created by `sf apex run test` ([5f48b77](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/5f48b777f1ccd003d650c50ef87a0b24e2b4a73f)) 307 | 308 | ## [1.7.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.7.0...v1.7.1) (2024-04-30) 309 | 310 | ### Bug Fixes 311 | 312 | - fix no-map replacement for windows-style paths ([e6d4fef](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/e6d4fef16266a8075c01601b3f5a838a058c5fa2)) 313 | 314 | # [1.7.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.8...v1.7.0) (2024-04-30) 315 | 316 | ### Features 317 | 318 | - add a post run hook for post deployments ([894a4cd](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/894a4cdb14f6367b3e9248e757b1463e3134ae83)) 319 | 320 | ## [1.6.8](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.7...v1.6.8) (2024-04-23) 321 | 322 | ### Bug Fixes 323 | 324 | - get package directories and repo root before looping through files ([b630002](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/b6300020d650e02928c3b5700b05aa0c4bda050e)) 325 | 326 | ## [1.6.7](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.6...v1.6.7) (2024-04-23) 327 | 328 | ### Bug Fixes 329 | 330 | - remove `--sfdx-configuration` flag and get `sfdx-project.json` path using `simple-git` ([87d92f3](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/87d92f39fdd4f404887dc2c931940ea3221d7606)) 331 | 332 | ## [1.6.6](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.5...v1.6.6) (2024-04-22) 333 | 334 | ### Bug Fixes 335 | 336 | - fix path resolution when running in non-root directories ([c16fe7d](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/c16fe7d09898b857c68c2cc73a1ac2bcc8665f1e)) 337 | 338 | ## [1.6.5](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.4...v1.6.5) (2024-04-18) 339 | 340 | ### Bug Fixes 341 | 342 | - build XML using xmlbuilder2 and normalize path building using posix ([c6d6d94](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/c6d6d944e2675eb6be0dd90e972d67e6311f6738)) 343 | 344 | ## [1.6.4](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.3...v1.6.4) (2024-04-09) 345 | 346 | ### Bug Fixes 347 | 348 | - switch to promises/async and refactor imports ([2577692](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/2577692672aeb9271151f67e8347ef8a09a07b37)) 349 | 350 | ## [1.6.3](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.2...v1.6.3) (2024-03-27) 351 | 352 | ### Bug Fixes 353 | 354 | - remove flow logic ([dd3db2a](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/dd3db2ab60614dd21bdc844b78c8e882814c43a8)) 355 | 356 | ## [1.6.2](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.1...v1.6.2) (2024-03-26) 357 | 358 | ### Bug Fixes 359 | 360 | - 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)) 361 | 362 | ## [1.6.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.6.0...v1.6.1) (2024-03-22) 363 | 364 | ### Bug Fixes 365 | 366 | - search the directories recursively without hard-coding the sub-folder names ([8880ab3](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/8880ab3e3c9097b8e7927230395eae32560ae55a)) 367 | 368 | ## [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) 369 | 370 | ### Bug Fixes 371 | 372 | - search the directories recursively without hard-coding the sub-folder names ([8880ab3](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/8880ab3e3c9097b8e7927230395eae32560ae55a)) 373 | 374 | # [1.6.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.5.0...v1.6.0) (2024-03-21) 375 | 376 | ### Features 377 | 378 | - add `covered` lines, renumbering out-of-range lines numbers ([1733b09](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/1733b09063eac28f0e627e66080dcd24d7c74bf9)) 379 | 380 | # [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) 381 | 382 | ### Features 383 | 384 | - add `covered` lines, renumbering out-of-range lines numbers ([1733b09](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/1733b09063eac28f0e627e66080dcd24d7c74bf9)) 385 | 386 | # [1.5.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.4.0...v1.5.0) (2024-03-20) 387 | 388 | ### Features 389 | 390 | - support multiple package directories via the sfdx-project.json ([52c1a12](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/52c1a12ff5fbfb10c215a41e010ee7fc6c0370de)) 391 | 392 | # [1.4.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.3.1...v1.4.0) (2024-02-27) 393 | 394 | ### Features 395 | 396 | - if coverage JSON includes file extensions, use that to determine paths ([efc1fa6](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/efc1fa61ce21cff394bbc696afce88c4d57894ea)) 397 | 398 | ## [1.3.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.3.0...v1.3.1) (2024-02-26) 399 | 400 | ### Bug Fixes 401 | 402 | - dx-directory should be an existing directory and fix flag in messages ([38fb20b](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/38fb20b8a107c203ba78266cb05d133805135ce4)) 403 | 404 | # [1.3.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.2.0...v1.3.0) (2024-02-07) 405 | 406 | ### Features 407 | 408 | - add support for flows ([6bf0da1](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/6bf0da14a39871dc3b7d50565416c2d24fba7524)) 409 | 410 | # [1.2.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.1.1...v1.2.0) (2024-02-07) 411 | 412 | ### Features 413 | 414 | - 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)) 415 | 416 | ## [1.1.1](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.1.0...v1.1.1) (2024-02-07) 417 | 418 | ### Bug Fixes 419 | 420 | - resolve path to xml ([cc75e96](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/cc75e96ef26120f86cff8588256e4f55e79d5473)) 421 | 422 | # [1.1.0](https://github.com/mcarvin8/apex-code-coverage-transformer/compare/v1.0.0...v1.1.0) (2024-02-07) 423 | 424 | ### Features 425 | 426 | - 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)) 427 | 428 | # 1.0.0 (2024-02-07) 429 | 430 | ### Features 431 | 432 | - init release ([504b4cf](https://github.com/mcarvin8/apex-code-coverage-transformer/commit/504b4cfb028fc14241b892e1cc872adadec736d7)) 433 | -------------------------------------------------------------------------------- /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 >= 18.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 tests for new additions. 33 | 34 | ```bash 35 | # run unit tests 36 | yarn test 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 file in `src/handlers` with a `constructor`, `processFile` and `finalize` class. 61 | 1. The `finalize` class should sort items in the coverage object before returning. 62 | 4. Add new coverage handler class to `src/handlers/getHandler.ts`. 63 | 5. Add new `{format}CoverageObject` type to `src/transformers/reportGenerator.ts` and add anything needed to create the final report for that format. 64 | 6. Add new unit and non-unit tests for new format to `test/commands/acc-transformer`. 65 | 1. 1 new test should transform the deploy command coverage JSON (`test/deploy_coverage.json`) into the new format 66 | 2. 1 new test should transform the test command coverage JSON (`test/test_coverage.json`) into the new format 67 | 3. A new baseline report for the new format should be added as `test/{format}_baseline.{ext}` 68 | 4. The existing baseline compare test should be updated to compare `test/{format}_baseline.{ext}` to the 2 reports created in the 2 new tests. Update and use the `test/commands/acc-transformer/normalizeCoverageReport.ts` to remove timestamps if the new format report has timestamps, i.e. Cobertura and Clover. 69 | -------------------------------------------------------------------------------- /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 | [default: "sonar"] 103 | -i, --ignore-package-directory= Package directory to ignore when looking for matching files in the coverage report. 104 | Should be as they appear in the "sfdx-project.json". 105 | Can be declared multiple times. 106 | 107 | GLOBAL FLAGS 108 | --json Format output as json. 109 | 110 | EXAMPLES 111 | Transform the JSON into Sonar format: 112 | 113 | $ sf acc-transformer transform -j "coverage.json" -r "coverage.xml" -f "sonar" 114 | 115 | Transform the JSON into Cobertura format: 116 | 117 | $ sf acc-transformer transform -j "coverage.json" -r "coverage.xml" -f "cobertura" 118 | 119 | Transform the JSON into Clover format: 120 | 121 | $ sf acc-transformer transform -j "coverage.json" -r "coverage.xml" -f "clover" 122 | 123 | Transform the JSON into LCovOnly format: 124 | 125 | $ sf acc-transformer transform -j "coverage.json" -r "coverage.info" -f "lcovonly" 126 | 127 | Transform the JSON into Sonar format, ignoring Apex in the "force-app" directory: 128 | 129 | $ sf acc-transformer transform -j "coverage.json" -i "force-app" 130 | ``` 131 | 132 | ## Coverage Report Formats 133 | 134 | The `-f`/`--format` flag allows you to specify the format of the transformed coverage report. 135 | 136 | | Flag Option | Description | 137 | | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | 138 | | [sonar](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/test/sonar_baseline.xml) | Generates a SonarQube-compatible coverage report. This is the default option. | 139 | | [clover](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/test/clover_baseline.xml) | Produces a Clover XML report format, commonly used with Atlassian tools. | 140 | | [lcovonly](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/test/lcov_baseline.info) | Outputs coverage data in LCOV format, useful for integrating with LCOV-based tools. | 141 | | [cobertura](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/test/cobertura_baseline.xml) | Creates a Cobertura XML report, a widely used format for coverage reporting. | 142 | | [jacoco](https://raw.githubusercontent.com/mcarvin8/apex-code-coverage-transformer/main/test/jacoco_baseline.xml) | Creates a JaCoCo XML report, the standard for Java projects. | 143 | 144 | ## Hook 145 | 146 | To enable automatic transformation after the below `sf` commands complete, create a `.apexcodecovtransformer.config.json` in your project’s root directory. 147 | 148 | - `sf project deploy [start/validate/report/resume]` 149 | - `sf apex run test` 150 | - `sf apex get test` 151 | - `sf hardis project deploy smart` 152 | - only if `sfdx-hardis` is installed 153 | - `COVERAGE_FORMATTER_JSON=true` must be set in the environment variables 154 | - `sf hardis org test apex` 155 | - only if `sfdx-hardis` is installed 156 | 157 | 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". 158 | 159 | 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. 160 | 161 | **`.apexcodecovtransformer.config.json` structure** 162 | 163 | | JSON Key | Required | Description | 164 | | -------------------------- | -------------------------------------- | --------------------------------------------------------------------------------------------- | 165 | | `deployCoverageJsonPath` | Yes (for deploy command) | Code coverage JSON created by the Salesforce CLI deploy commands. | 166 | | `testCoverageJsonPath` | Yes (for test command) | Code coverage JSON created by the Salesforce CLI test commands. | 167 | | `outputReportPath` | No (defaults to `coverage.[xml/info]`) | Transformed code coverage report path. | 168 | | `format` | No (defaults to `sonar`) | Transformed code coverage report [format](#coverage-report-formats). | 169 | | `ignorePackageDirectories` | No | Comma-separated string of package directories to ignore when looking for matching Apex files. | 170 | 171 | ## Troubleshooting 172 | 173 | 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: 174 | 175 | ``` 176 | Warning: The file name AccountTrigger was not found in any package directory. 177 | ``` 178 | 179 | 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: 180 | 181 | ``` 182 | Warning: The file name AccountTrigger was not found in any package directory. 183 | Warning: The file name AccountProfile was not found in any package directory. 184 | Warning: None of the files listed in the coverage JSON were processed. The coverage report will be empty. 185 | ``` 186 | 187 | 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: 188 | 189 | ``` 190 | Error (1): The provided JSON does not match a known coverage data format from the Salesforce deploy or test command. 191 | ``` 192 | 193 | If `sfdx-project.json` file is missing from the project root, the plugin will fail with: 194 | 195 | ``` 196 | Error (1): sfdx-project.json not found in any parent directory. 197 | ``` 198 | 199 | If a package directory listed in `sfdx-project.json` cannot be found, the plugin will encounter a **ENOENT** error: 200 | 201 | ``` 202 | Error (1): ENOENT: no such file or directory: {packageDir} 203 | ``` 204 | 205 | ## Issues 206 | 207 | If you encounter any issues or would like to suggest features, please create an [issue](https://github.com/mcarvin8/apex-code-coverage-transformer/issues). 208 | 209 | ## Contributing 210 | 211 | Contributions are welcome! See [Contributing](https://github.com/mcarvin8/apex-code-coverage-transformer/blob/main/CONTRIBUTING.md). 212 | 213 | ## License 214 | 215 | 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. 216 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 created during deployments and test runs into other formats accepted by SonarQube, GitHub, GitLab, Azure, Bitbucket, etc.", 4 | "version": "2.11.8", 5 | "dependencies": { 6 | "@oclif/core": "^4.3.0", 7 | "@salesforce/core": "^8.11.4", 8 | "@salesforce/sf-plugins-core": "^12.2.1", 9 | "async": "^3.2.6", 10 | "xmlbuilder2": "^3.1.1" 11 | }, 12 | "devDependencies": { 13 | "@commitlint/cli": "^19.8.0", 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.21", 22 | "husky": "^9.1.7", 23 | "jest": "^29.7.0", 24 | "oclif": "^4.17.46", 25 | "shx": "0.4.0", 26 | "ts-jest": "^29.3.4", 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": ">=18.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 | ], 72 | "license": "MIT", 73 | "oclif": { 74 | "commands": "./lib/commands", 75 | "bin": "sf", 76 | "topicSeparator": " ", 77 | "topics": { 78 | "acc-transformer": { 79 | "description": "description for acc-transformer" 80 | } 81 | }, 82 | "hooks": { 83 | "postrun": "./lib/hooks/postrun" 84 | }, 85 | "devPlugins": [ 86 | "@oclif/plugin-help" 87 | ], 88 | "flexibleTaxonomy": true 89 | }, 90 | "scripts": { 91 | "command-docs": "oclif readme", 92 | "build": "tsc -b", 93 | "clean": "sf-clean", 94 | "clean-all": "sf-clean all", 95 | "clean:lib": "shx rm -rf lib && shx rm -rf coverage && shx rm -rf .nyc_output && shx rm -f oclif.manifest.json oclif.lock", 96 | "compile": "wireit", 97 | "docs": "sf-docs", 98 | "format": "sf-format", 99 | "lint": "wireit", 100 | "postpack": "shx rm -f oclif.manifest.json oclif.lock", 101 | "prepack": "sf-prepack", 102 | "prepare": "husky install", 103 | "test": "wireit", 104 | "test:nuts": "oclif manifest && jest --testMatch \"**/*.nut.ts\"", 105 | "test:only": "wireit", 106 | "version": "oclif readme" 107 | }, 108 | "publishConfig": { 109 | "access": "public" 110 | }, 111 | "wireit": { 112 | "build": { 113 | "dependencies": [ 114 | "compile", 115 | "lint" 116 | ] 117 | }, 118 | "compile": { 119 | "command": "tsc -p . --pretty --incremental", 120 | "files": [ 121 | "src/**/*.ts", 122 | "**/tsconfig.json", 123 | "messages/**" 124 | ], 125 | "output": [ 126 | "lib/**", 127 | "*.tsbuildinfo" 128 | ], 129 | "clean": "if-file-deleted" 130 | }, 131 | "format": { 132 | "command": "prettier --write \"+(src|test|schemas)/**/*.+(ts|js|json)|command-snapshot.json\"", 133 | "files": [ 134 | "src/**/*.ts", 135 | "test/**/*.ts", 136 | "schemas/**/*.json", 137 | "command-snapshot.json", 138 | ".prettier*" 139 | ], 140 | "output": [] 141 | }, 142 | "lint": { 143 | "command": "eslint src test --color --cache --cache-location .eslintcache", 144 | "files": [ 145 | "src/**/*.ts", 146 | "test/**/*.ts", 147 | "messages/**", 148 | "**/.eslint*", 149 | "**/tsconfig.json" 150 | ], 151 | "output": [] 152 | }, 153 | "test:compile": { 154 | "command": "tsc -p \"./test\" --pretty", 155 | "files": [ 156 | "test/**/*.ts", 157 | "**/tsconfig.json" 158 | ], 159 | "output": [] 160 | }, 161 | "test": { 162 | "dependencies": [ 163 | "test:compile", 164 | "test:only", 165 | "lint" 166 | ] 167 | }, 168 | "test:only": { 169 | "command": "jest --coverage", 170 | "env": { 171 | "FORCE_COLOR": "2" 172 | }, 173 | "files": [ 174 | "test/**/*.ts", 175 | "src/**/*.ts", 176 | "**/tsconfig.json", 177 | ".mocha*", 178 | "!*.nut.ts", 179 | ".nycrc" 180 | ], 181 | "output": [] 182 | }, 183 | "test:command-reference": { 184 | "command": "\"./bin/dev\" commandreference:generate --erroronwarnings", 185 | "files": [ 186 | "src/**/*.ts", 187 | "messages/**", 188 | "package.json" 189 | ], 190 | "output": [ 191 | "tmp/root" 192 | ] 193 | }, 194 | "test:deprecation-policy": { 195 | "command": "\"./bin/dev\" snapshot:compare", 196 | "files": [ 197 | "src/**/*.ts" 198 | ], 199 | "output": [], 200 | "dependencies": [ 201 | "compile" 202 | ] 203 | }, 204 | "test:json-schema": { 205 | "command": "\"./bin/dev\" schema:compare", 206 | "files": [ 207 | "src/**/*.ts", 208 | "schemas" 209 | ], 210 | "output": [] 211 | } 212 | }, 213 | "exports": "./lib/index.js", 214 | "type": "module", 215 | "author": "Matt Carvin", 216 | "repository": { 217 | "type": "git", 218 | "url": "git+https://github.com/mcarvin8/apex-code-coverage-transformer.git" 219 | }, 220 | "bugs": { 221 | "url": "https://github.com/mcarvin8/apex-code-coverage-transformer/issues" 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /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: true, 33 | multiple: false, 34 | default: 'sonar', 35 | options: formatOptions, 36 | }), 37 | 'ignore-package-directory': Flags.directory({ 38 | summary: messages.getMessage('flags.ignore-package-directory.summary'), 39 | char: 'i', 40 | required: false, 41 | multiple: true, 42 | }), 43 | }; 44 | 45 | public async run(): Promise { 46 | const { flags } = await this.parse(TransformerTransform); 47 | const warnings: string[] = []; 48 | 49 | const result = await transformCoverageReport( 50 | flags['coverage-json'], 51 | flags['output-report'], 52 | flags['format'], 53 | flags['ignore-package-directory'] ?? [] 54 | ); 55 | warnings.push(...result.warnings); 56 | const finalPath = result.finalPath; 57 | 58 | if (warnings.length > 0) { 59 | warnings.forEach((warning) => { 60 | this.warn(warning); 61 | }); 62 | } 63 | 64 | this.log(`The coverage report has been written to ${finalPath}`); 65 | return { path: finalPath }; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /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 | 10 | export function getCoverageHandler(format: string): CoverageHandler { 11 | const handlers: Record = { 12 | sonar: new SonarCoverageHandler(), 13 | cobertura: new CoberturaCoverageHandler(), 14 | clover: new CloverCoverageHandler(), 15 | lcovonly: new LcovCoverageHandler(), 16 | jacoco: new JaCoCoCoverageHandler(), 17 | }; 18 | 19 | const handler = handlers[format]; 20 | if (!handler) { 21 | throw new Error(`Unsupported format: ${format}`); 22 | } 23 | return handler; 24 | } 25 | -------------------------------------------------------------------------------- /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 | commandArgs.push('--format'); 71 | commandArgs.push(coverageFormat); 72 | if (ignorePackageDirs.trim() !== '') { 73 | const ignorePackageDirArray: string[] = ignorePackageDirs.split(','); 74 | for (const dirs of ignorePackageDirArray) { 75 | const sanitizedDir = dirs.replace(/,/g, ''); 76 | commandArgs.push('--ignore-package-directory'); 77 | commandArgs.push(sanitizedDir); 78 | } 79 | } 80 | await TransformerTransform.run(commandArgs); 81 | }; 82 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /src/transformers/coverageTransformer.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'node:fs/promises'; 2 | import { mapLimit } from 'async'; 3 | 4 | import { getCoverageHandler } from '../handlers/getHandler.js'; 5 | import { DeployCoverageData, TestCoverageData } from '../utils/types.js'; 6 | import { getPackageDirectories } from '../utils/getPackageDirectories.js'; 7 | import { findFilePath } from '../utils/findFilePath.js'; 8 | import { setCoveredLines } from '../utils/setCoveredLines.js'; 9 | import { getConcurrencyThreshold } from '../utils/getConcurrencyThreshold.js'; 10 | import { checkCoverageDataType } from '../utils/setCoverageDataType.js'; 11 | import { generateAndWriteReport } from './reportGenerator.js'; 12 | 13 | type CoverageInput = DeployCoverageData | TestCoverageData[]; 14 | 15 | export async function transformCoverageReport( 16 | jsonFilePath: string, 17 | outputReportPath: string, 18 | format: string, 19 | ignoreDirs: string[] 20 | ): Promise<{ finalPath: string; warnings: string[] }> { 21 | const warnings: string[] = []; 22 | let filesProcessed = 0; 23 | let jsonData: string; 24 | try { 25 | jsonData = await readFile(jsonFilePath, 'utf-8'); 26 | } catch (error) { 27 | warnings.push(`Failed to read ${jsonFilePath}. Confirm file exists.`); 28 | return { finalPath: outputReportPath, warnings }; 29 | } 30 | 31 | const parsedData = JSON.parse(jsonData) as CoverageInput; 32 | 33 | const { repoRoot, packageDirectories } = await getPackageDirectories(ignoreDirs); 34 | const handler = getCoverageHandler(format); 35 | const commandType = checkCoverageDataType(parsedData); 36 | 37 | const concurrencyLimit = getConcurrencyThreshold(); 38 | 39 | if (commandType === 'DeployCoverageData') { 40 | await mapLimit(Object.keys(parsedData as DeployCoverageData), concurrencyLimit, async (fileName: string) => { 41 | const fileInfo = (parsedData as DeployCoverageData)[fileName]; 42 | const formattedFileName = fileName.replace(/no-map[\\/]+/, ''); 43 | const relativeFilePath = await findFilePath(formattedFileName, packageDirectories, repoRoot); 44 | 45 | if (!relativeFilePath) { 46 | warnings.push(`The file name ${formattedFileName} was not found in any package directory.`); 47 | return; 48 | } 49 | 50 | const updatedLines = await setCoveredLines(relativeFilePath, repoRoot, fileInfo.s); 51 | fileInfo.s = updatedLines; 52 | 53 | handler.processFile(relativeFilePath, formattedFileName, fileInfo.s); 54 | filesProcessed++; 55 | }); 56 | } else if (commandType === 'TestCoverageData') { 57 | await mapLimit(parsedData as TestCoverageData[], concurrencyLimit, async (entry: TestCoverageData) => { 58 | const name = entry?.name; 59 | const lines = entry?.lines; 60 | 61 | const formattedFileName = name.replace(/no-map[\\/]+/, ''); 62 | const relativeFilePath = await findFilePath(formattedFileName, packageDirectories, repoRoot); 63 | 64 | if (!relativeFilePath) { 65 | warnings.push(`The file name ${formattedFileName} was not found in any package directory.`); 66 | return; 67 | } 68 | 69 | handler.processFile(relativeFilePath, formattedFileName, lines); 70 | filesProcessed++; 71 | }); 72 | } else { 73 | throw new Error( 74 | 'The provided JSON does not match a known coverage data format from the Salesforce deploy or test command.' 75 | ); 76 | } 77 | 78 | if (filesProcessed === 0) { 79 | warnings.push('None of the files listed in the coverage JSON were processed. The coverage report will be empty.'); 80 | } 81 | 82 | const coverageObj = handler.finalize(); 83 | const finalPath = await generateAndWriteReport(outputReportPath, coverageObj, format); 84 | return { finalPath, warnings }; 85 | } 86 | -------------------------------------------------------------------------------- /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 | } from '../utils/types.js'; 12 | 13 | export async function generateAndWriteReport( 14 | outputPath: string, 15 | coverageObj: 16 | | SonarCoverageObject 17 | | CoberturaCoverageObject 18 | | CloverCoverageObject 19 | | LcovCoverageObject 20 | | JaCoCoCoverageObject, 21 | format: string 22 | ): Promise { 23 | const content = generateReportContent(coverageObj, format); 24 | const extension = getExtensionForFormat(format); 25 | 26 | const base = basename(outputPath, extname(outputPath)); // remove existing extension 27 | const dir = dirname(outputPath); 28 | const filePath = join(dir, `${base}${extension}`); 29 | 30 | await writeFile(filePath, content, 'utf-8'); 31 | return filePath; 32 | } 33 | 34 | function generateReportContent( 35 | coverageObj: 36 | | SonarCoverageObject 37 | | CoberturaCoverageObject 38 | | CloverCoverageObject 39 | | LcovCoverageObject 40 | | JaCoCoCoverageObject, 41 | format: string 42 | ): string { 43 | if (format === 'lcovonly' && 'files' in coverageObj) { 44 | return generateLcov(coverageObj); 45 | } 46 | 47 | const isHeadless = ['cobertura', 'clover', 'jacoco'].includes(format); 48 | const xml = create(coverageObj).end({ prettyPrint: true, indent: ' ', headless: isHeadless }); 49 | 50 | return prependXmlHeader(xml, format); 51 | } 52 | 53 | function generateLcov(coverageObj: LcovCoverageObject): string { 54 | return coverageObj.files 55 | .map((file) => { 56 | const lineData = file.lines.map((line) => `DA:${line.lineNumber},${line.hitCount}`).join('\n'); 57 | return [ 58 | 'TN:', 59 | `SF:${file.sourceFile}`, 60 | 'FNF:0', 61 | 'FNH:0', 62 | lineData, 63 | `LF:${file.totalLines}`, 64 | `LH:${file.coveredLines}`, 65 | 'BRF:0', 66 | 'BRH:0', 67 | 'end_of_record', 68 | ].join('\n'); 69 | }) 70 | .join('\n'); 71 | } 72 | 73 | function prependXmlHeader(xml: string, format: string): string { 74 | switch (format) { 75 | case 'cobertura': 76 | return `\n\n${xml}`; 77 | case 'clover': 78 | return `\n${xml}`; 79 | case 'jacoco': 80 | return `\n\n${xml}`; 81 | default: 82 | return xml; 83 | } 84 | } 85 | 86 | function getExtensionForFormat(format: string): string { 87 | if (format === 'lcovonly') { 88 | return '.info'; 89 | } 90 | 91 | return '.xml'; 92 | } 93 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const formatOptions: string[] = ['sonar', 'cobertura', 'clover', 'lcovonly', 'jacoco']; 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 | 3 | export type TransformerTransformResult = { 4 | path: string; 5 | }; 6 | 7 | export type DeployCoverageData = { 8 | [className: string]: { 9 | fnMap: Record; 10 | branchMap: Record; 11 | path: string; 12 | f: Record; 13 | b: Record; 14 | s: Record; 15 | statementMap: Record< 16 | string, 17 | { 18 | start: { line: number; column: number }; 19 | end: { line: number; column: number }; 20 | } 21 | >; 22 | }; 23 | }; 24 | 25 | export type TestCoverageData = { 26 | id: string; 27 | name: string; 28 | totalLines: number; 29 | lines: Record; 30 | totalCovered: number; 31 | coveredPercent: number; 32 | }; 33 | 34 | export type SfdxProject = { 35 | packageDirectories: Array<{ path: string }>; 36 | }; 37 | 38 | type SonarLine = { 39 | '@lineNumber': number; 40 | '@covered': string; 41 | }; 42 | 43 | export type SonarClass = { 44 | '@path': string; 45 | lineToCover: SonarLine[]; 46 | }; 47 | 48 | export type SonarCoverageObject = { 49 | coverage: { 50 | file: SonarClass[]; 51 | '@version': string; 52 | }; 53 | }; 54 | 55 | export type HookFile = { 56 | deployCoverageJsonPath: string; 57 | testCoverageJsonPath: string; 58 | outputReportPath: string; 59 | format: string; 60 | ignorePackageDirectories: string; 61 | }; 62 | 63 | export type CoberturaLine = { 64 | '@number': number; 65 | '@hits': number; 66 | '@branch': string; 67 | }; 68 | 69 | export type CoberturaClass = { 70 | '@name': string; 71 | '@filename': string; 72 | '@line-rate': string; 73 | '@branch-rate': string; 74 | methods: Record; 75 | lines: { 76 | line: CoberturaLine[]; 77 | }; 78 | }; 79 | 80 | export type CoberturaPackage = { 81 | '@name': string; 82 | '@line-rate': number; 83 | '@branch-rate': number; 84 | classes: { 85 | class: CoberturaClass[]; 86 | }; 87 | }; 88 | 89 | export type CoberturaCoverageObject = { 90 | coverage: { 91 | '@lines-valid': number; 92 | '@lines-covered': number; 93 | '@line-rate': number; 94 | '@branches-valid': number; 95 | '@branches-covered': number; 96 | '@branch-rate': number | string; 97 | '@timestamp': number; 98 | '@complexity': number; 99 | '@version': string; 100 | sources: { 101 | source: string[]; 102 | }; 103 | packages: { 104 | package: CoberturaPackage[]; 105 | }; 106 | }; 107 | }; 108 | 109 | export type CloverLine = { 110 | '@num': number; 111 | '@count': number; 112 | '@type': string; 113 | }; 114 | 115 | export type CloverFile = { 116 | '@name': string; 117 | '@path': string; 118 | metrics: { 119 | '@statements': number; 120 | '@coveredstatements': number; 121 | '@conditionals': number; 122 | '@coveredconditionals': number; 123 | '@methods': number; 124 | '@coveredmethods': number; 125 | }; 126 | line: CloverLine[]; 127 | }; 128 | 129 | type CloverProjectMetrics = { 130 | '@statements': number; 131 | '@coveredstatements': number; 132 | '@conditionals': number; 133 | '@coveredconditionals': number; 134 | '@methods': number; 135 | '@coveredmethods': number; 136 | '@elements': number; 137 | '@coveredelements': number; 138 | '@complexity': number; 139 | '@loc': number; 140 | '@ncloc': number; 141 | '@packages': number; 142 | '@files': number; 143 | '@classes': number; 144 | }; 145 | 146 | type CloverProject = { 147 | '@timestamp': number; 148 | '@name': string; 149 | metrics: CloverProjectMetrics; 150 | file: CloverFile[]; 151 | }; 152 | 153 | export type CloverCoverageObject = { 154 | coverage: { 155 | '@generated': number; 156 | '@clover': string; 157 | project: CloverProject; 158 | }; 159 | }; 160 | 161 | export type CoverageHandler = { 162 | processFile(filePath: string, fileName: string, lines: Record): void; 163 | finalize(): 164 | | SonarCoverageObject 165 | | CoberturaCoverageObject 166 | | CloverCoverageObject 167 | | LcovCoverageObject 168 | | JaCoCoCoverageObject; 169 | }; 170 | 171 | type LcovLine = { 172 | lineNumber: number; 173 | hitCount: number; 174 | }; 175 | 176 | export type LcovFile = { 177 | sourceFile: string; 178 | lines: LcovLine[]; 179 | totalLines: number; 180 | coveredLines: number; 181 | }; 182 | 183 | export type LcovCoverageObject = { 184 | files: LcovFile[]; 185 | }; 186 | 187 | export type JaCoCoCoverageObject = { 188 | report: { 189 | '@name': string; 190 | package: JaCoCoPackage[]; 191 | counter: JaCoCoCounter[]; 192 | }; 193 | }; 194 | 195 | export type JaCoCoPackage = { 196 | '@name': string; 197 | sourcefile: JaCoCoSourceFile[]; 198 | counter: JaCoCoCounter[]; 199 | }; 200 | 201 | export type JaCoCoSourceFile = { 202 | '@name': string; 203 | line: JaCoCoLine[]; 204 | counter: JaCoCoCounter[]; 205 | }; 206 | 207 | export type JaCoCoLine = { 208 | '@nr': number; // Line number 209 | '@mi': number; // Missed (0 = not covered, 1 = covered) 210 | '@ci': number; // Covered (1 = covered, 0 = missed) 211 | '@mb'?: number; // Missed Branch (optional, can be adjusted if needed) 212 | '@cb'?: number; // Covered Branch (optional, can be adjusted if needed) 213 | }; 214 | 215 | export type JaCoCoCounter = { 216 | '@type': 'INSTRUCTION' | 'BRANCH' | 'LINE' | 'METHOD' | 'CLASS' | 'PACKAGE'; 217 | '@missed': number; 218 | '@covered': number; 219 | }; 220 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /test/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 | -------------------------------------------------------------------------------- /test/commands/acc-transformer/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', () => { 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/commands/acc-transformer/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', () => { 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/commands/acc-transformer/transform.nut.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | 'use strict'; 3 | 4 | import { resolve } from 'node:path'; 5 | import { describe, it } from '@jest/globals'; 6 | 7 | import { execCmd, TestSession } from '@salesforce/cli-plugins-testkit'; 8 | import { expect } from 'chai'; 9 | import { formatOptions } from '../../../src/utils/constants.js'; 10 | import { inputJsons, invalidJson } from '../../utils/testConstants.js'; 11 | import { compareToBaselines } from '../../utils/baselineCompare.js'; 12 | import { postTestCleanup } from '../../utils/testCleanup.js'; 13 | import { preTestSetup } from '../../utils/testSetup.js'; 14 | 15 | describe('acc-transformer transform NUTs', () => { 16 | let session: TestSession; 17 | 18 | beforeAll(async () => { 19 | session = await TestSession.create({ devhubAuthStrategy: 'NONE' }); 20 | await preTestSetup(); 21 | }); 22 | 23 | afterAll(async () => { 24 | await session?.clean(); 25 | await postTestCleanup(); 26 | }); 27 | 28 | formatOptions.forEach((format) => { 29 | inputJsons.forEach(({ label, path }) => { 30 | const reportExtension = format === 'lcovonly' ? 'info' : 'xml'; 31 | const reportPath = resolve(`${format}_${label}.${reportExtension}`); 32 | it(`transforms the ${label} command JSON file into ${format} format`, async () => { 33 | const command = `acc-transformer transform --coverage-json "${path}" --output-report "${reportPath}" --format ${format} -i "samples"`; 34 | const output = execCmd(command, { ensureExitCode: 0 }).shellOutput.stdout; 35 | 36 | expect(output.replace('\n', '')).to.equal(`The coverage report has been written to ${reportPath}`); 37 | }); 38 | }); 39 | }); 40 | 41 | it('confirm the reports created are the same as the baselines.', async () => { 42 | await compareToBaselines(); 43 | }); 44 | 45 | it('confirms a failure on an invalid JSON file.', async () => { 46 | const command = `acc-transformer transform --coverage-json "${invalidJson}"`; 47 | const error = execCmd(command, { ensureExitCode: 1 }).shellOutput.stderr; 48 | 49 | expect(error.replace('\n', '')).to.contain( 50 | 'The provided JSON does not match a known coverage data format from the Salesforce deploy or test command.' 51 | ); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/commands/acc-transformer/transform.test.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-await-in-loop */ 2 | 'use strict'; 3 | import { resolve } from 'node:path'; 4 | import { describe, it } from '@jest/globals'; 5 | 6 | import { TestContext } from '@salesforce/core/testSetup'; 7 | import { expect } from 'chai'; 8 | import { transformCoverageReport } from '../../../src/transformers/coverageTransformer.js'; 9 | import { formatOptions } from '../../../src/utils/constants.js'; 10 | import { getCoverageHandler } from '../../../src/handlers/getHandler.js'; 11 | import { checkCoverageDataType } from '../../../src/utils/setCoverageDataType.js'; 12 | import { DeployCoverageData } from '../../../src/utils/types.js'; 13 | import { inputJsons, invalidJson, deployCoverage, testCoverage } from '../../utils/testConstants.js'; 14 | import { compareToBaselines } from '../../utils/baselineCompare.js'; 15 | import { postTestCleanup } from '../../utils/testCleanup.js'; 16 | import { preTestSetup } from '../../utils/testSetup.js'; 17 | 18 | describe('main', () => { 19 | const $$ = new TestContext(); 20 | 21 | beforeAll(async () => { 22 | await preTestSetup(); 23 | }); 24 | 25 | afterEach(() => { 26 | $$.restore(); 27 | }); 28 | 29 | afterAll(async () => { 30 | await postTestCleanup(); 31 | }); 32 | 33 | formatOptions.forEach((format) => { 34 | inputJsons.forEach(({ label, path }) => { 35 | const reportExtension = format === 'lcovonly' ? 'info' : 'xml'; 36 | const reportPath = resolve(`${format}_${label}.${reportExtension}`); 37 | it(`transforms the ${label} command JSON file into ${format} format`, async () => { 38 | await transformCoverageReport(path, reportPath, format, ['samples']); 39 | }); 40 | }); 41 | }); 42 | it('confirm the reports created are the same as the baselines.', async () => { 43 | await compareToBaselines(); 44 | }); 45 | it('confirms a failure on an invalid JSON file.', async () => { 46 | try { 47 | await transformCoverageReport(invalidJson, 'coverage.xml', 'sonar', []); 48 | throw new Error('Command did not fail as expected'); 49 | } catch (error) { 50 | if (error instanceof Error) { 51 | expect(error.message).to.include( 52 | 'The provided JSON does not match a known coverage data format from the Salesforce deploy or test command.' 53 | ); 54 | } else { 55 | throw new Error('An unknown error type was thrown.'); 56 | } 57 | } 58 | }); 59 | it('confirms a failure with an invalid format.', async () => { 60 | try { 61 | getCoverageHandler('invalid'); 62 | throw new Error('Command did not fail as expected'); 63 | } catch (error) { 64 | if (error instanceof Error) { 65 | expect(error.message).to.include('Unsupported format: invalid'); 66 | } else { 67 | throw new Error('An unknown error type was thrown.'); 68 | } 69 | } 70 | }); 71 | it('confirms a warning with a JSON file that does not exist.', async () => { 72 | const result = await transformCoverageReport('nonexistent.json', 'coverage.xml', 'sonar', []); 73 | expect(result.warnings).to.include('Failed to read nonexistent.json. Confirm file exists.'); 74 | }); 75 | it('ignore a package directory and produce a warning on the deploy command report.', async () => { 76 | const result = await transformCoverageReport(deployCoverage, 'coverage.xml', 'sonar', [ 77 | 'packaged', 78 | 'force-app', 79 | 'samples', 80 | ]); 81 | expect(result.warnings).to.include('The file name AccountTrigger was not found in any package directory.'); 82 | }); 83 | it('ignore a package directory and produce a warning on the test command report.', async () => { 84 | const result = await transformCoverageReport(testCoverage, 'coverage.xml', 'sonar', ['packaged', 'samples']); 85 | expect(result.warnings).to.include('The file name AccountTrigger was not found in any package directory.'); 86 | }); 87 | it('test where a statementMap has a non-object value.', async () => { 88 | const invalidDeployData = { 89 | 'someFile.js': { 90 | path: 'someFile.js', 91 | fnMap: {}, 92 | branchMap: {}, 93 | f: {}, 94 | b: {}, 95 | s: {}, 96 | statementMap: { 97 | someStatement: null, 98 | }, 99 | }, 100 | }; 101 | 102 | const result = checkCoverageDataType(invalidDeployData as unknown as DeployCoverageData); 103 | expect(result).to.equal('Unknown'); 104 | }); 105 | it('create a cobertura report using only 1 package directory', async () => { 106 | await transformCoverageReport(deployCoverage, 'coverage.xml', 'cobertura', ['packaged', 'force-app']); 107 | }); 108 | it('create a jacoco report using only 1 package directory', async () => { 109 | await transformCoverageReport(deployCoverage, 'coverage.xml', 'jacoco', ['packaged', 'force-app']); 110 | }); 111 | }); 112 | -------------------------------------------------------------------------------- /test/commands/acc-transformer/typeGuards.test.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { checkCoverageDataType } from '../../../src/utils/setCoverageDataType.js'; 3 | import { DeployCoverageData } from '../../../src/utils/types.js'; 4 | 5 | describe('isSingleTestCoverageData - non-object element', () => { 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).to.equal('Unknown'); 11 | }); 12 | }); 13 | 14 | describe('isDeployCoverageData - non-object', () => { 15 | it('returns Unknown when data is not an object', () => { 16 | const result = checkCoverageDataType(42 as unknown as DeployCoverageData); // 👈 non-object input 17 | expect(result).to.equal('Unknown'); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/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 | -------------------------------------------------------------------------------- /test/invalid.json: -------------------------------------------------------------------------------- 1 | { 2 | "no-map/AccountTrigger.trigger": { 3 | "path": "no-map/AccountTrigger.trigger" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/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 | -------------------------------------------------------------------------------- /test/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 -------------------------------------------------------------------------------- /test/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 | -------------------------------------------------------------------------------- /test/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 | -------------------------------------------------------------------------------- /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/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 { 8 | jacocoBaselinePath, 9 | lcovBaselinePath, 10 | sonarBaselinePath, 11 | cloverBaselinePath, 12 | coberturaBaselinePath, 13 | inputJsons, 14 | } from './testConstants.js'; 15 | import { normalizeCoverageReport } from './normalizeCoverageReport.js'; 16 | 17 | export async function compareToBaselines(): Promise { 18 | const baselineMap = { 19 | sonar: sonarBaselinePath, 20 | lcovonly: lcovBaselinePath, 21 | jacoco: jacocoBaselinePath, 22 | cobertura: coberturaBaselinePath, 23 | clover: cloverBaselinePath, 24 | } as const; 25 | 26 | const normalizationRequired = new Set(['cobertura', 'clover']); 27 | 28 | for (const format of formatOptions as Array) { 29 | for (const { label } of inputJsons) { 30 | const reportExtension = format === 'lcovonly' ? 'info' : 'xml'; 31 | const outputPath = resolve(`${format}_${label}.${reportExtension}`); 32 | const outputContent = await readFile(outputPath, 'utf-8'); 33 | const baselineContent = await readFile(baselineMap[format], 'utf-8'); 34 | 35 | if (normalizationRequired.has(format)) { 36 | strictEqual( 37 | normalizeCoverageReport(outputContent), 38 | normalizeCoverageReport(baselineContent), 39 | `Mismatch between ${outputPath} and ${baselineMap[format]}` 40 | ); 41 | } else { 42 | strictEqual(outputContent, baselineContent, `Mismatch between ${outputPath} and ${baselineMap[format]}`); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /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 { sfdxConfigFile, inputJsons, defaultPath } from './testConstants.js'; 7 | 8 | export async function postTestCleanup(): Promise { 9 | await rm(sfdxConfigFile); 10 | await rm('force-app/main/default/classes/AccountProfile.cls'); 11 | await rm('packaged/triggers/AccountTrigger.trigger'); 12 | await rm('force-app', { recursive: true }); 13 | await rm('packaged', { recursive: true }); 14 | 15 | const pathsToRemove = formatOptions 16 | .flatMap((format) => 17 | inputJsons.map(({ label }) => { 18 | const extension = format === 'lcovonly' ? 'info' : 'xml'; 19 | return resolve(`${format}_${label}.${extension}`); 20 | }) 21 | ) 22 | .concat(defaultPath); 23 | 24 | for (const path of pathsToRemove) { 25 | await rm(path).catch(() => {}); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /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('test/deploy_coverage.json'); 6 | export const testCoverage = resolve('test/test_coverage.json'); 7 | export const invalidJson = resolve('test/invalid.json'); 8 | export const sonarBaselinePath = resolve('test/sonar_baseline.xml'); 9 | export const jacocoBaselinePath = resolve('test/jacoco_baseline.xml'); 10 | export const lcovBaselinePath = resolve('test/lcov_baseline.info'); 11 | export const coberturaBaselinePath = resolve('test/cobertura_baseline.xml'); 12 | export const cloverBaselinePath = resolve('test/clover_baseline.xml'); 13 | export const defaultPath = resolve('coverage.xml'); 14 | export const sfdxConfigFile = resolve('sfdx-project.json'); 15 | 16 | const configFile = { 17 | packageDirectories: [{ path: 'force-app', default: true }, { path: 'packaged' }, { path: 'samples' }], 18 | namespace: '', 19 | sfdcLoginUrl: 'https://login.salesforce.com', 20 | sourceApiVersion: '58.0', 21 | }; 22 | export const configJsonString = JSON.stringify(configFile, null, 2); 23 | export const inputJsons = [ 24 | { label: 'deploy', path: deployCoverage }, 25 | { label: 'test', path: testCoverage }, 26 | ] as const; 27 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------