├── .dockerignore ├── .gitignore ├── tests ├── reportTest.json ├── reportTestFail.json ├── 0.json ├── 0b.json ├── 100.json ├── 100b.json ├── 60.json ├── 60b.json ├── 70.json ├── 70b.json ├── 78.json ├── 78b.json ├── 80.json ├── 80b.json ├── 87.json ├── 87b.json ├── 90.json ├── 90b.json ├── 99.json ├── 99b.json ├── 599.json ├── 599b.json ├── 899.json ├── 899b.json ├── 999.json ├── 999b.json ├── custom1.json ├── custom2.json ├── jacocoDivZero.csv ├── multi1.csv ├── multi2.csv ├── branchesDivZero.csv ├── jacoco.csv ├── jacoco100.csv ├── jacoco90.csv ├── jacoco901.csv ├── branches100.csv ├── branches90.csv ├── branches901.csv ├── summaryReportTest.csv ├── 0.svg ├── 60.svg ├── 70.svg ├── 78.svg ├── 80.svg ├── 87b.svg ├── 90.svg ├── 90b.svg ├── 99.svg ├── 100.svg ├── 599.svg ├── 899.svg ├── 999.svg ├── 999b.svg ├── custom1.svg ├── custom2.svg ├── integration-actions-mode.py ├── integration-cli-mode.py └── tests.py ├── images ├── job-summary-example.png └── jacoco-badge-generator.png ├── Dockerfile ├── .github ├── dependabot.yml └── workflows │ ├── major-release-num.yml │ ├── pypi-publish.yml │ ├── codeql-analysis.yml │ └── build.yml ├── LICENSE ├── src ├── jacoco_badge_generator │ ├── __init__.py │ ├── __main__.py │ └── coverage_badges.py └── entrypoint.py ├── pyproject.toml ├── action.yml └── CHANGELOG.md /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !Dockerfile 3 | !src 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | tests/__pycache__/ 3 | *.pyc 4 | dist/ 5 | -------------------------------------------------------------------------------- /tests/reportTest.json: -------------------------------------------------------------------------------- 1 | {"branches": 77.77777777777777, "coverage": 72.72727272727272} -------------------------------------------------------------------------------- /tests/reportTestFail.json: -------------------------------------------------------------------------------- 1 | {"branches": 77.777777787777777, "coverage": 72.72727282727272} -------------------------------------------------------------------------------- /tests/0.json: -------------------------------------------------------------------------------- 1 | {"color": "#e05d44", "label": "coverage", "message": "0%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/0b.json: -------------------------------------------------------------------------------- 1 | {"color": "#e05d44", "label": "branches", "message": "0%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/100.json: -------------------------------------------------------------------------------- 1 | {"color": "#4c1", "label": "coverage", "message": "100%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/100b.json: -------------------------------------------------------------------------------- 1 | {"color": "#4c1", "label": "branches", "message": "100%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/60.json: -------------------------------------------------------------------------------- 1 | {"color": "#fe7d37", "label": "coverage", "message": "60%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/60b.json: -------------------------------------------------------------------------------- 1 | {"color": "#fe7d37", "label": "branches", "message": "60%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/70.json: -------------------------------------------------------------------------------- 1 | {"color": "#dfb317", "label": "coverage", "message": "70%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/70b.json: -------------------------------------------------------------------------------- 1 | {"color": "#dfb317", "label": "branches", "message": "70%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/78.json: -------------------------------------------------------------------------------- 1 | {"color": "#dfb317", "label": "coverage", "message": "78%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/78b.json: -------------------------------------------------------------------------------- 1 | {"color": "#dfb317", "label": "branches", "message": "78%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/80.json: -------------------------------------------------------------------------------- 1 | {"color": "#a4a61d", "label": "coverage", "message": "80%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/80b.json: -------------------------------------------------------------------------------- 1 | {"color": "#a4a61d", "label": "branches", "message": "80%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/87.json: -------------------------------------------------------------------------------- 1 | {"color": "#a4a61d", "label": "coverage", "message": "87%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/87b.json: -------------------------------------------------------------------------------- 1 | {"color": "#a4a61d", "label": "branches", "message": "87%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/90.json: -------------------------------------------------------------------------------- 1 | {"color": "#97ca00", "label": "coverage", "message": "90%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/90b.json: -------------------------------------------------------------------------------- 1 | {"color": "#97ca00", "label": "branches", "message": "90%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/99.json: -------------------------------------------------------------------------------- 1 | {"color": "#97ca00", "label": "coverage", "message": "99%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/99b.json: -------------------------------------------------------------------------------- 1 | {"color": "#97ca00", "label": "branches", "message": "99%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/599.json: -------------------------------------------------------------------------------- 1 | {"color": "#e05d44", "label": "coverage", "message": "59.9%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/599b.json: -------------------------------------------------------------------------------- 1 | {"color": "#e05d44", "label": "branches", "message": "59.9%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/899.json: -------------------------------------------------------------------------------- 1 | {"color": "#a4a61d", "label": "coverage", "message": "89.9%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/899b.json: -------------------------------------------------------------------------------- 1 | {"color": "#a4a61d", "label": "branches", "message": "89.9%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/999.json: -------------------------------------------------------------------------------- 1 | {"color": "#97ca00", "label": "coverage", "message": "99.9%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/999b.json: -------------------------------------------------------------------------------- 1 | {"color": "#97ca00", "label": "branches", "message": "99.9%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/custom1.json: -------------------------------------------------------------------------------- 1 | {"color": "#4c1", "label": "custom coverage label one", "message": "100%", "schemaVersion": 1} -------------------------------------------------------------------------------- /tests/custom2.json: -------------------------------------------------------------------------------- 1 | {"color": "#97ca00", "label": "custom coverage label two", "message": "90%", "schemaVersion": 1} -------------------------------------------------------------------------------- /images/job-summary-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cicirello/jacoco-badge-generator/HEAD/images/job-summary-example.png -------------------------------------------------------------------------------- /images/jacoco-badge-generator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cicirello/jacoco-badge-generator/HEAD/images/jacoco-badge-generator.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2020-2025 Vincent A. Cicirello 2 | # https://www.cicirello.org/ 3 | # Licensed under the MIT License 4 | FROM ghcr.io/cicirello/pyaction:3.14.1-gh-2.83.1 5 | COPY src / 6 | ENTRYPOINT ["/entrypoint.py"] 7 | -------------------------------------------------------------------------------- /tests/jacocoDivZero.csv: -------------------------------------------------------------------------------- 1 | GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED 2 | Program Name,org.something.package,OneClass,0,0,0,12,0,33,0,13,0,7 3 | Program Name,org.something.package,AnotherClass,0,0,0,16,0,37,0,15,0,7 4 | Program Name,org.something.package,AnotherClass,0,0,0,16,0,37,0,15,0,7 5 | -------------------------------------------------------------------------------- /tests/multi1.csv: -------------------------------------------------------------------------------- 1 | GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED 2 | Program Name,org.something.package,OneClass,20,80,4,32,0,33,0,13,0,7 3 | Program Name,org.something.package,AnotherClass,40,160,3,28,0,37,0,15,0,7 4 | Program Name,org.something.package,AnotherClass,60,240,8,25,0,37,0,15,0,7 5 | -------------------------------------------------------------------------------- /tests/multi2.csv: -------------------------------------------------------------------------------- 1 | GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED 2 | Program Name,org.something.package,OneClass,30,100,6,70,0,33,0,13,0,7 3 | Program Name,org.something.package,AnotherClass,50,100,3,10,0,37,0,15,0,7 4 | Program Name,org.something.package,AnotherClass,20,100,2,9,0,37,0,15,0,7 5 | -------------------------------------------------------------------------------- /tests/branchesDivZero.csv: -------------------------------------------------------------------------------- 1 | GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED 2 | Program Name,org.something.package,OneClass,5,107,0,0,0,33,0,13,0,7 3 | Program Name,org.something.package,AnotherClass,6,1632,0,0,0,37,0,15,0,7 4 | Program Name,org.something.package,AnotherClass,7,61,0,0,0,37,0,15,0,7 5 | -------------------------------------------------------------------------------- /tests/jacoco.csv: -------------------------------------------------------------------------------- 1 | GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED 2 | Program Name,org.something.package,OneClass,0,123,75,700,0,33,0,13,0,7 3 | Program Name,org.something.package,AnotherClass,0,1632,15,150,0,37,0,15,0,7 4 | Program Name,org.something.package,AnotherClass,0,61,10,50,0,37,0,15,0,7 5 | -------------------------------------------------------------------------------- /tests/jacoco100.csv: -------------------------------------------------------------------------------- 1 | GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED 2 | Program Name,org.something.package,OneClass,0,157,0,12,0,33,0,13,0,7 3 | Program Name,org.something.package,AnotherClass,0,1742,0,16,0,37,0,15,0,7 4 | Program Name,org.something.package,AnotherClass,0,101,0,16,0,37,0,15,0,7 5 | -------------------------------------------------------------------------------- /tests/jacoco90.csv: -------------------------------------------------------------------------------- 1 | GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED 2 | Program Name,org.something.package,OneClass,50,107,0,12,0,33,0,13,0,7 3 | Program Name,org.something.package,AnotherClass,110,1632,0,16,0,37,0,15,0,7 4 | Program Name,org.something.package,AnotherClass,40,61,0,16,0,37,0,15,0,7 5 | -------------------------------------------------------------------------------- /tests/jacoco901.csv: -------------------------------------------------------------------------------- 1 | GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED 2 | Program Name,org.something.package,OneClass,50,107,0,12,0,33,0,13,0,7 3 | Program Name,org.something.package,AnotherClass,100,1642,0,16,0,37,0,15,0,7 4 | Program Name,org.something.package,AnotherClass,48,53,0,16,0,37,0,15,0,7 5 | -------------------------------------------------------------------------------- /tests/branches100.csv: -------------------------------------------------------------------------------- 1 | GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED 2 | Program Name,org.something.package,OneClass,5,107,0,600,0,33,0,13,0,7 3 | Program Name,org.something.package,AnotherClass,6,1632,0,300,0,37,0,15,0,7 4 | Program Name,org.something.package,AnotherClass,7,61,0,100,0,37,0,15,0,7 5 | -------------------------------------------------------------------------------- /tests/branches90.csv: -------------------------------------------------------------------------------- 1 | GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED 2 | Program Name,org.something.package,OneClass,0,123,75,700,0,33,0,13,0,7 3 | Program Name,org.something.package,AnotherClass,0,1632,15,150,0,37,0,15,0,7 4 | Program Name,org.something.package,AnotherClass,0,61,10,50,0,37,0,15,0,7 5 | -------------------------------------------------------------------------------- /tests/branches901.csv: -------------------------------------------------------------------------------- 1 | GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED 2 | Program Name,org.something.package,OneClass,0,123,75,700,0,33,0,13,0,7 3 | Program Name,org.something.package,AnotherClass,0,1632,14,151,0,37,0,15,0,7 4 | Program Name,org.something.package,AnotherClass,0,61,10,50,0,37,0,15,0,7 5 | -------------------------------------------------------------------------------- /tests/summaryReportTest.csv: -------------------------------------------------------------------------------- 1 | GROUP,PACKAGE,CLASS,INSTRUCTION_MISSED,INSTRUCTION_COVERED,BRANCH_MISSED,BRANCH_COVERED,LINE_MISSED,LINE_COVERED,COMPLEXITY_MISSED,COMPLEXITY_COVERED,METHOD_MISSED,METHOD_COVERED 2 | Program Name,org.something.package,OneClass,50,100,50,200,0,33,0,13,0,7 3 | Program Name,org.something.package,AnotherClass,100,300,150,300,0,37,0,15,0,7 4 | Program Name,org.something.package,YetAnotherClass,150,400,0,200,0,37,0,15,0,7 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "docker" 9 | directory: "/" 10 | target-branch: "main" 11 | schedule: 12 | interval: "daily" 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | target-branch: "main" 16 | schedule: 17 | interval: "daily" 18 | -------------------------------------------------------------------------------- /.github/workflows/major-release-num.yml: -------------------------------------------------------------------------------- 1 | name: Move Major Release Tag 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | movetag: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v6 13 | 14 | - name: Get major version num and update tag 15 | run: | 16 | VERSION=${GITHUB_REF#refs/tags/} 17 | MAJOR=${VERSION%%.*} 18 | git config --global user.name 'Vincent A Cicirello' 19 | git config --global user.email 'cicirello@users.noreply.github.com' 20 | git tag -fa ${MAJOR} -m "Update major version tag" 21 | git push origin ${MAJOR} --force 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/pypi-publish.yml: -------------------------------------------------------------------------------- 1 | name: Upload Package to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | deploy: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: pypi 18 | url: https://pypi.org/p/jacoco-badge-generator 19 | permissions: 20 | id-token: write 21 | 22 | steps: 23 | - uses: actions/checkout@v6 24 | 25 | - name: Set up Python 26 | uses: actions/setup-python@v6 27 | with: 28 | python-version: '3.14' 29 | 30 | - name: Install dependencies 31 | run: | 32 | python3 -m pip install --upgrade pip 33 | python3 -m pip install --upgrade build 34 | 35 | - name: Build package 36 | run: python3 -m build 37 | 38 | - name: Publish package 39 | uses: pypa/gh-action-pypi-publish@release/v1 40 | -------------------------------------------------------------------------------- /tests/0.svg: -------------------------------------------------------------------------------- 1 | coverage0% -------------------------------------------------------------------------------- /tests/60.svg: -------------------------------------------------------------------------------- 1 | coverage60% -------------------------------------------------------------------------------- /tests/70.svg: -------------------------------------------------------------------------------- 1 | coverage70% -------------------------------------------------------------------------------- /tests/78.svg: -------------------------------------------------------------------------------- 1 | coverage78% -------------------------------------------------------------------------------- /tests/80.svg: -------------------------------------------------------------------------------- 1 | coverage80% -------------------------------------------------------------------------------- /tests/87b.svg: -------------------------------------------------------------------------------- 1 | branches87% -------------------------------------------------------------------------------- /tests/90.svg: -------------------------------------------------------------------------------- 1 | coverage90% -------------------------------------------------------------------------------- /tests/90b.svg: -------------------------------------------------------------------------------- 1 | branches90% -------------------------------------------------------------------------------- /tests/99.svg: -------------------------------------------------------------------------------- 1 | coverage99% -------------------------------------------------------------------------------- /tests/100.svg: -------------------------------------------------------------------------------- 1 | coverage100% -------------------------------------------------------------------------------- /tests/599.svg: -------------------------------------------------------------------------------- 1 | coverage59.9% -------------------------------------------------------------------------------- /tests/899.svg: -------------------------------------------------------------------------------- 1 | coverage89.9% -------------------------------------------------------------------------------- /tests/999.svg: -------------------------------------------------------------------------------- 1 | coverage99.9% -------------------------------------------------------------------------------- /tests/999b.svg: -------------------------------------------------------------------------------- 1 | branches99.9% -------------------------------------------------------------------------------- /tests/custom1.svg: -------------------------------------------------------------------------------- 1 | custom coverage label one100% -------------------------------------------------------------------------------- /tests/custom2.svg: -------------------------------------------------------------------------------- 1 | custom coverage label two90% -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Vincent A. Cicirello 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 | -------------------------------------------------------------------------------- /src/jacoco_badge_generator/__init__.py: -------------------------------------------------------------------------------- 1 | # jacoco-badge-generator: Coverage badges, and pull request coverage checks, 2 | # from JaCoCo reports in GitHub Actions. 3 | # 4 | # Copyright (c) 2020-2023 Vincent A Cicirello 5 | # https://www.cicirello.org/ 6 | # 7 | # MIT License 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # 27 | 28 | from .coverage_badges import main 29 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "jacoco-badge-generator" 7 | version = "2.12.1" 8 | authors = [ 9 | { name="Vincent A. Cicirello", email="development@cicirello.org" }, 10 | ] 11 | description = "JaCoCo coverage badges (SVG format), and coverage checks (e.g., decreasing coverage and minimum coverage)" 12 | readme = "README.md" 13 | requires-python = ">=3.10" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | "Intended Audience :: Developers", 19 | "Topic :: Software Development :: Testing", 20 | ] 21 | keywords = [ 22 | "badges", 23 | "badge generator", 24 | "branches coverage", 25 | "C0 coverage", 26 | "C1 coverage", 27 | "coverage", 28 | "coverage reports", 29 | "instructions coverage", 30 | "jacoco", 31 | "Java", 32 | "JVM", 33 | "Kotlin", 34 | "Scala", 35 | "testing", 36 | ] 37 | 38 | [project.urls] 39 | "Information Page" = "https://actions.cicirello.org/jacoco-badge-generator/" 40 | "GitHub Repository" = "https://github.com/cicirello/jacoco-badge-generator" 41 | "Bug Tracker" = "https://github.com/cicirello/jacoco-badge-generator/issues" 42 | "Changelog" = "https://github.com/cicirello/jacoco-badge-generator/blob/main/CHANGELOG.md" 43 | 44 | [tool.hatch.build] 45 | exclude = [ 46 | "/.github", 47 | "/images", 48 | "/src/entrypoint.py", 49 | "/tests", 50 | "/.dockerignore", 51 | "/action.yml", 52 | "/Dockerfile", 53 | ] 54 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '36 23 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'python' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v6 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v4 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v4 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v4 68 | -------------------------------------------------------------------------------- /src/entrypoint.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env -S python3 -B 2 | # 3 | # jacoco-badge-generator: Coverage badges, and pull request coverage checks, 4 | # from JaCoCo reports in GitHub Actions. 5 | # 6 | # Copyright (c) 2020-2025 Vincent A Cicirello 7 | # https://www.cicirello.org/ 8 | # 9 | # MIT License 10 | # 11 | # Permission is hereby granted, free of charge, to any person obtaining a copy 12 | # of this software and associated documentation files (the "Software"), to deal 13 | # in the Software without restriction, including without limitation the rights 14 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | # copies of the Software, and to permit persons to whom the Software is 16 | # furnished to do so, subject to the following conditions: 17 | # 18 | # The above copyright notice and this permission notice shall be included in all 19 | # copies or substantial portions of the Software. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | # SOFTWARE. 28 | # 29 | 30 | from jacoco_badge_generator import main 31 | from jacoco_badge_generator.coverage_badges import stringToPercentage 32 | from jacoco_badge_generator.coverage_badges import colorCutoffsStringToNumberList 33 | import sys 34 | 35 | if __name__ == "__main__" : 36 | # IMPORTANT: This is the entrypoint for the GitHub Action only. 37 | # 38 | # This is the entry point when using the 39 | # jacoco-badge-generator as a GitHub Action 40 | # (its primary use-case). The source code for the entry 41 | # point for use locally as a command-line utility is found 42 | # at src/jacoco_badge_generator/__main__.py, 43 | 44 | main( 45 | jacocoCsvFile = sys.argv[1], 46 | badgesDirectory = sys.argv[2], 47 | coverageFilename = sys.argv[3], 48 | branchesFilename = sys.argv[4], 49 | generateCoverageBadge = sys.argv[5].lower() == "true", 50 | generateBranchesBadge = sys.argv[6].lower() == "true", 51 | onMissingReport = sys.argv[7].lower(), 52 | minCoverage = stringToPercentage(sys.argv[8]), 53 | minBranches = stringToPercentage(sys.argv[9]), 54 | failOnCoverageDecrease = sys.argv[10].lower() == "true", 55 | failOnBranchesDecrease = sys.argv[11].lower() == "true", 56 | colorCutoffs = colorCutoffsStringToNumberList(sys.argv[12]), 57 | colors = sys.argv[13].replace(',', ' ').split(), 58 | generateCoverageJSON = sys.argv[14].lower() == "true", 59 | generateBranchesJSON = sys.argv[15].lower() == "true", 60 | coverageJSON = sys.argv[16], 61 | branchesJSON = sys.argv[17], 62 | generateSummary = sys.argv[18].lower() == "true", 63 | summaryFilename = sys.argv[19], 64 | appendWorkflowSummary = sys.argv[20].lower() == "true", 65 | coverageLabel = sys.argv[21], 66 | branchesLabel = sys.argv[22], 67 | ghActionsMode = True, 68 | workflowJobSummaryHeading = sys.argv[23], 69 | coverageDecreaseLimit = stringToPercentage(sys.argv[24]), 70 | branchesDecreaseLimit = stringToPercentage(sys.argv[25]) 71 | ) 72 | -------------------------------------------------------------------------------- /tests/integration-actions-mode.py: -------------------------------------------------------------------------------- 1 | # jacoco-badge-generator: Github action for generating a jacoco coverage 2 | # percentage badge. 3 | # 4 | # Copyright (c) 2020-2023 Vincent A Cicirello 5 | # https://www.cicirello.org/ 6 | # 7 | # MIT License 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # 27 | 28 | import unittest 29 | import json 30 | 31 | import sys 32 | sys.path.insert(0,'src') 33 | import jacoco_badge_generator.coverage_badges as jbg 34 | 35 | class IntegrationTest(unittest.TestCase) : 36 | 37 | def testGLOBIntegrationMultiJacocoReportsCase(self) : 38 | with open("tests/78.svg","r") as expected : 39 | with open("tests/glob/badges/coverageMulti.svg","r") as generated : 40 | self.assertEqual(expected.read(), generated.read()) 41 | with open("tests/87b.svg","r") as expected : 42 | with open("tests/glob/badges/branchesMulti.svg","r") as generated : 43 | self.assertEqual(expected.read(), generated.read()) 44 | 45 | def testIntegrationCustomCoverageLabel(self) : 46 | with open("tests/custom1.svg","r") as expected : 47 | with open("tests/badges/customCoverage.svg","r") as generated : 48 | self.assertEqual(expected.read(), generated.read()) 49 | 50 | def testIntegrationCustomBranchesLabel(self) : 51 | with open("tests/custom2.svg","r") as expected : 52 | with open("tests/badges/customBranches.svg","r") as generated : 53 | self.assertEqual(expected.read(), generated.read()) 54 | 55 | def testIntegrationCustomCoverageLabelJSON(self) : 56 | with open("tests/custom1.json","r") as expected : 57 | with open("tests/badges/customCoverage.json","r") as generated : 58 | self.assertEqual(expected.read(), generated.read()) 59 | 60 | def testIntegrationCustomBranchesLabelJSON(self) : 61 | with open("tests/custom2.json","r") as expected : 62 | with open("tests/badges/customBranches.json","r") as generated : 63 | self.assertEqual(expected.read(), generated.read()) 64 | 65 | def testIntegrationInstructionsBadge(self) : 66 | with open("tests/100.svg","r") as expected : 67 | with open("tests/badges/jacoco.svg","r") as generated : 68 | self.assertEqual(expected.read(), generated.read()) 69 | 70 | def testIntegrationBranchesBadge(self) : 71 | with open("tests/90b.svg","r") as expected : 72 | with open("tests/badges/branches.svg","r") as generated : 73 | self.assertEqual(expected.read(), generated.read()) 74 | 75 | def testIntegrationMultiJacocoReportsCase(self) : 76 | with open("tests/78.svg","r") as expected : 77 | with open("tests/badges/coverageMulti.svg","r") as generated : 78 | self.assertEqual(expected.read(), generated.read()) 79 | with open("tests/87b.svg","r") as expected : 80 | with open("tests/badges/branchesMulti.svg","r") as generated : 81 | self.assertEqual(expected.read(), generated.read()) 82 | 83 | def testIntegrationSummaryReport(self) : 84 | with open("tests/summary/coverage-summary.json", "r") as f : 85 | d = json.load(f) 86 | self.assertAlmostEqual(72.72727272727272, d["coverage"]) 87 | self.assertAlmostEqual(77.77777777777777, d["branches"]) 88 | 89 | def testIntegrationInstructionsJSON(self) : 90 | with open("tests/endpoints/jacoco.json", "r") as f : 91 | d = json.load(f) 92 | self.assertEqual(1, d["schemaVersion"]) 93 | self.assertEqual("coverage", d["label"]) 94 | self.assertEqual("100%", d["message"]) 95 | self.assertEqual(jbg.defaultColors[0], d["color"]) 96 | 97 | def testIntegrationBranchesJSON(self) : 98 | with open("tests/endpoints/branches.json", "r") as f : 99 | d = json.load(f) 100 | self.assertEqual(1, d["schemaVersion"]) 101 | self.assertEqual("branches", d["label"]) 102 | self.assertEqual("90%", d["message"]) 103 | self.assertEqual(jbg.defaultColors[1], d["color"]) 104 | -------------------------------------------------------------------------------- /tests/integration-cli-mode.py: -------------------------------------------------------------------------------- 1 | # jacoco-badge-generator: Github action for generating a jacoco coverage 2 | # percentage badge. 3 | # 4 | # Copyright (c) 2020-2023 Vincent A Cicirello 5 | # https://www.cicirello.org/ 6 | # 7 | # MIT License 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # 27 | 28 | import unittest 29 | import json 30 | 31 | import sys 32 | sys.path.insert(0,'src') 33 | import jacoco_badge_generator.coverage_badges as jbg 34 | 35 | class IntegrationTest(unittest.TestCase) : 36 | 37 | def testCLIGLOBIntegrationMultiJacocoReportsCase(self) : 38 | with open("tests/78.svg","r") as expected : 39 | with open("tests/glob/badges/coverageMultiCLI.svg","r") as generated : 40 | self.assertEqual(expected.read(), generated.read()) 41 | with open("tests/87b.svg","r") as expected : 42 | with open("tests/glob/badges/branchesMultiCLI.svg","r") as generated : 43 | self.assertEqual(expected.read(), generated.read()) 44 | 45 | def testCLIIntegrationCustomCoverageLabel(self) : 46 | with open("tests/custom1.svg","r") as expected : 47 | with open("tests/cli/badges/customCoverage.svg","r") as generated : 48 | self.assertEqual(expected.read(), generated.read()) 49 | 50 | def testCLIIntegrationCustomBranchesLabel(self) : 51 | with open("tests/custom2.svg","r") as expected : 52 | with open("tests/cli/badges/customBranches.svg","r") as generated : 53 | self.assertEqual(expected.read(), generated.read()) 54 | 55 | def testCLIIntegrationCustomCoverageLabelJSON(self) : 56 | with open("tests/custom1.json","r") as expected : 57 | with open("tests/cli/badges/customCoverage.json","r") as generated : 58 | self.assertEqual(expected.read(), generated.read()) 59 | 60 | def testCLIIntegrationCustomBranchesLabelJSON(self) : 61 | with open("tests/custom2.json","r") as expected : 62 | with open("tests/cli/badges/customBranches.json","r") as generated : 63 | self.assertEqual(expected.read(), generated.read()) 64 | 65 | def testCLIIntegrationInstructionsBadge(self) : 66 | with open("tests/100.svg","r") as expected : 67 | with open("tests/cli/badges/jacoco.svg","r") as generated : 68 | self.assertEqual(expected.read(), generated.read()) 69 | 70 | def testCLIIntegrationBranchesBadge(self) : 71 | with open("tests/90b.svg","r") as expected : 72 | with open("tests/cli/badges/branches.svg","r") as generated : 73 | self.assertEqual(expected.read(), generated.read()) 74 | 75 | def testCLIIntegrationMultiJacocoReportsCase(self) : 76 | with open("tests/78.svg","r") as expected : 77 | with open("tests/cli/badges/coverageMulti.svg","r") as generated : 78 | self.assertEqual(expected.read(), generated.read()) 79 | with open("tests/87b.svg","r") as expected : 80 | with open("tests/cli/badges/branchesMulti.svg","r") as generated : 81 | self.assertEqual(expected.read(), generated.read()) 82 | 83 | def testCLIIntegrationSummaryReport(self) : 84 | with open("tests/cli/summary/coverage-summary.json", "r") as f : 85 | d = json.load(f) 86 | self.assertAlmostEqual(72.72727272727272, d["coverage"]) 87 | self.assertAlmostEqual(77.77777777777777, d["branches"]) 88 | 89 | def testCLIIntegrationInstructionsJSON(self) : 90 | with open("tests/cli/badgesJSON/jacoco.json", "r") as f : 91 | d = json.load(f) 92 | self.assertEqual(1, d["schemaVersion"]) 93 | self.assertEqual("coverage", d["label"]) 94 | self.assertEqual("100%", d["message"]) 95 | self.assertEqual(jbg.defaultColors[0], d["color"]) 96 | 97 | def testCLIIntegrationBranchesJSON(self) : 98 | with open("tests/cli/badgesJSON/branches.json", "r") as f : 99 | d = json.load(f) 100 | self.assertEqual(1, d["schemaVersion"]) 101 | self.assertEqual("branches", d["label"]) 102 | self.assertEqual("90%", d["message"]) 103 | self.assertEqual(jbg.defaultColors[1], d["color"]) 104 | 105 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | # jacoco-badge-generator: Coverage badges, and pull request coverage checks, 2 | # from JaCoCo reports in GitHub Actions 3 | # 4 | # Copyright (c) 2020-2025 Vincent A Cicirello 5 | # https://www.cicirello.org/ 6 | # 7 | # MIT License 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # 27 | 28 | 29 | name: 'jacoco-badge-generator' 30 | description: 'JaCoCo coverage badges and pull request coverage checks in GitHub Actions' 31 | branding: 32 | icon: 'book-open' 33 | color: 'green' 34 | inputs: 35 | jacoco-csv-file: 36 | description: 'Full path, relative to the root of the repository, to the jacoco csv file(s), including filename(s)' 37 | required: false 38 | default: 'target/site/jacoco/jacoco.csv' 39 | badges-directory: 40 | description: 'The directory for storing badges, relative to the root of the repository.' 41 | required: false 42 | default: '.github/badges' 43 | coverage-badge-filename: 44 | description: 'The filename of the coverage badge.' 45 | required: false 46 | default: 'jacoco.svg' 47 | branches-badge-filename: 48 | description: 'The filename of the branches coverage badge.' 49 | required: false 50 | default: 'branches.svg' 51 | generate-coverage-badge: 52 | description: 'Controls whether or not to generate the coverage badge.' 53 | required: false 54 | default: true 55 | generate-branches-badge: 56 | description: 'Controls whether or not to generate the branches coverage badge.' 57 | required: false 58 | default: false 59 | coverage-label: 60 | description: 'Text for the left-side of the coverage badge.' 61 | required: false 62 | default: coverage 63 | branches-label: 64 | description: 'Text for the left-side of the branches coverage badge.' 65 | required: false 66 | default: branches 67 | on-missing-report: 68 | description: 'Controls what happens if one or more jacoco.csv files do not exist.' 69 | required: false 70 | default: 'fail' 71 | fail-if-coverage-less-than: 72 | description: 'Enables failing workflow run when coverage below specified threshold.' 73 | required: false 74 | default: 0 75 | fail-if-branches-less-than: 76 | description: 'Enables failing workflow run when branches coverage below specified threshold.' 77 | required: false 78 | default: 0 79 | fail-on-coverage-decrease: 80 | description: 'Enables failing workflow if coverage is less than it was on previous run.' 81 | required: false 82 | default: false 83 | fail-on-branches-decrease: 84 | description: 'Enables failing workflow if branches coverage is less than it was on previous run.' 85 | required: false 86 | default: false 87 | coverage-decrease-limit: 88 | description: 'Overrides fail-on-coverage-decrease when coverage is at least this limit' 89 | required: false 90 | default: 100 91 | branches-decrease-limit: 92 | description: 'Overrides fail-on-branches-decrease when branches coverage is at least this limit' 93 | required: false 94 | default: 100 95 | intervals: 96 | description: 'List of coverage percentages as cutoffs for each color.' 97 | required: false 98 | default: 100 90 80 70 60 0 99 | colors: 100 | description: 'List of colors to use ordered by coverage interval, best coverage to worst.' 101 | required: false 102 | default: '#4c1 #97ca00 #a4a61d #dfb317 #fe7d37 #e05d44' 103 | generate-coverage-endpoint: 104 | description: 'Controls whether or not to generate the coverage JSON endpoint.' 105 | required: false 106 | default: false 107 | generate-branches-endpoint: 108 | description: 'Controls whether or not to generate the branches coverage JSON endpoint.' 109 | required: false 110 | default: false 111 | coverage-endpoint-filename: 112 | description: 'The filename of the coverage JSON endpoint.' 113 | required: false 114 | default: 'jacoco.json' 115 | branches-endpoint-filename: 116 | description: 'The filename of the branches coverage JSON endpoint.' 117 | required: false 118 | default: 'branches.json' 119 | generate-summary: 120 | description: 'Controls whether or not to generate a JSON file containing the coverage percentages as floating-point values.' 121 | required: false 122 | default: false 123 | summary-filename: 124 | description: 'The filename of the summary file.' 125 | required: false 126 | default: 'coverage-summary.json' 127 | generate-workflow-summary: 128 | description: 'Controls whether or not to append summary to the GitHub workflow summary page.' 129 | required: false 130 | default: true 131 | workflow-summary-heading: 132 | description: 'The heading for the GitHub workflow job summary' 133 | required: false 134 | default: JaCoCo Test Coverage Summary 135 | outputs: 136 | coverage: 137 | description: 'The jacoco coverage percentage as computed from the data in the jacoco.csv file.' 138 | branches: 139 | description: 'The jacoco branch coverage percentage as computed from the data in the jacoco.csv file.' 140 | runs: 141 | using: 'docker' 142 | image: 'Dockerfile' 143 | args: 144 | - ${{ inputs.jacoco-csv-file }} 145 | - ${{ inputs.badges-directory }} 146 | - ${{ inputs.coverage-badge-filename }} 147 | - ${{ inputs.branches-badge-filename }} 148 | - ${{ inputs.generate-coverage-badge }} 149 | - ${{ inputs.generate-branches-badge }} 150 | - ${{ inputs.on-missing-report }} 151 | - ${{ inputs.fail-if-coverage-less-than }} 152 | - ${{ inputs.fail-if-branches-less-than }} 153 | - ${{ inputs.fail-on-coverage-decrease }} 154 | - ${{ inputs.fail-on-branches-decrease }} 155 | - ${{ inputs.intervals }} 156 | - ${{ inputs.colors }} 157 | - ${{ inputs.generate-coverage-endpoint }} 158 | - ${{ inputs.generate-branches-endpoint }} 159 | - ${{ inputs.coverage-endpoint-filename }} 160 | - ${{ inputs.branches-endpoint-filename }} 161 | - ${{ inputs.generate-summary }} 162 | - ${{ inputs.summary-filename }} 163 | - ${{ inputs.generate-workflow-summary }} 164 | - ${{ inputs.coverage-label }} 165 | - ${{ inputs.branches-label }} 166 | - ${{ inputs.workflow-summary-heading }} 167 | - ${{ inputs.coverage-decrease-limit }} 168 | - ${{ inputs.branches-decrease-limit }} 169 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | 11 | unit-test: 12 | 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] 17 | 18 | steps: 19 | - uses: actions/checkout@v6 20 | 21 | - name: Setup Python 22 | uses: actions/setup-python@v6 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | 26 | - name: Run Python unit tests 27 | run: python3 -u -B -m unittest tests/tests.py 28 | 29 | 30 | cli-mode-tests: 31 | 32 | runs-on: ubuntu-latest 33 | strategy: 34 | matrix: 35 | python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] 36 | 37 | steps: 38 | - uses: actions/checkout@v6 39 | 40 | - name: Setup Python 41 | uses: actions/setup-python@v6 42 | with: 43 | python-version: ${{ matrix.python-version }} 44 | 45 | - name: Integration test of CLI use-case 46 | id: integrationCLI 47 | run: | 48 | export PYTHONPATH=$PWD/src 49 | echo $PYTHONPATH 50 | python3 -B -m jacoco_badge_generator --jacoco-csv-file tests/jacoco.csv --badges-directory tests/cli/badges --generate-branches-badge true 51 | python3 -B -m jacoco_badge_generator --jacoco-csv-file tests/multi1.csv tests/multi2.csv --badges-directory tests/cli/badges --generate-branches-badge true --coverage-badge-filename coverageMulti.svg --branches-badge-filename branchesMulti.svg 52 | python3 -B -m jacoco_badge_generator --jacoco-csv-file tests/jacoco.csv --badges-directory tests/cli/badgesJSON --generate-coverage-badge false --generate-coverage-endpoint true --generate-branches-endpoint true 53 | python3 -B -m jacoco_badge_generator --jacoco-csv-file tests/summaryReportTest.csv --badges-directory tests/cli/summary --generate-coverage-badge false --generate-summary true 54 | python3 -B -m jacoco_badge_generator --jacoco-csv-file tests/jacoco.csv --badges-directory tests/cli/badges --generate-branches-badge true --generate-coverage-endpoint true --generate-branches-endpoint true --coverage-badge-filename customCoverage.svg --branches-badge-filename customBranches.svg --coverage-endpoint-filename customCoverage.json --branches-endpoint-filename customBranches.json --coverage-label "custom coverage label one" --branches-label "custom coverage label two" 55 | python3 -B -m jacoco_badge_generator --jacoco-csv-file **/multi*.csv --badges-directory tests/glob/badges --generate-branches-badge true --coverage-badge-filename coverageMultiCLI.svg --branches-badge-filename branchesMultiCLI.svg 56 | 57 | - name: Verify integration test results 58 | run: python3 -u -B -m unittest tests/integration-cli-mode.py 59 | 60 | # This test can be used to test failing the workflow run on decrease. 61 | # Uncomment to use. Success is if this fails the workflow. 62 | # - name: Fail on decrease test 63 | # id: integrationFailDecreaseCLI 64 | # run: | 65 | # export PYTHONPATH=$PWD/src 66 | # echo $PYTHONPATH 67 | # python3 -B -m jacoco_badge_generator --jacoco-csv-file tests/summaryReportTest.csv --badges-directory tests --generate-coverage-badge false --generate-branches-badge false --generate-coverage-endpoint false --generate-branches-endpoint false --generate-summary true --summary-filename reportTestFail.json --fail-on-coverage-decrease true 68 | # python3 -B -m jacoco_badge_generator --jacoco-csv-file tests/summaryReportTest.csv --badges-directory tests --generate-coverage-badge false --generate-branches-badge false --generate-coverage-endpoint false --generate-branches-endpoint false --generate-summary true --summary-filename reportTestFail.json --fail-on-branches-decrease true 69 | # #python3 -B -m jacoco_badge_generator --jacoco-csv-file tests/summaryReportTest.csv --badges-directory tests --generate-coverage-badge false --generate-branches-badge false --generate-coverage-endpoint false --generate-branches-endpoint false --generate-summary true --summary-filename reportTestFail.json --fail-on-coverage-decrease true --coverage-decrease-limit 0.72 70 | # #python3 -B -m jacoco_badge_generator --jacoco-csv-file tests/summaryReportTest.csv --badges-directory tests --generate-coverage-badge false --generate-branches-badge false --generate-coverage-endpoint false --generate-branches-endpoint false --generate-summary true --summary-filename reportTestFail.json --fail-on-branches-decrease true --branches-decrease-limit 77 71 | 72 | 73 | actions-mode-tests: 74 | 75 | runs-on: ubuntu-latest 76 | 77 | steps: 78 | - uses: actions/checkout@v6 79 | 80 | - name: Setup Python 81 | uses: actions/setup-python@v6 82 | with: 83 | python-version: '3.14' 84 | 85 | - name: Verify that the Docker image for the action builds 86 | run: docker build . --file Dockerfile 87 | 88 | - name: Integration test with a single jacoco.csv 89 | id: integration1 90 | uses: ./ 91 | with: 92 | jacoco-csv-file: tests/jacoco.csv 93 | badges-directory: tests/badges 94 | generate-branches-badge: true 95 | 96 | - name: Log integration test outputs with a single jacoco.csv 97 | run: | 98 | echo "coverage = ${{ steps.integration1.outputs.coverage }}" 99 | echo "branch coverage = ${{ steps.integration1.outputs.branches }}" 100 | 101 | - name: Integration test with multiple jacoco.csv files 102 | id: integration2 103 | uses: ./ 104 | with: 105 | jacoco-csv-file: > 106 | tests/multi1.csv 107 | tests/multi2.csv 108 | badges-directory: tests/badges 109 | generate-branches-badge: true 110 | coverage-badge-filename: coverageMulti.svg 111 | branches-badge-filename: branchesMulti.svg 112 | workflow-summary-heading: "JaCoCo Test Coverage Summary: Multimodule Testcase" 113 | 114 | - name: Log integration test outputs with multiple jacoco.csv files 115 | run: | 116 | echo "coverage = ${{ steps.integration2.outputs.coverage }}" 117 | echo "branch coverage = ${{ steps.integration2.outputs.branches }}" 118 | 119 | - name: Integration endpoints test 120 | id: integration3 121 | uses: ./ 122 | with: 123 | jacoco-csv-file: tests/jacoco.csv 124 | badges-directory: tests/endpoints 125 | generate-coverage-badge: false 126 | generate-branches-badge: false 127 | generate-coverage-endpoint: true 128 | generate-branches-endpoint: true 129 | workflow-summary-heading: "JaCoCo Test Coverage Summary: Endpoints Testcase" 130 | 131 | - name: Log integration endpoints test outputs 132 | run: | 133 | echo "coverage = ${{ steps.integration3.outputs.coverage }}" 134 | echo "branch coverage = ${{ steps.integration3.outputs.branches }}" 135 | 136 | - name: Integration test for summary report 137 | id: integration4 138 | uses: ./ 139 | with: 140 | jacoco-csv-file: tests/summaryReportTest.csv 141 | badges-directory: tests/summary 142 | generate-coverage-badge: false 143 | generate-branches-badge: false 144 | generate-coverage-endpoint: false 145 | generate-branches-endpoint: false 146 | generate-summary: true 147 | workflow-summary-heading: "JaCoCo Test Coverage Summary: Summary Report Testcase" 148 | 149 | - name: Log integration summary report test outputs 150 | run: | 151 | echo "coverage = ${{ steps.integration4.outputs.coverage }}" 152 | echo "branch coverage = ${{ steps.integration4.outputs.branches }}" 153 | 154 | - name: Integration test with custom labels 155 | id: integration5 156 | uses: ./ 157 | with: 158 | jacoco-csv-file: tests/jacoco.csv 159 | badges-directory: tests/badges 160 | generate-branches-badge: true 161 | generate-coverage-endpoint: true 162 | generate-branches-endpoint: true 163 | coverage-badge-filename: customCoverage.svg 164 | branches-badge-filename: customBranches.svg 165 | coverage-endpoint-filename: customCoverage.json 166 | branches-endpoint-filename: customBranches.json 167 | coverage-label: custom coverage label one 168 | branches-label: custom coverage label two 169 | workflow-summary-heading: "JaCoCo Test Coverage Summary: Custom Labels Testcase" 170 | 171 | - name: Log integration test outputs with custom labels 172 | run: | 173 | echo "coverage = ${{ steps.integration5.outputs.coverage }}" 174 | echo "branch coverage = ${{ steps.integration5.outputs.branches }}" 175 | 176 | - name: Integration test with multiple csv files with glob 177 | id: integration6 178 | uses: ./ 179 | with: 180 | jacoco-csv-file: "**/multi*.csv" 181 | badges-directory: tests/glob/badges 182 | generate-branches-badge: true 183 | coverage-badge-filename: coverageMulti.svg 184 | branches-badge-filename: branchesMulti.svg 185 | workflow-summary-heading: "JaCoCo Test Coverage Summary: Glob Testcase" 186 | 187 | - name: Log integration test outputs with multiple csv files with glob 188 | run: | 189 | echo "coverage = ${{ steps.integration6.outputs.coverage }}" 190 | echo "branch coverage = ${{ steps.integration6.outputs.branches }}" 191 | 192 | - name: Verify integration test results 193 | run: python3 -u -B -m unittest tests/integration-actions-mode.py 194 | 195 | 196 | # This test can be used to test failing the workflow run on decrease. 197 | # Uncomment to use. Success is if this fails the workflow. 198 | # - name: Integration test for fail on decrease 199 | # id: integrationFailures 200 | # uses: ./ 201 | # with: 202 | # jacoco-csv-file: tests/summaryReportTest.csv 203 | # badges-directory: tests 204 | # generate-coverage-badge: false 205 | # generate-branches-badge: false 206 | # generate-coverage-endpoint: false 207 | # generate-branches-endpoint: false 208 | # generate-summary: true 209 | # summary-filename: reportTestFail.json 210 | # fail-on-coverage-decrease: true 211 | # fail-on-branches-decrease: true 212 | # #coverage-decrease-limit: 0.72 213 | # #branches-decrease-limit: 77 214 | -------------------------------------------------------------------------------- /src/jacoco_badge_generator/__main__.py: -------------------------------------------------------------------------------- 1 | # jacoco-badge-generator: Coverage badges, and pull request coverage checks, 2 | # from JaCoCo reports in GitHub Actions. 3 | # 4 | # Copyright (c) 2020-2025 Vincent A Cicirello 5 | # https://www.cicirello.org/ 6 | # 7 | # MIT License 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # 27 | 28 | import argparse 29 | from .coverage_badges import stringToPercentage 30 | from .coverage_badges import main 31 | 32 | if __name__ == "__main__" : 33 | # IMPORTANT: This is the entrypoint for the use-case of 34 | # running as a command-line utility only. 35 | # 36 | # This is the entry point when using the 37 | # jacoco-badge-generator as a command-line tool, 38 | # such as via a build script, locally (and not via 39 | # GitHub Actions). The source code for the entry 40 | # point for GitHub Actions is found at src/entrypoint.py, 41 | # although please see the project README.md for detailed 42 | # usage within an GitHub Actions workflow. 43 | 44 | print("jacoco-badge-generator: Generate coverage badges from JaCoCo coverage reports") 45 | print("Copyright (C) 2022-2025 Vincent A. Cicirello (https://www.cicirello.org/)") 46 | print("MIT License: https://github.com/cicirello/jacoco-badge-generator/blob/main/LICENSE") 47 | print() 48 | 49 | parser = argparse.ArgumentParser( 50 | prog="jacoco-badge-generator", 51 | description="Generate coverage badges from JaCoCo coverage reports. All parameters are optional, provided that the defaults meet your use-case." 52 | ) 53 | parser.add_argument( 54 | "-j", "--jacoco-csv-file", 55 | nargs='+', 56 | default=["target/site/jacoco/jacoco.csv"], 57 | help="Filename(s) with full path(s) relative to current working directory of one or more JaCoCo csv reports (default is target/site/jacoco/jacoco.csv).", 58 | dest="csvReports", 59 | metavar="report_files" 60 | ) 61 | parser.add_argument( 62 | "-d", "--badges-directory", 63 | default="badges", 64 | help="Directory for storing the badges relative to current working directory (default: badges), which will be created if it doesn't exist.", 65 | dest="badgesDirectory", 66 | metavar="badges-directory" 67 | ) 68 | parser.add_argument( 69 | "--generate-coverage-badge", 70 | default="true", 71 | help="Controls whether to generate the coverage badge (default: true).", 72 | dest="generateCoverageBadge", 73 | choices=['true', 'false'] 74 | ) 75 | parser.add_argument( 76 | "--coverage-badge-filename", 77 | default="jacoco.svg", 78 | help="Filename for the coverage badge (Instructions or C0 Coverage) (default: jacoco.svg), which will be created in the badges directory.", 79 | dest="coverageFilename", 80 | metavar="coverage-badge-filename" 81 | ) 82 | parser.add_argument( 83 | "--generate-branches-badge", 84 | default="false", 85 | help="Controls whether to generate the branches coverage badge (default: false).", 86 | dest="generateBranchesBadge", 87 | choices=['true', 'false'] 88 | ) 89 | parser.add_argument( 90 | "--branches-badge-filename", 91 | default="branches.svg", 92 | help="Filename for the branches coverage badge (C1 Coverage) (default: branches.svg), which will be created in the badges directory.", 93 | dest="branchesFilename", 94 | metavar="branches-badge-filename" 95 | ) 96 | parser.add_argument( 97 | "--generate-coverage-endpoint", 98 | default="false", 99 | help="Controls whether to generate a Shields JSON endpoint for coverage (default: false).", 100 | dest="generateCoverageJSON", 101 | choices=['true', 'false'] 102 | ) 103 | parser.add_argument( 104 | "--coverage-endpoint-filename", 105 | default="jacoco.json", 106 | help="Filename for the coverage Shields JSON endpoint (Instructions or C0 Coverage) (default: jacoco.json), which will be created in the badges directory.", 107 | dest="coverageJSON", 108 | metavar="coverage-endpoint-filename" 109 | ) 110 | parser.add_argument( 111 | "--generate-branches-endpoint", 112 | default="false", 113 | help="Controls whether to generate a Shields JSON endpoint for branches coverage (default: false).", 114 | dest="generateBranchesJSON", 115 | choices=['true', 'false'] 116 | ) 117 | parser.add_argument( 118 | "--branches-endpoint-filename", 119 | default="branches.json", 120 | help="Filename for the branches coverage Shields JSON endpoint (C1 Coverage) (default: branches.json), which will be created in the badges directory.", 121 | dest="branchesJSON", 122 | metavar="branches-endpoint-filename" 123 | ) 124 | parser.add_argument( 125 | "--generate-summary", 126 | default="false", 127 | help="Controls whether or not to generate a simple JSON summary report of the following form: {\"branches\": 77.77777777777779, \"coverage\": 72.72727272727273}. Default: false.", 128 | dest="generateSummary", 129 | choices=['true', 'false'] 130 | ) 131 | parser.add_argument( 132 | "--summary-filename", 133 | default="coverage-summary.json", 134 | help="Filename for the summary report (see above). Default: coverage-summary.json, and will be created within the badges directory.", 135 | dest="summaryFilename", 136 | metavar="summary-filename" 137 | ) 138 | parser.add_argument( 139 | "--coverage-label", 140 | default="coverage", 141 | help="Text for the label on the left side of the coverage badge. Default: coverage.", 142 | dest="coverageLabel", 143 | metavar="coverage-label" 144 | ) 145 | parser.add_argument( 146 | "--branches-label", 147 | default="branches", 148 | help="Text for the label on the left side of the branches coverage badge. Default: branches.", 149 | dest="branchesLabel", 150 | metavar="branches-label" 151 | ) 152 | parser.add_argument( 153 | "--colors", 154 | nargs="+", 155 | default=["#4c1", "#97ca00", "#a4a61d", "#dfb317", "#fe7d37", "#e05d44"], 156 | help="Badge colors in order corresponding to the order of the coverage percentages specified in the --intervals input. Colors can be specified with 6-digit hex, such as #97ca00, or 3-digit hex, such as #4c1, or as SVG named colors, such as green. Default: #4c1 #97ca00 #a4a61d #dfb317 #fe7d37 #e05d44. Depending upon your shell, you may need to escape the # characters or quote each color.", 157 | dest="colors", 158 | metavar="color" 159 | ) 160 | parser.add_argument( 161 | "--intervals", 162 | nargs="+", 163 | default=[100, 90, 80, 70, 60, 0], 164 | help="Percentages in decreasing order that serve as minimum needed for each color. Order corresponds to that of --colors input. Default: 100 90 80 70 60 0, which means coverage of 100 gets first color, at least 90 gets second color, etc.", 165 | dest="colorCutoffs", 166 | metavar="min-coverage-for-color", 167 | type=float 168 | ) 169 | parser.add_argument( 170 | "--on-missing-report", 171 | default="fail", 172 | help="Controls what happens if one or more jacoco.csv files do not exist (fail = output error and return non-zero exit code, quiet = exit silently without generating badges and exit code of 0, badges = generate badges from the csv files present (not recommended)). Default: fail.", 173 | dest="onMissingReport", 174 | choices=['fail', 'quiet', 'badges'] 175 | ) 176 | parser.add_argument( 177 | "--fail-if-coverage-less-than", 178 | default=0, 179 | help="Don't generate badges and return a non-zero exit code if coverage less than a minimum percentage, specified as value between 0.0 and 1.0, or as a percent (with or without the %% sign). E.g., 0.6, 60, and 60%% are all equivalent. Default: 0.", 180 | dest="minCoverage", 181 | metavar="min-coverage", 182 | type=lambda s : stringToPercentage(s) 183 | ) 184 | parser.add_argument( 185 | "--fail-if-branches-less-than", 186 | default=0, 187 | help="Don't generate badges and return a non-zero exit code if branches coverage less than a minimum percentage, specified as value between 0.0 and 1.0, or as a percent (with or without the %% sign). E.g., 0.6, 60, and 60%% are all equivalent. Default: 0.", 188 | dest="minBranches", 189 | metavar="min-branches", 190 | type=lambda s : stringToPercentage(s) 191 | ) 192 | parser.add_argument( 193 | "--fail-on-coverage-decrease", 194 | default="false", 195 | help="If true, will exit with non-zero error code if coverage is less than prior run as recorded in either the existing badge, the existing Shields endpoint, or the JSON summary report (default: false).", 196 | dest="failOnCoverageDecrease", 197 | choices=['true', 'false'] 198 | ) 199 | parser.add_argument( 200 | "--fail-on-branches-decrease", 201 | default="false", 202 | help="If true, will exit with non-zero error code if branches coverage is less than prior run as recorded in either the existing badge, the existing Shields endpoint, or the JSON summary report (default: false).", 203 | dest="failOnBranchesDecrease", 204 | choices=['true', 'false'] 205 | ) 206 | parser.add_argument( 207 | "--coverage-decrease-limit", 208 | default=1.0, 209 | help="Overrides fail-on-coverage-decrease when coverage is at least this limit", 210 | dest="coverageDecreaseLimit", 211 | type=lambda s : stringToPercentage(s) 212 | ) 213 | parser.add_argument( 214 | "--branches-decrease-limit", 215 | default=1.0, 216 | help="Overrides fail-on-branches-decrease when branches coverage is at least this limit", 217 | dest="branchesDecreaseLimit", 218 | type=lambda s : stringToPercentage(s) 219 | ) 220 | args = parser.parse_args() 221 | 222 | main( 223 | jacocoCsvFile = " ".join(args.csvReports), 224 | badgesDirectory = args.badgesDirectory, 225 | coverageFilename = args.coverageFilename, 226 | branchesFilename = args.branchesFilename, 227 | generateCoverageBadge = args.generateCoverageBadge == "true", 228 | generateBranchesBadge = args.generateBranchesBadge == "true", 229 | onMissingReport = args.onMissingReport, 230 | minCoverage = args.minCoverage, 231 | minBranches = args.minBranches, 232 | failOnCoverageDecrease = args.failOnCoverageDecrease == "true", 233 | failOnBranchesDecrease = args.failOnBranchesDecrease == "true", 234 | colorCutoffs = args.colorCutoffs, 235 | colors = args.colors, 236 | generateCoverageJSON = args.generateCoverageJSON == "true", 237 | generateBranchesJSON = args.generateBranchesJSON == "true", 238 | coverageJSON = args.coverageJSON, 239 | branchesJSON = args.branchesJSON, 240 | generateSummary = args.generateSummary == "true", 241 | summaryFilename = args.summaryFilename, 242 | appendWorkflowSummary = False, 243 | coverageLabel = args.coverageLabel, 244 | branchesLabel = args.branchesLabel, 245 | ghActionsMode = False, 246 | workflowJobSummaryHeading = "JaCoCo Test Coverage Summary", 247 | coverageDecreaseLimit = args.coverageDecreaseLimit, 248 | branchesDecreaseLimit = args.branchesDecreaseLimit 249 | ) 250 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] - 2025-12-08 8 | 9 | ### Added 10 | 11 | ### Changed 12 | 13 | ### Deprecated 14 | 15 | ### Removed 16 | 17 | ### Fixed 18 | 19 | ### Dependencies 20 | * Bump `cicirello/pyaction` to `3.14.1-gh-2.83.1` 21 | * Bump Python to 3.14 in the action's container 22 | * Bump minimum supported Python version to 3.10 for CLI-mode 23 | 24 | ### CI/CD 25 | 26 | 27 | ## [2.12.1] - 2025-07-16 28 | 29 | ### Dependencies (GitHub Actions Mode) 30 | * Updated Docker file to use new tag format for `cicirello/pyaction` 31 | * Bumps `cicirello/pyaction` to `3.13.5-gh-2.75.1` (Python 3.13.5). 32 | 33 | 34 | ## [2.12.0] - 2025-05-20 35 | 36 | ### Added 37 | * Added inputs for disabling failure on decrease when coverage is above a specified limit: 38 | * `coverage-decrease-limit`: Overrides `fail-on-coverage-decrease` when coverage is at least this limit 39 | * `branches-decrease-limit`: Overrides `fail-on-branches-decrease` when branches coverage is at least this limit 40 | 41 | ### Dependencies 42 | * Bump cicirello/pyaction from 4.23.0 to 4.33.0, including upgrading Python to 3.13 within the Docker container of the action. 43 | 44 | ### CI/CD 45 | * Bump Python to 3.13 in CI/CD workflows when running unit tests. 46 | * Matrix testing for Python versions 3.8 through 3.13 for unit tests. 47 | * Matrix testing the CLI mode for Python versions 3.8 through 3.13. 48 | 49 | 50 | ## [2.11.0] - 2023-09-15 51 | 52 | ### Added 53 | * Option to customize heading for GitHub Actions workflow job summary 54 | 55 | ### Dependencies 56 | * Bump cicirello/pyaction from 4.22.0 to 4.23.0 57 | 58 | 59 | ## [2.10.0] - 2023-09-04 60 | 61 | ### Added 62 | * Option to suppress workflow job summary in GitHub Actions Mode (#126). 63 | 64 | ### Dependencies 65 | * Bump cicirello/pyaction from 4.19.0 to 4.22.0 66 | 67 | 68 | ## [2.9.0] - 2023-05-24 69 | 70 | ### Added 71 | * Support for glob patterns in GitHub Actions mode for specifying multiple JaCoCo reports for multi-module projects (note: CLI mode already supported this indirectly since the shell expands globs automatically). 72 | 73 | ### Dependencies 74 | * Bump cicirello/pyaction from 4.11.1 to 4.19.0, including upgrading Python within the Docker container to 3.11. 75 | 76 | ### CI/CD 77 | * Bump Python to 3.11 in CI/CD workflows. 78 | 79 | 80 | ## [2.8.1] - 2022-10-24 81 | 82 | ### Fixed 83 | * The replacement for GitHub Action's deprecated `set-output` is not available yet for all self-hosted users. This patch 84 | handles that by using the new `$GITHUB_OUTPUT` environment file if it exists, and otherwise falling back to `set-output`. 85 | 86 | ### Dependencies 87 | * Bump cicirello/pyaction from 4.11.0 to 4.11.1 88 | 89 | 90 | ## [2.8.0] - 2022-10-21 91 | 92 | ### Added 93 | * Generate and output a GitHub Actions workflow job summary with the coverage percentages. 94 | 95 | ### Fixed 96 | * Replaced use of GitHub Action's deprecated `set-output` workflow command. 97 | 98 | ### Dependencies 99 | * Bump cicirello/pyaction from 4.6.0 to 4.11.0, which includes upgrading Python within the Docker container to 3.10.7. 100 | 101 | 102 | ## [2.7.0] - 2022-06-28 103 | 104 | ### Added 105 | * CLI Mode: Ability to run as a command-line utility outside of GitHub Actions, such as part of a local build script, etc. 106 | 107 | ### Changed 108 | * Refactored main control block to improve maintainability (#63). 109 | * Refactored organization of source files (#64). 110 | * Bumped base Docker image cicirello/pyaction from 4.1.0 to 4.6.0. 111 | 112 | ### CI/CD 113 | * Added workflow to automatically publish CLI utility to PyPI on new releases of GitHub Action to GitHub Marketplace. 114 | 115 | 116 | ## [2.6.1] - 2022-02-18 117 | 118 | ### Fixed 119 | * Suppressed Python's pycache on imports (fixes Issue #46). 120 | 121 | 122 | ## [2.6.0] - 2022-02-17 123 | 124 | ### Added 125 | * Option to specify custom labels for the left side of the badges controlled 126 | by the new inputs `coverage-label` and `branches-label`. 127 | 128 | ### Changed 129 | * Left-side text width and position calculated rather than hard-coded to 130 | width of "coverage" and "branches". 131 | * Changed Dockerfile to pull base image from GitHub Container Registry, assuming 132 | within GitHub Actions likely faster to pull from GitHub rather than Docker Hub. 133 | * Repository reorganized to move Python source code to a new src directory. 134 | 135 | 136 | ## [2.5.0] - 2021-11-11 137 | 138 | ### Added 139 | * Option to generate a simple JSON summary report containing the coverage 140 | and branches coverage values as double-precision floating-point values. 141 | This may be useful as input to other tools. Additionally, if used in 142 | combination with the existing `fail-on-coverage-decrease` and/or 143 | `fail-on-branches-decrease` features, those checks will be more accurate. 144 | This new feature is controlled by a pair of new inputs `generate-summary` 145 | and `summary-filename`. This feature is disabled by default. 146 | 147 | 148 | ## [2.4.1] - 2021-08-16 149 | 150 | ### Fixed 151 | * Visual improvements to right side of badges: 152 | * Adjusted calculation of text lengths for right side of badges 153 | for improved character spacing. 154 | * Badge width now also adjusted by the right side text lengths. 155 | 156 | 157 | ## [2.4.0] - 2021-08-13 158 | 159 | ### Added 160 | * Added an option to generate Shields.io JSON endpoints either in addition to, 161 | or instead of, directly generating badges. For most users, the existing direct 162 | generation of the badges is probably the preferred approach (e.g., probably 163 | faster serving when loading README, and much simpler insertion of badge into 164 | README). But for those who use one of Shields styles other than the default, 165 | and who would like to be able to match the coverage badges to the style of 166 | their project's other badges, then providing the ability to generate a 167 | Shields JSON endpoint gives them the ability to do so. The new feature is 168 | controlled by 4 new inputs: `generate-coverage-endpoint`, `generate-branches-endpoint`, 169 | `coverage-endpoint-filename`, and `branches-endpoint-filename`. All of these 170 | have default values and are optional. The current default behavior is retained, 171 | so by default the JSON endpoints are not generated. 172 | 173 | 174 | ## [2.3.0] - 2021-6-25 175 | 176 | ### Added 177 | * Customization of badge colors, using two new inputs (`colors` and 178 | `intervals`). The defaults for the new inputs produce badges with 179 | the existing color scheme. 180 | 181 | 182 | ## [2.2.1] - 2021-5-20 183 | 184 | ### Changed 185 | * Improved log messages related to the `fail-on-coverage-decrease` 186 | and `fail-on-branches-decrease` inputs. 187 | * Non-functional changes: Refactoring to improve maintainability. 188 | * Use major release tag when pulling base docker image (e.g., automatically get non-breaking 189 | changes to base image, such as bug fixes, etc without need to update Dockerfile). 190 | * Improved documentation of `fail-on-coverage-decrease` and `fail-if-coverage-less-than` inputs. 191 | 192 | 193 | ## [2.2.0] - 2021-5-8 194 | 195 | ### Added 196 | * A new optional input, `fail-if-coverage-less-than`, that 197 | enables failing the workflow run if coverage is below a 198 | user specified minimum. 199 | * A new optional input, `fail-if-branches-less-than`, that 200 | enables failing the workflow run if branches coverage is below a 201 | user specified minimum. 202 | * A new optional input, `fail-on-coverage-decrease`, that enables 203 | failing the workflow run if coverage decreased relative to previous run. 204 | * A new optional input, `fail-on-branches-decrease`, that enables 205 | failing the workflow run if branches coverage decreased relative to previous run. 206 | 207 | 208 | 209 | ## [2.1.2] - 2021-5-6 210 | 211 | ### CI/CD 212 | * Introduced major release tag, and automated tag update upon release. 213 | 214 | 215 | ## [2.1.1] - 2021-5-5 216 | 217 | ### Fixed 218 | * Previously, if any jacoco.csv files passed to the action were missing 219 | for any reason (e.g., typo in path or file name in workflow, or otherwise 220 | not generated by previous step of workflow), the action would simply fail 221 | resulting the the workflow run failing. Although in many cases this may be the 222 | desirable behavior, the action now logs the names of any missing jacoco 223 | report files to enable debugging what went wrong in a failed workflow run, 224 | and there is now an input, `on-missing-report`, that allows for specifying 225 | the behavior of the action in this case (e.g., user of the action can 226 | decide whether the workflow run should fail in this case). 227 | 228 | 229 | ## [2.1.0] - 2021-4-22 230 | 231 | ### Added 232 | * Added support for multi-module projects: The `jacoco-badge-generator` is now 233 | able to generate coverage badges (both instructions and branches) for a multi-module 234 | project, computing the coverage percentages from a combination of the data 235 | from the separate coverage reports generated by JaCoCo for the sub-projects. 236 | 237 | ### Changed 238 | * Updated example workflows to utilize the updated release of actions/setup-java. 239 | * Bumped base docker image to pyaction-lite, v3.13.5. 240 | 241 | ### CI/CD 242 | * Enabled CodeQL code scanning on all push/pull-request events. 243 | 244 | 245 | ## [2.0.1] - 2021-3-3 246 | 247 | ### Changed 248 | * Changed the tag used to pull the base docker image from `latest` 249 | to the specific version number that is the latest. The reason for this change 250 | is to ensure that we have the opportunity to test against updates to 251 | the base image before such changes affect releases. Using the `latest` 252 | tag when pulling the base image runs the risk of a change in the base 253 | image breaking the action (although this risk is small). 254 | 255 | ### Fixed 256 | * Fixed a bug related to permissions on the badges directory if it 257 | didn't already exist prior to running the action. Bug only appeared to 258 | exhibit itself if the `jacoco-badge-generator` was used in combination with 259 | version 3.6 or later of the `peter-evans/create-pull-request` action, and only 260 | if the badges directory didn't already exist. This bug is now resolved. 261 | 262 | 263 | ## [2.0.0] - 2021-2-15 264 | 265 | ### Compatibility Notes 266 | * If you are upgrading from an earlier version, and if 267 | you were not using the `jacoco-badge-file` input (deprecated 268 | since v1.2.0) to change the default location and name of the 269 | badge file, then you can simply upgrade to v2.0.0 without need 270 | to make any other changes to your workflow file. 271 | * If you have been using the deprecated `jacoco-badge-file` input 272 | to change the default location and name of the badge file, then 273 | you will need to instead use the `badges-directory` and 274 | the `coverage-badge-filename` inputs. 275 | 276 | ### Changed 277 | * The documentation has been significantly revised to provide more 278 | detail on the JaCoCo coverage metrics that are supported by 279 | the jacoco-badge-generator. 280 | 281 | ### Removed 282 | * Removed the previously deprecated input `jacoco-badge-file`. 283 | 284 | 285 | ## [1.2.1] - 2021-2-11 286 | 287 | ### Fixed 288 | * Corrected division by zero error in the case when there are 289 | either no instructions (e.g., running tests on an initially 290 | empty class) or no branches (e.g., no if statements or switch 291 | statements, etc). In such cases, badge generator will now 292 | compute 100% coverage (e.g., if there aren't any instructions 293 | to cover, your tests must have covered all 0 of the instructions). 294 | 295 | 296 | ## [1.2.0] - 2021-2-8 297 | 298 | ### Added 299 | * Generation of a branches coverage badge (in addition to 300 | the existing overall coverage badge). 301 | * Inputs for finer grained control over the behavior of the 302 | action (e.g., for controlling name and locations of generated 303 | badges, as well as for controlling which badges are generated). 304 | The default values for all of the new inputs are consistent 305 | with the behavior of the previous release. 306 | 307 | ### Deprecated 308 | * The `jacoco-badge-file` input is deprecated. In this release 309 | it still functions as in prior releases, but it will be removed 310 | in the next release. Users of the action should instead use the 311 | combination of the new inputs `badges-directory` and 312 | `coverage-badge-filename`. This change was made to simplify 313 | configuration of badge file names now that the action generates 314 | multiple badges. 315 | 316 | 317 | ## [1.1.0] - 2021-2-5 318 | 319 | ### Added 320 | * An additional action output for the percentage of branches 321 | covered. A future release will provide an option to generate 322 | an additional badge for this. 323 | 324 | 325 | ## [1.0.0] - 2020-10-21 326 | 327 | ### Initial release 328 | * The jacoco-badge-generator GitHub Action parses a `jacoco.csv` 329 | from a Jacoco coverage report, computes the coverage percentage, 330 | and generates a badge to provide an easy to read visual summary 331 | of the code coverage of your test cases. The badge that is 332 | generated is inspired by the style (including color palette) of 333 | the badges of Shields.io, however, the badge is entirely 334 | generated within the jacoco-badge-generator GitHub Action, 335 | with no external calls. 336 | -------------------------------------------------------------------------------- /src/jacoco_badge_generator/coverage_badges.py: -------------------------------------------------------------------------------- 1 | # jacoco-badge-generator: Coverage badges, and pull request coverage checks, 2 | # from JaCoCo reports in GitHub Actions. 3 | # 4 | # Copyright (c) 2020-2025 Vincent A Cicirello 5 | # https://www.cicirello.org/ 6 | # 7 | # MIT License 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # 27 | 28 | import csv 29 | import sys 30 | import math 31 | import pathlib 32 | import os 33 | import os.path 34 | import json 35 | from .text_length import calculateTextLength110 36 | from glob import glob 37 | 38 | badgeTemplate = '\ 40 | \ 41 | \ 42 | \ 43 | \ 44 | \ 45 | \ 46 | \ 47 | \ 50 | \ 52 | {3}\ 54 | {0}\ 58 | ' 59 | 60 | defaultColors = [ "#4c1", "#97ca00", "#a4a61d", "#dfb317", "#fe7d37", "#e05d44" ] 61 | 62 | markdownSummaryTemplate = """## {2} 63 | * __Coverage:__ {0:.3%} 64 | * __Branches:__ {1:.3%} 65 | * __Generated by:__ jacoco-badge-generator 66 | """ 67 | 68 | def generateBadge(covStr, color, badgeType="coverage") : 69 | """Generates the badge as a string. This includes calculating 70 | label width, etc, to support custom labels. 71 | 72 | Keyword arguments: 73 | covStr - The coverage as a string. 74 | color - The color for the badge. 75 | badgeType - The text string for a label on the badge. 76 | """ 77 | # textLength for coverage percentage string computed 78 | # assuming DejaVu Sans, 110pt font. 79 | textLength = calculateTextLength110(covStr) 80 | labelTextLength = calculateTextLength110(badgeType) 81 | rightWidth = math.ceil(textLength / 10) + 10 82 | leftWidth = math.ceil(labelTextLength / 10) + 10 83 | badgeWidth = leftWidth + rightWidth 84 | # The -10 below is for an exta buffer on right end of badge 85 | rightCenter = 10 * leftWidth + rightWidth * 5 - 10 86 | # The +10 below is for an extra buffer on left end of badge 87 | leftCenter = 10 + leftWidth * 5 88 | return badgeTemplate.format( 89 | covStr, #0 90 | color, #1 91 | textLength, #2 92 | badgeType, #3 93 | labelTextLength, #4 94 | rightWidth, #5 95 | badgeWidth, #6 96 | rightCenter, #7 97 | leftWidth, #8 98 | leftCenter #9 99 | ) 100 | 101 | def generateDictionaryForEndpoint(covStr, color, badgeType) : 102 | """Generated a Python dictionary containing all of the required 103 | fields for a Shields.io JSON endpoint. 104 | 105 | Keyword arguments: 106 | covStr - The coverage as a string. 107 | color - The color for the badge. 108 | badgeType - The text string for a label on the badge. 109 | """ 110 | return { 111 | "schemaVersion" : 1, 112 | "label" : badgeType, 113 | "message" : covStr, 114 | "color" : color 115 | } 116 | 117 | def computeCoverage(fileList) : 118 | """Parses one or more jacoco.csv files and computes code coverage 119 | percentages. Returns: coverage, branchCoverage. The coverage 120 | is instruction coverage. 121 | 122 | Keyword arguments: 123 | fileList - A list (or any iterable) of the filenames, including path, of the jacoco.csv files. 124 | """ 125 | missed = 0 126 | covered = 0 127 | missedBranches = 0 128 | coveredBranches = 0 129 | for filename in fileList : 130 | with open(filename, newline='') as csvfile : 131 | jacocoReader = csv.reader(csvfile) 132 | for i, row in enumerate(jacocoReader) : 133 | if i > 0 : 134 | missed += int(row[3]) 135 | covered += int(row[4]) 136 | missedBranches += int(row[5]) 137 | coveredBranches += int(row[6]) 138 | return (calculatePercentage(covered, missed), 139 | calculatePercentage(coveredBranches, missedBranches)) 140 | 141 | def calculatePercentage(covered, missed) : 142 | """Calculates the coverage percentage from number of 143 | covered and number of missed. Returns 1 if both are 0 144 | to handle the special case of running on an empty class 145 | (no instructions) or a case with no if, switch, loops (no 146 | branches). 147 | 148 | Keyword arguments: 149 | covered - The number of X covered (where X is the metric). 150 | missed - The number of X missed (where X is the metric). 151 | """ 152 | if missed == 0 : 153 | return 1 154 | return covered / (covered + missed) 155 | 156 | def coverageTruncatedToString(coverage) : 157 | """Converts the coverage percentage to a formatted string. 158 | Returns: coveragePercentageAsString, coverageTruncatedToOneDecimalPlace 159 | 160 | Keyword arguments: 161 | coverage - The coverage percentage. 162 | """ 163 | # Truncate the 2nd decimal place, rather than rounding 164 | # to avoid considering a non-passing percentage as 165 | # passing (e.g., if user considers 70% as passing threshold, 166 | # then 69.99999...% is technically not passing). 167 | coverage = int(1000 * coverage) / 10 168 | if coverage - int(coverage) == 0 : 169 | covStr = "{0:d}%".format(int(coverage)) 170 | else : 171 | covStr = "{0:.1f}%".format(coverage) 172 | return covStr, coverage 173 | 174 | def badgeCoverageStringColorPair(coverage, cutoffs=[100, 90, 80, 70, 60], colors=[]) : 175 | """Converts the coverage percentage to a formatted string, 176 | and determines the badge color. 177 | Returns: coveragePercentageAsString, colorAsString 178 | 179 | Keyword arguments: 180 | coverage - The coverage percentage. 181 | cutoffs - List of percentages that begin begin each color interval. 182 | colors - List of badge colors in decreasing order of coverage percentages. 183 | """ 184 | if len(colors) == 0 : 185 | colors = defaultColors 186 | cov, coverage = coverageTruncatedToString(coverage) 187 | c = computeColorIndex(coverage, cutoffs, len(colors)) 188 | return cov, colors[c] 189 | 190 | def computeColorIndex(coverage, cutoffs, numColors) : 191 | """Computes index into color list from coverage. 192 | 193 | Keyword arguments: 194 | coverage - The coverage percentage. 195 | cutoffs - The thresholds for each color. 196 | """ 197 | numIntervals = min(numColors, len(cutoffs)+1) 198 | for c in range(numIntervals-1) : 199 | if coverage >= cutoffs[c] : 200 | return c 201 | return numIntervals-1 202 | 203 | def createOutputDirectories(badgesDirectory) : 204 | """Creates the output directory if it doesn't already exist. 205 | 206 | Keyword arguments: 207 | badgesDirectory - The badges directory 208 | """ 209 | if not os.path.exists(badgesDirectory) : 210 | p = pathlib.Path(badgesDirectory) 211 | os.umask(0) 212 | p.mkdir(mode=0o777, parents=True, exist_ok=True) 213 | 214 | def splitPath(filenameWithPath) : 215 | """Breaks a filename including path into containing directory and filename. 216 | 217 | Keyword arguments: 218 | filenameWithPath - The filename including path. 219 | """ 220 | if filenameWithPath.startswith("./") : 221 | filenameWithPath = filenameWithPath[2:] 222 | if filenameWithPath[0] == "/" : 223 | filenameWithPath = filenameWithPath[1:] 224 | i = filenameWithPath.rfind("/") 225 | if i >= 0 : 226 | return filenameWithPath[:i], filenameWithPath[i+1:] 227 | else : 228 | return ".", filenameWithPath 229 | 230 | def formFullPathToFile(directory, filename) : 231 | """Generates path string. 232 | 233 | Keyword arguments: 234 | directory - The directory for the badges 235 | filename - The filename for the badge. 236 | """ 237 | if len(filename) > 1 and filename[0:2] == "./" : 238 | filename = filename[2:] 239 | if filename[0] == "/" : 240 | filename = filename[1:] 241 | if len(directory) > 1 and directory[0:2] == "./" : 242 | directory = directory[2:] 243 | if len(directory) > 0 and directory[0] == "/" : 244 | directory = directory[1:] 245 | if directory == "" or directory == "." : 246 | return filename 247 | elif directory[-1] == "/" : 248 | return directory + filename 249 | else : 250 | return directory + "/" + filename 251 | 252 | def filterMissingReports(jacocoFileList, failIfMissing=False) : 253 | """Validates report file existence, and returns a list 254 | containing a subset of the report files that exist and a boolean 255 | flag that will be True if any reports were missing or False 256 | otherwise. Logs files that don't exist to the console as warnings. 257 | 258 | Keyword arguments: 259 | jacocoFileList - A list of jacoco.csv files or glob patterns specifying such files. 260 | failIfMissing - If true and if any of the jacoco.csv files 261 | don't exist, then it will exit with a non-zero exit code causing 262 | workflow to fail. 263 | """ 264 | goodReports = [] 265 | isMissing = False 266 | for f in jacocoFileList : 267 | files = glob(f, recursive=True) 268 | for report in files: 269 | goodReports.append(report) 270 | if len(files) == 0: 271 | isMissing = True 272 | print("WARNING: Report file", f, "does not exist or glob", f, "is empty.") 273 | if len(goodReports) == 0 : 274 | print("WARNING: No JaCoCo csv reports found.") 275 | if failIfMissing : 276 | sys.exit(1) 277 | if failIfMissing and isMissing : 278 | sys.exit(1) 279 | return goodReports, isMissing 280 | 281 | def stringToPercentage(s) : 282 | """Converts a string describing a percentage to 283 | a float. The string s can be of any of the following 284 | forms: 60.2%, 60.2, or 0.602. All three of these will 285 | be treated the same. Without the percent sign, it is 286 | treated the same as with the percent sign if the value 287 | is greater than 1. This is to gracefully handle 288 | user misinterpretation of action input specification. In all cases, 289 | this function will return a float in the interval [0.0, 1.0]. 290 | 291 | Keyword arguments: 292 | s - the string to convert. 293 | """ 294 | if len(s)==0 : 295 | return 0 296 | doDivide = False 297 | if s[-1]=="%" : 298 | s = s[:-1].strip() 299 | if len(s)==0 : 300 | return 0 301 | doDivide = True 302 | try : 303 | p = float(s) 304 | except ValueError : 305 | return 0 306 | if p > 1 : 307 | doDivide = True 308 | return p / 100 if doDivide else p 309 | 310 | def coverageIsFailing(coverage, branches, minCoverage, minBranches) : 311 | """Checks if coverage or branchs coverage or both are 312 | below minimum to pass workflow run. Logs messages if it is. 313 | Actual failing behavior should be handled by caller. 314 | 315 | Keyword arguments: 316 | coverage - instructions coverage in interval 0.0 to 1.0. 317 | branches - branches coverage in interval 0.0 to 1.0. 318 | minCoverage - minimum instructions coverage to pass in interval 0.0 to 1.0. 319 | minBranches - minimum branches coverage to pass in interval 0.0 to 1.0. 320 | """ 321 | shouldFail = False 322 | if coverage < minCoverage : 323 | shouldFail = True 324 | print("Coverage of", coverage, "is below passing threshold of", minCoverage) 325 | if branches < minBranches : 326 | shouldFail = True 327 | print("Branches of", branches, "is below passing threshold of", minBranches) 328 | return shouldFail 329 | 330 | def getPriorCoverage(badgeFilename, whichBadge) : 331 | """Parses an existing badge (if one exists) returning 332 | the coverage percentage stored there. Returns -1 if 333 | badge file doesn't exist or if it isn't of the expected format. 334 | 335 | Keyword arguments: 336 | badgeFilename - the filename with path 337 | whichBadge - the badge label such as 'coverage' or 'branches' or some custom label 338 | """ 339 | if not os.path.isfile(badgeFilename) : 340 | return -1 341 | with open(badgeFilename, "r") as f : 342 | priorBadge = f.read() 343 | i = priorBadge.find(whichBadge) 344 | if i < 0 : 345 | return -1 346 | i += len(whichBadge) + 1 347 | j = priorBadge.find("%", i) 348 | if j < 0 : 349 | return -1 350 | return stringToPercentage(priorBadge[i:j+1].strip()) 351 | 352 | def getPriorCoverageFromEndpoint(jsonFilename, whichBadge) : 353 | """Parses an existing JSON endpoint (if one exists) returning 354 | the coverage percentage stored there. Returns -1 if 355 | file doesn't exist or if it isn't of the expected format. 356 | 357 | Keyword arguments: 358 | jsonFilename - the filename with path 359 | whichBadge - the badge label such as 'coverage' or 'branches' or some custom label 360 | """ 361 | if not os.path.isfile(jsonFilename) : 362 | return -1 363 | try : 364 | with open(jsonFilename, "r") as f : 365 | priorEndpoint = json.load(f) 366 | except : 367 | return -1 368 | if "message" not in priorEndpoint : 369 | return -1 370 | if "label" not in priorEndpoint : 371 | return -1 372 | if priorEndpoint["label"] != whichBadge : 373 | return -1 374 | return stringToPercentage(priorEndpoint["message"].strip()) 375 | 376 | def coverageDecreased(coverage, badgeFilename, whichBadge) : 377 | """Checks if coverage decreased relative to previous run, and logs 378 | a message if it did. 379 | 380 | Keyword arguments: 381 | coverage - The coverage in interval 0.0 to 1.0 382 | badgeFilename - the filename with path 383 | whichBadge - the badge label 384 | """ 385 | previous = getPriorCoverage(badgeFilename, whichBadge) 386 | # Badge only records 1 decimal place, and thus need 387 | # to take care to avoid floating-point rounding error 388 | # when old is converted to [0.0 to 1.0] range with div by 389 | # 100 in getPriorCoverage. e.g., 99.9 / 100 = 0.9990000000000001 390 | # due to rounding error. 391 | old = round(previous * 1000) 392 | # Don't need to round with new since this is still as computed 393 | # from coverage data at this point. 394 | new = coverage * 1000 395 | if new < old : 396 | print(whichBadge, "decreased from", coverageTruncatedToString(previous)[0], "to", coverageTruncatedToString(coverage)[0]) 397 | return True 398 | return False 399 | 400 | def coverageDecreasedEndpoint(coverage, jsonFilename, whichBadge) : 401 | """Checks if coverage decreased relative to previous run, and logs 402 | a message if it did. 403 | 404 | Keyword arguments: 405 | coverage - The coverage in interval 0.0 to 1.0 406 | jsonFilename - the filename with path 407 | whichBadge - the badge label 408 | """ 409 | previous = getPriorCoverageFromEndpoint(jsonFilename, whichBadge) 410 | # Badge only records 1 decimal place, and thus need 411 | # to take care to avoid floating-point rounding error 412 | # when old is converted to [0.0 to 1.0] range with div by 413 | # 100 in getPriorCoverage. e.g., 99.9 / 100 = 0.9990000000000001 414 | # due to rounding error. 415 | old = round(previous * 1000) 416 | # Don't need to round with new since this is still as computed 417 | # from coverage data at this point. 418 | new = coverage * 1000 419 | if new < old : 420 | print(whichBadge, "decreased from", coverageTruncatedToString(previous)[0], "to", coverageTruncatedToString(coverage)[0]) 421 | return True 422 | return False 423 | 424 | def coverageDecreasedSummary(checkCoverage, checkBranches, jsonFile, coverage, branches) : 425 | """Uses a summary report JSON file for the decreased coverage checks. 426 | Returns true if workflow should fail, and also logs appropriate message. 427 | 428 | Keyword arguments: 429 | checkCoverage - If true, check if coverage decreased. 430 | checkBranches - If true, check if branches coverage decreased. 431 | jsonFile - The summary report including full path. 432 | coverage - The instructions coverage in interval [0.0, 1.0]. 433 | branches - The branches coverage in interval [0.0, 1.0]. 434 | """ 435 | if not os.path.isfile(jsonFile) : 436 | return False 437 | try : 438 | with open(jsonFile, "r") as f : 439 | priorCoverage = json.load(f) 440 | except : 441 | return False 442 | result = False 443 | if checkCoverage and "coverage" in priorCoverage and 100*coverage < priorCoverage["coverage"] : 444 | print("Coverage decreased from", priorCoverage["coverage"], "to", 100*coverage) 445 | result = True 446 | if checkBranches and "branches" in priorCoverage and 100*branches < priorCoverage["branches"] : 447 | print("Branches coverage decreased from", priorCoverage["branches"], "to", 100*branches) 448 | result = True 449 | return result 450 | 451 | def colorCutoffsStringToNumberList(strCutoffs) : 452 | """Converts a string of space or comma separated percentages 453 | to a list of floats. 454 | 455 | Keyword arguments: 456 | strCutoffs - a string of space or comma separated percentages 457 | """ 458 | return list(map(float, strCutoffs.replace(',', ' ').split())) 459 | 460 | def coverageDictionary(cov, branches) : 461 | """Creates a dictionary with the coverage and branches coverage 462 | as double-precision floating-point values, specifically the raw 463 | computed values prior to truncation. Enables more accurate implementation 464 | of fail on decrease. Coverages are reported in interval [0.0, 100.0]. 465 | 466 | Keyword arguments: 467 | cov - Instruction coverage in interval [0.0, 1.0] 468 | branches - Branches coverage in interval [0.0, 1.0] 469 | """ 470 | return { "coverage" : 100 * cov, "branches" : 100 * branches } 471 | 472 | def set_action_outputs(output_pairs, ghActionsMode) : 473 | """Sets the GitHub Action outputs if running as a GitHub Action, 474 | and otherwise logs coverage percentages to terminal if running in 475 | CLI mode. Note that if the CLI mode is used within a GitHub Actions 476 | workflow, it will be treated the same as GitHub Actions mode. 477 | 478 | Temporary continued support for deprecated set-output for self-hosted 479 | GitHub users with versions that don't have the new GITHUB_OUTPUT env 480 | variable. Detected if running in GitHub Actions mode, but such that 481 | GITHUB_OUTPUT doesn't exist. 482 | 483 | Keyword arguments: 484 | output_pairs - Dictionary of outputs with values 485 | ghActionsMode - True if running as a GitHub Action, otherwise pass False 486 | """ 487 | if "GITHUB_OUTPUT" in os.environ : 488 | with open(os.environ["GITHUB_OUTPUT"], "a") as f : 489 | for key, value in output_pairs.items() : 490 | print("{0}={1}".format(key, value), file=f) 491 | else : 492 | output_template = "::set-output name={0}::{1}" if ghActionsMode else "{0}={1}" 493 | for key, value in output_pairs.items() : 494 | print(output_template.format(key, value)) 495 | 496 | def add_workflow_job_summary(cov, branches, heading) : 497 | """When running as a GitHub Action, adds a job summary, and otherwise 498 | does nothing when running in CLI mode. Note that if the CLI mode is 499 | used within a GitHub Actions workflow, it will be treated the same as 500 | GitHub Actions mode (no convenient way to tell the difference). 501 | 502 | Keyword arguments: 503 | cov - Coverage percentage 504 | branches - Branches coverage percentage 505 | heading - the heading for the GitHub workflow job summary 506 | """ 507 | if "GITHUB_STEP_SUMMARY" in os.environ : 508 | with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as f : 509 | print( 510 | markdownSummaryTemplate.format(cov, branches, heading), 511 | file=f) 512 | 513 | def main(jacocoCsvFile, 514 | badgesDirectory, 515 | coverageFilename, 516 | branchesFilename, 517 | generateCoverageBadge, 518 | generateBranchesBadge, 519 | onMissingReport, 520 | minCoverage, 521 | minBranches, 522 | failOnCoverageDecrease, 523 | failOnBranchesDecrease, 524 | colorCutoffs, 525 | colors, 526 | generateCoverageJSON, 527 | generateBranchesJSON, 528 | coverageJSON, 529 | branchesJSON, 530 | generateSummary, 531 | summaryFilename, 532 | appendWorkflowSummary, 533 | coverageLabel, 534 | branchesLabel, 535 | ghActionsMode, 536 | workflowJobSummaryHeading, 537 | coverageDecreaseLimit, 538 | branchesDecreaseLimit) : 539 | """Main function. 540 | 541 | Keyword arguments: 542 | jacocoCsvFile - Full path(s), relative to the root of the 543 | repository, to the jacoco csv file(s), including filename(s), 544 | delimited by whitespace if more than one. 545 | badgesDirectory - The directory for storing badges, relative to the root 546 | of the repository. 547 | coverageFilename - The filename of the coverage badge. 548 | branchesFilename - The filename of the branches coverage badge. 549 | generateCoverageBadge - Boolean to control whether or not to generate the 550 | coverage badge. 551 | generateBranchesBadge - Boolean to control whether or not to generate the 552 | branches coverage badge. 553 | onMissingReport - One of "fail", "quiet", or "badges" to controls what happens 554 | if one or more jacoco.csv files do not exist. 555 | minCoverage - Fails run if coverage is less than this percentage, specified as a 556 | floating-point value in the interval [0.0, 1.0]. 557 | minBranches - Fails run if branches coverage is less than this percentage, 558 | specified as a floating-point value in the interval [0.0, 1.0]. 559 | failOnCoverageDecrease - Boolean used to control whether or not to fail the 560 | run if coverage decreased relative to prior run. 561 | failOnBranchesDecrease - Boolean used to control whether or not to fail the 562 | run if branches coverage decreased relative to prior run. 563 | colorCutoffs - A list in descending order of coverage percents (as floats) for 564 | start of each color range. 565 | colors - A list of colors (each as a string), with position in list corresponding 566 | to percentages in above list of coverage percents. 567 | generateCoverageJSON - Boolean to control whether or not to generate the 568 | coverage JSON endpoint. 569 | generateBranchesJSON - Boolean to control whether or not to generate the 570 | branches coverage JSON endpoint. 571 | coverageJSON - The filename of the coverage JSON endpoint. 572 | branchesJSON - The filename of the branches coverage JSON endpoint. 573 | generateSummary - Boolean to control whether or not to generate a JSON file 574 | containing the coverage percentages as floating-point values. 575 | summaryFilename - The filename of the summary file. 576 | appendWorkflowSummary - If True and if running in GitHub Actions, logs coverage 577 | percentages to workflow job summary. 578 | coverageLabel - Text for the left-side of the coverage badge. 579 | branchesLabel - Text for the left-side of the branches coverage badge. 580 | ghActionsMode - True if running in GitHub Actions mode, or False otherwise. 581 | workflowJobSummaryHeading - the heading for the GitHub workflow job summary 582 | (ignored when run in CLI mode). 583 | coverageDecreaseLimit - Overrides fail-on-coverage-decrease when coverage is at 584 | least this limit, specified as a floating-point value in the interval [0.0, 1.0]. 585 | branchesDecreaseLimit - Overrides fail-on-branches-decrease when branches 586 | coverage is at least this limit, specified as a floating-point value in the 587 | interval [0.0, 1.0]. 588 | """ 589 | if onMissingReport not in {"fail", "quiet", "badges"} : 590 | print("ERROR: Invalid value for on-missing-report.") 591 | sys.exit(1) 592 | 593 | if len(badgesDirectory) > 1 and badgesDirectory[0:2] == "./" : 594 | badgesDirectory = badgesDirectory[2:] 595 | if len(badgesDirectory) > 0 and badgesDirectory[0] == "/" : 596 | badgesDirectory = badgesDirectory[1:] 597 | if badgesDirectory == "." : 598 | badgesDirectory = "" 599 | 600 | jacocoFileList = jacocoCsvFile.split() 601 | filteredFileList, isMissing = filterMissingReports(jacocoFileList, onMissingReport=="fail") 602 | 603 | noReportsMissing = not isMissing 604 | 605 | if len(filteredFileList) > 0 and (noReportsMissing or onMissingReport!="quiet") : 606 | 607 | cov, branches = computeCoverage(filteredFileList) 608 | 609 | # Disable fail on decrease when coverage is at least the limit 610 | failOnCoverageDecrease = failOnCoverageDecrease and cov < coverageDecreaseLimit 611 | failOnBranchesDecrease = failOnBranchesDecrease and branches < branchesDecreaseLimit 612 | 613 | if coverageIsFailing(cov, branches, minCoverage, minBranches) : 614 | print("Failing the workflow run.") 615 | sys.exit(1) 616 | 617 | coverageBadgeWithPath = formFullPathToFile(badgesDirectory, coverageFilename) 618 | branchesBadgeWithPath = formFullPathToFile(badgesDirectory, branchesFilename) 619 | coverageJSONWithPath = formFullPathToFile(badgesDirectory, coverageJSON) 620 | branchesJSONWithPath = formFullPathToFile(badgesDirectory, branchesJSON) 621 | summaryFilenameWithPath = formFullPathToFile(badgesDirectory, summaryFilename) 622 | 623 | # If using the fail on decrease options, in combination with the summary report, use summary 624 | # report for the check since it is more accurate. 625 | if ((failOnCoverageDecrease or failOnBranchesDecrease) and 626 | generateSummary and 627 | os.path.isfile(summaryFilenameWithPath)): 628 | if coverageDecreasedSummary(failOnCoverageDecrease, failOnBranchesDecrease, summaryFilenameWithPath, cov, branches) : 629 | print("Failing the workflow run.") 630 | sys.exit(1) 631 | else : # Otherwise use the prior coverages as stored in badges / JSON. 632 | if (failOnCoverageDecrease and 633 | generateCoverageBadge and 634 | coverageDecreased(cov, coverageBadgeWithPath, coverageLabel)): 635 | print("Failing the workflow run.") 636 | sys.exit(1) 637 | if (failOnBranchesDecrease and 638 | generateBranchesBadge and 639 | coverageDecreased(branches, branchesBadgeWithPath, branchesLabel)): 640 | print("Failing the workflow run.") 641 | sys.exit(1) 642 | if (failOnCoverageDecrease and 643 | generateCoverageJSON and 644 | coverageDecreasedEndpoint(cov, coverageJSONWithPath, coverageLabel)): 645 | print("Failing the workflow run.") 646 | sys.exit(1) 647 | if (failOnBranchesDecrease and 648 | generateBranchesJSON and 649 | coverageDecreasedEndpoint(branches, branchesJSONWithPath, branchesLabel)): 650 | print("Failing the workflow run.") 651 | sys.exit(1) 652 | 653 | if (generateSummary or generateCoverageBadge or generateBranchesBadge or generateCoverageJSON or generateBranchesJSON) and badgesDirectory != "" : 654 | createOutputDirectories(badgesDirectory) 655 | 656 | if generateCoverageBadge or generateCoverageJSON : 657 | covStr, color = badgeCoverageStringColorPair(cov, colorCutoffs, colors) 658 | if generateCoverageBadge : 659 | with open(coverageBadgeWithPath, "w") as badge : 660 | badge.write(generateBadge(covStr, color, coverageLabel)) 661 | if generateCoverageJSON : 662 | with open(coverageJSONWithPath, "w") as endpoint : 663 | json.dump(generateDictionaryForEndpoint(covStr, color, coverageLabel), endpoint, sort_keys=True) 664 | 665 | if generateBranchesBadge or generateBranchesJSON : 666 | covStr, color = badgeCoverageStringColorPair(branches, colorCutoffs, colors) 667 | if generateBranchesBadge : 668 | with open(branchesBadgeWithPath, "w") as badge : 669 | badge.write(generateBadge(covStr, color, branchesLabel)) 670 | if generateBranchesJSON : 671 | with open(branchesJSONWithPath, "w") as endpoint : 672 | json.dump(generateDictionaryForEndpoint(covStr, color, branchesLabel), endpoint, sort_keys=True) 673 | 674 | if generateSummary : 675 | with open(summaryFilenameWithPath, "w") as summaryFile : 676 | json.dump(coverageDictionary(cov, branches), summaryFile, sort_keys=True) 677 | 678 | set_action_outputs( 679 | {"coverage" : cov, "branches" : branches}, 680 | ghActionsMode 681 | ) 682 | 683 | if appendWorkflowSummary : 684 | add_workflow_job_summary(cov, branches, workflowJobSummaryHeading) 685 | 686 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | # jacoco-badge-generator: Github action for generating a jacoco coverage 2 | # percentage badge. 3 | # 4 | # Copyright (c) 2020-2023 Vincent A Cicirello 5 | # https://www.cicirello.org/ 6 | # 7 | # MIT License 8 | # 9 | # Permission is hereby granted, free of charge, to any person obtaining a copy 10 | # of this software and associated documentation files (the "Software"), to deal 11 | # in the Software without restriction, including without limitation the rights 12 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | # copies of the Software, and to permit persons to whom the Software is 14 | # furnished to do so, subject to the following conditions: 15 | # 16 | # The above copyright notice and this permission notice shall be included in all 17 | # copies or substantial portions of the Software. 18 | # 19 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | # SOFTWARE. 26 | # 27 | 28 | import unittest 29 | import json 30 | 31 | import sys 32 | sys.path.insert(0,'src') 33 | import jacoco_badge_generator.coverage_badges as jbg 34 | import jacoco_badge_generator.text_length as textLength 35 | 36 | class TestJacocoBadgeGenerator(unittest.TestCase) : 37 | 38 | def testTextLength(self) : 39 | self.assertEqual(510, textLength.calculateTextLength110("coverage")) 40 | self.assertEqual(507, textLength.calculateTextLength110("branches")) 41 | self.assertAlmostEqual(51.0, textLength.calculateTextLength("coverage", 11, False, 400)) 42 | self.assertAlmostEqual(50.7, textLength.calculateTextLength("branches", 11, False, 400)) 43 | self.assertAlmostEqual(510, textLength.calculateTextLength("coverage", 146 + 2/3, True, 400)) 44 | self.assertAlmostEqual(507, textLength.calculateTextLength("branches", 146 + 2/3, True, 400)) 45 | self.assertAlmostEqual(51.0, textLength.calculateTextLength("coverage", 14 + 2/3, True, 400)) 46 | self.assertAlmostEqual(50.7, textLength.calculateTextLength("branches", 14 + 2/3, True, 400)) 47 | self.assertAlmostEqual(76.5, textLength.calculateTextLength("coverage", 11, False, 600)) 48 | self.assertAlmostEqual(76.05, textLength.calculateTextLength("branches", 11, False, 600)) 49 | self.assertAlmostEqual(765, textLength.calculateTextLength("coverage", 146 + 2/3, True, 600)) 50 | self.assertAlmostEqual(760.5, textLength.calculateTextLength("branches", 146 + 2/3, True, 600)) 51 | self.assertAlmostEqual(76.5, textLength.calculateTextLength("coverage", 14 + 2/3, True, 600)) 52 | self.assertAlmostEqual(76.05, textLength.calculateTextLength("branches", 14 + 2/3, True, 600)) 53 | 54 | def testCoverageDecreasedSummary(self) : 55 | # Same coverages 56 | self.assertFalse( 57 | jbg.coverageDecreasedSummary( 58 | True, 59 | True, 60 | "tests/reportTest.json", 61 | 0.7272727272727272, 62 | 0.7777777777777777 63 | ) 64 | ) 65 | self.assertFalse( 66 | jbg.coverageDecreasedSummary( 67 | False, 68 | True, 69 | "tests/reportTest.json", 70 | 0.7272727272727272, 71 | 0.7777777777777777 72 | ) 73 | ) 74 | self.assertFalse( 75 | jbg.coverageDecreasedSummary( 76 | True, 77 | False, 78 | "tests/reportTest.json", 79 | 0.7272727272727272, 80 | 0.7777777777777777 81 | ) 82 | ) 83 | self.assertFalse( 84 | jbg.coverageDecreasedSummary( 85 | False, 86 | False, 87 | "tests/reportTest.json", 88 | 0.7272727272727272, 89 | 0.7777777777777777 90 | ) 91 | ) 92 | # Decreased coverage 93 | self.assertTrue( 94 | jbg.coverageDecreasedSummary( 95 | True, 96 | True, 97 | "tests/reportTest.json", 98 | 0.7272727172727272, 99 | 0.7777777777777777 100 | ) 101 | ) 102 | self.assertTrue( 103 | jbg.coverageDecreasedSummary( 104 | True, 105 | False, 106 | "tests/reportTest.json", 107 | 0.7272727172727272, 108 | 0.7777777777777777 109 | ) 110 | ) 111 | self.assertFalse( 112 | jbg.coverageDecreasedSummary( 113 | False, 114 | True, 115 | "tests/reportTest.json", 116 | 0.7272727172727272, 117 | 0.7777777777777777 118 | ) 119 | ) 120 | self.assertFalse( 121 | jbg.coverageDecreasedSummary( 122 | False, 123 | False, 124 | "tests/reportTest.json", 125 | 0.7272727172727272, 126 | 0.7777777777777777 127 | ) 128 | ) 129 | # Decreased branches 130 | self.assertTrue( 131 | jbg.coverageDecreasedSummary( 132 | True, 133 | True, 134 | "tests/reportTest.json", 135 | 0.7272727272727272, 136 | 0.7777777677777777 137 | ) 138 | ) 139 | self.assertTrue( 140 | jbg.coverageDecreasedSummary( 141 | False, 142 | True, 143 | "tests/reportTest.json", 144 | 0.7272727272727272, 145 | 0.7777777677777777 146 | ) 147 | ) 148 | self.assertFalse( 149 | jbg.coverageDecreasedSummary( 150 | True, 151 | False, 152 | "tests/reportTest.json", 153 | 0.7272727272727272, 154 | 0.7777777677777777 155 | ) 156 | ) 157 | self.assertFalse( 158 | jbg.coverageDecreasedSummary( 159 | False, 160 | False, 161 | "tests/reportTest.json", 162 | 0.7272727272727272, 163 | 0.7777777677777777 164 | ) 165 | ) 166 | # Both decreased 167 | self.assertTrue( 168 | jbg.coverageDecreasedSummary( 169 | True, 170 | True, 171 | "tests/reportTest.json", 172 | 0.7272727172727272, 173 | 0.7777777677777777 174 | ) 175 | ) 176 | self.assertTrue( 177 | jbg.coverageDecreasedSummary( 178 | True, 179 | False, 180 | "tests/reportTest.json", 181 | 0.7272727172727272, 182 | 0.7777777677777777 183 | ) 184 | ) 185 | self.assertTrue( 186 | jbg.coverageDecreasedSummary( 187 | False, 188 | True, 189 | "tests/reportTest.json", 190 | 0.7272727172727272, 191 | 0.7777777677777777 192 | ) 193 | ) 194 | self.assertFalse( 195 | jbg.coverageDecreasedSummary( 196 | False, 197 | False, 198 | "tests/reportTest.json", 199 | 0.7272727172727272, 200 | 0.7777777677777777 201 | ) 202 | ) 203 | 204 | def testCoverageDictionary(self) : 205 | cov = 8 / 9 206 | branches = 8 / 11 207 | d = jbg.coverageDictionary(cov, branches) 208 | expected = { "coverage" : 800 / 9, "branches" : 800 / 11 } 209 | self.assertAlmostEqual(expected["coverage"], d["coverage"]) 210 | self.assertAlmostEqual(expected["branches"], d["branches"]) 211 | d = jbg.coverageDictionary(0, 1) 212 | self.assertAlmostEqual(0.0, d["coverage"]) 213 | self.assertAlmostEqual(100.0, d["branches"]) 214 | d = jbg.coverageDictionary(1, 0) 215 | self.assertAlmostEqual(100.0, d["coverage"]) 216 | self.assertAlmostEqual(0.0, d["branches"]) 217 | 218 | def testCoverageDecreased(self) : 219 | badgeFiles = [ "tests/0.svg", 220 | "tests/599.svg", 221 | "tests/60.svg", 222 | "tests/70.svg", 223 | "tests/80.svg", 224 | "tests/899.svg", 225 | "tests/90.svg", 226 | "tests/99.svg", 227 | "tests/999.svg", 228 | "tests/100.svg" 229 | ] 230 | prior = [0, 0.599, 0.6, 0.7, 0.8, 0.899, 0.9, 0.99, 0.999, 1.0 ] 231 | for i, f in enumerate(badgeFiles) : 232 | self.assertFalse(jbg.coverageDecreased(prior[i], f, "coverage")) 233 | self.assertFalse(jbg.coverageDecreased(prior[i]+0.1, f, "coverage")) 234 | self.assertTrue(jbg.coverageDecreased(prior[i]-0.1, f, "coverage")) 235 | self.assertFalse(jbg.coverageDecreased(prior[i]+0.0001, f, "coverage")) 236 | self.assertTrue(jbg.coverageDecreased(prior[i]-0.0001, f, "coverage")) 237 | 238 | branchesBadgeFiles = [ "tests/87b.svg", "tests/90b.svg", "tests/999b.svg" ] 239 | prior = [0.87, 0.9, 0.999] 240 | for i, f in enumerate(branchesBadgeFiles) : 241 | self.assertFalse(jbg.coverageDecreased(prior[i], f, "branches")) 242 | self.assertFalse(jbg.coverageDecreased(prior[i]+0.1, f, "branches")) 243 | self.assertTrue(jbg.coverageDecreased(prior[i]-0.1, f, "branches")) 244 | self.assertFalse(jbg.coverageDecreased(prior[i]+0.0001, f, "branches")) 245 | self.assertTrue(jbg.coverageDecreased(prior[i]-0.0001, f, "branches")) 246 | 247 | for i in range(0, 101, 5) : 248 | cov = i / 100 249 | self.assertFalse(jbg.coverageDecreased(cov, "tests/idontexist.svg", "coverage")) 250 | self.assertFalse(jbg.coverageDecreased(cov, "tests/idontexist.svg", "branches")) 251 | 252 | def testCoverageDecreasedCustomLabelCase(self) : 253 | self.assertFalse(jbg.coverageDecreased(1.0, "tests/custom1.svg", "custom coverage label one")) 254 | self.assertFalse(jbg.coverageDecreased(1.0001, "tests/custom1.svg", "custom coverage label one")) 255 | self.assertTrue(jbg.coverageDecreased(0.9999, "tests/custom1.svg", "custom coverage label one")) 256 | self.assertFalse(jbg.coverageDecreased(0.9, "tests/custom2.svg", "custom coverage label two")) 257 | self.assertFalse(jbg.coverageDecreased(0.9001, "tests/custom2.svg", "custom coverage label two")) 258 | self.assertTrue(jbg.coverageDecreased(0.8999, "tests/custom2.svg", "custom coverage label two")) 259 | 260 | self.assertFalse(jbg.coverageDecreasedEndpoint(1.0, "tests/custom1.json", "custom coverage label one")) 261 | self.assertFalse(jbg.coverageDecreasedEndpoint(1.0001, "tests/custom1.json", "custom coverage label one")) 262 | self.assertTrue(jbg.coverageDecreasedEndpoint(0.9999, "tests/custom1.json", "custom coverage label one")) 263 | self.assertFalse(jbg.coverageDecreasedEndpoint(0.9, "tests/custom2.json", "custom coverage label two")) 264 | self.assertFalse(jbg.coverageDecreasedEndpoint(0.9001, "tests/custom2.json", "custom coverage label two")) 265 | self.assertTrue(jbg.coverageDecreasedEndpoint(0.8999, "tests/custom2.json", "custom coverage label two")) 266 | 267 | def testCoverageDecreasedEndpoint(self) : 268 | jsonFiles = [ "tests/0.json", 269 | "tests/599.json", 270 | "tests/60.json", 271 | "tests/70.json", 272 | "tests/80.json", 273 | "tests/899.json", 274 | "tests/90.json", 275 | "tests/99.json", 276 | "tests/999.json", 277 | "tests/100.json" 278 | ] 279 | prior = [0, 0.599, 0.6, 0.7, 0.8, 0.899, 0.9, 0.99, 0.999, 1.0 ] 280 | for i, f in enumerate(jsonFiles) : 281 | self.assertFalse(jbg.coverageDecreasedEndpoint(prior[i], f, "coverage")) 282 | self.assertFalse(jbg.coverageDecreasedEndpoint(prior[i]+0.1, f, "coverage")) 283 | self.assertTrue(jbg.coverageDecreasedEndpoint(prior[i]-0.1, f, "coverage")) 284 | self.assertFalse(jbg.coverageDecreasedEndpoint(prior[i]+0.0001, f, "coverage")) 285 | self.assertTrue(jbg.coverageDecreasedEndpoint(prior[i]-0.0001, f, "coverage")) 286 | 287 | branchesJsonFiles = [ "tests/0b.json", 288 | "tests/599b.json", 289 | "tests/60b.json", 290 | "tests/70b.json", 291 | "tests/80b.json", 292 | "tests/899b.json", 293 | "tests/90b.json", 294 | "tests/99b.json", 295 | "tests/999b.json", 296 | "tests/100b.json" 297 | ] 298 | for i, f in enumerate(branchesJsonFiles) : 299 | self.assertFalse(jbg.coverageDecreasedEndpoint(prior[i], f, "branches")) 300 | self.assertFalse(jbg.coverageDecreasedEndpoint(prior[i]+0.1, f, "branches")) 301 | self.assertTrue(jbg.coverageDecreasedEndpoint(prior[i]-0.1, f, "branches")) 302 | self.assertFalse(jbg.coverageDecreasedEndpoint(prior[i]+0.0001, f, "branches")) 303 | self.assertTrue(jbg.coverageDecreasedEndpoint(prior[i]-0.0001, f, "branches")) 304 | 305 | for i in range(0, 101, 5) : 306 | cov = i / 100 307 | self.assertFalse(jbg.coverageDecreasedEndpoint(cov, "tests/idontexist.svg", "coverage")) 308 | self.assertFalse(jbg.coverageDecreasedEndpoint(cov, "tests/idontexist.svg", "branches")) 309 | 310 | def testGetPriorCoverage(self): 311 | badgeFiles = [ "tests/0.svg", 312 | "tests/599.svg", 313 | "tests/60.svg", 314 | "tests/70.svg", 315 | "tests/80.svg", 316 | "tests/899.svg", 317 | "tests/90.svg", 318 | "tests/99.svg", 319 | "tests/999.svg", 320 | "tests/100.svg" 321 | ] 322 | expected = [0, 0.599, 0.6, 0.7, 0.8, 0.899, 0.9, 0.99, 0.999, 1.0 ] 323 | for i, f in enumerate(badgeFiles) : 324 | self.assertAlmostEqual(expected[i], jbg.getPriorCoverage(f, "coverage")) 325 | self.assertEqual(-1, jbg.getPriorCoverage("tests/idontexist.svg", "coverage")) 326 | self.assertEqual(-1, jbg.getPriorCoverage("tests/999b.svg", "coverage")) 327 | 328 | branchesBadgeFiles = [ "tests/87b.svg", "tests/90b.svg", "tests/999b.svg" ] 329 | expected = [0.87, 0.9, 0.999] 330 | for i, f in enumerate(branchesBadgeFiles) : 331 | self.assertAlmostEqual(expected[i], jbg.getPriorCoverage(f, "branches")) 332 | self.assertEqual(-1, jbg.getPriorCoverage("tests/idontexist.svg", "branches")) 333 | self.assertEqual(-1, jbg.getPriorCoverage("tests/999.svg", "branches")) 334 | 335 | def testGetPriorCoverageCustomBadgeLabelCase(self): 336 | self.assertAlmostEqual(1.0, jbg.getPriorCoverage("tests/custom1.svg", "custom coverage label one")) 337 | self.assertAlmostEqual(0.9, jbg.getPriorCoverage("tests/custom2.svg", "custom coverage label two")) 338 | self.assertAlmostEqual(1.0, jbg.getPriorCoverageFromEndpoint("tests/custom1.json", "custom coverage label one")) 339 | self.assertAlmostEqual(0.9, jbg.getPriorCoverageFromEndpoint("tests/custom2.json", "custom coverage label two")) 340 | 341 | def testGetPriorCoverageFromEndpoint(self): 342 | jsonFiles = [ 343 | "tests/0.json", 344 | "tests/599.json", 345 | "tests/60.json", 346 | "tests/70.json", 347 | "tests/78.json", 348 | "tests/80.json", 349 | "tests/87.json", 350 | "tests/899.json", 351 | "tests/90.json", 352 | "tests/99.json", 353 | "tests/999.json", 354 | "tests/100.json", 355 | "tests/idontexist.json" 356 | ] 357 | jsonFilesB = [ 358 | "tests/0b.json", 359 | "tests/599b.json", 360 | "tests/60b.json", 361 | "tests/70b.json", 362 | "tests/78b.json", 363 | "tests/80b.json", 364 | "tests/87b.json", 365 | "tests/899b.json", 366 | "tests/90b.json", 367 | "tests/99b.json", 368 | "tests/999b.json", 369 | "tests/100b.json", 370 | "tests/idontexist.json" 371 | ] 372 | expected = [0, 0.599, 0.6, 0.7, 0.78, 0.8, 0.87, 0.899, 0.9, 0.99, 0.999, 1.0, -1 ] 373 | for i, f in enumerate(jsonFiles) : 374 | self.assertAlmostEqual(expected[i], jbg.getPriorCoverageFromEndpoint(f, "coverage"), msg="file:"+f) 375 | for i, f in enumerate(jsonFilesB) : 376 | self.assertAlmostEqual(expected[i], jbg.getPriorCoverageFromEndpoint(f, "branches"), msg="file:"+f) 377 | 378 | def testCoverageIsFailing(self) : 379 | self.assertFalse(jbg.coverageIsFailing(0, 0, 0, 0)) 380 | self.assertFalse(jbg.coverageIsFailing(0.5, 0.5, 0.5, 0.5)) 381 | self.assertFalse(jbg.coverageIsFailing(0.51, 0.51, 0.5, 0.5)) 382 | self.assertTrue(jbg.coverageIsFailing(0.49, 0.5, 0.5, 0.5)) 383 | self.assertTrue(jbg.coverageIsFailing(0.5, 0.49, 0.5, 0.5)) 384 | self.assertTrue(jbg.coverageIsFailing(0.49, 0.49, 0.5, 0.5)) 385 | 386 | def testStringToPercentage(self) : 387 | for i in range(0, 101, 10) : 388 | expected = i/100 389 | s1 = str(i) 390 | s2 = s1 + "%" 391 | s3 = s1 + " %" 392 | s4 = str(expected) 393 | self.assertAlmostEqual(expected, jbg.stringToPercentage(s1)) 394 | self.assertAlmostEqual(expected, jbg.stringToPercentage(s2)) 395 | self.assertAlmostEqual(expected, jbg.stringToPercentage(s3)) 396 | self.assertAlmostEqual(expected, jbg.stringToPercentage(s4)) 397 | for j in range(10, 101, 10) : 398 | i = j - 0.5 399 | expected = i/100 400 | s1 = str(i) 401 | s2 = s1 + "%" 402 | s3 = s1 + " %" 403 | s4 = str(expected) 404 | self.assertAlmostEqual(expected, jbg.stringToPercentage(s1)) 405 | self.assertAlmostEqual(expected, jbg.stringToPercentage(s2)) 406 | self.assertAlmostEqual(expected, jbg.stringToPercentage(s3)) 407 | self.assertAlmostEqual(expected, jbg.stringToPercentage(s4)) 408 | i = 0.0 409 | while i <= 1.0 : 410 | s1 = str(i) 411 | self.assertAlmostEqual(i, jbg.stringToPercentage(s1)) 412 | s2 = s1 + "%" 413 | s3 = s1 + " %" 414 | self.assertAlmostEqual(i/100, jbg.stringToPercentage(s2)) 415 | self.assertAlmostEqual(i/100, jbg.stringToPercentage(s3)) 416 | i += 0.05 417 | self.assertAlmostEqual(0, jbg.stringToPercentage("")) 418 | self.assertAlmostEqual(0, jbg.stringToPercentage("%")) 419 | self.assertAlmostEqual(0, jbg.stringToPercentage(" %")) 420 | self.assertAlmostEqual(0, jbg.stringToPercentage(" ")) 421 | self.assertAlmostEqual(0, jbg.stringToPercentage("hello")) 422 | 423 | def testFilterMissingReports_empty(self) : 424 | self.assertEqual(([], False), jbg.filterMissingReports([])) 425 | self.assertEqual(([], True), jbg.filterMissingReports(["**/idontexist*.csv"])) 426 | 427 | def testFilterMissingReports(self) : 428 | expected = ["tests/jacoco100.csv", "tests/jacoco90.csv"] 429 | case = ["tests/idontexist1.csv", 430 | "tests/jacoco100.csv", 431 | "tests/idontexist2.csv", 432 | "tests/idontexist3.csv", 433 | "tests/jacoco90.csv", 434 | "tests/idontexist4.csv"] 435 | self.assertEqual((expected, True), jbg.filterMissingReports(case)) 436 | 437 | def testFilterMissingReports_withGlobs(self) : 438 | expected = { 439 | "tests/jacoco.csv", 440 | "tests/jacoco90.csv", 441 | "tests/jacoco100.csv", 442 | "tests/jacoco901.csv", 443 | "tests/jacocoDivZero.csv" } 444 | case = ["**/j*.csv"] 445 | reports, isMissing = jbg.filterMissingReports(case) 446 | actual = { s.replace("\\", "/") for s in reports } 447 | self.assertEqual((expected, False), (actual, isMissing)) 448 | 449 | def testFullCoverage(self) : 450 | self.assertAlmostEqual(1, jbg.computeCoverage(["tests/jacoco100.csv"])[0]) 451 | 452 | def testCoverage90(self) : 453 | self.assertAlmostEqual(0.9, jbg.computeCoverage(["tests/jacoco90.csv"])[0]) 454 | 455 | def testCoverage901(self) : 456 | self.assertAlmostEqual(0.901, jbg.computeCoverage(["tests/jacoco901.csv"])[0]) 457 | 458 | def testCoverageNoInstructions(self) : 459 | self.assertAlmostEqual(1, jbg.computeCoverage(["tests/jacocoDivZero.csv"])[0]) 460 | 461 | def testFullCoverageBranches(self) : 462 | self.assertAlmostEqual(1, jbg.computeCoverage(["tests/branches100.csv"])[1]) 463 | 464 | def testCoverage90Branches(self) : 465 | self.assertAlmostEqual(0.9, jbg.computeCoverage(["tests/branches90.csv"])[1]) 466 | 467 | def testCoverage901Branches(self) : 468 | self.assertAlmostEqual(0.901, jbg.computeCoverage(["tests/branches901.csv"])[1]) 469 | 470 | def testCoverageNoBranches(self) : 471 | self.assertAlmostEqual(1, jbg.computeCoverage(["tests/branchesDivZero.csv"])[1]) 472 | 473 | def testComputeCoverageMultiJacocoReports(self) : 474 | coverage, branches = jbg.computeCoverage(["tests/multi1.csv", "tests/multi2.csv"]) 475 | self.assertAlmostEqual(0.78, coverage) 476 | self.assertAlmostEqual(0.87, branches) 477 | 478 | def testCoverageTruncatedToString_str(self) : 479 | self.assertEqual("100%", jbg.coverageTruncatedToString(1)[0]) 480 | self.assertEqual("100%", jbg.coverageTruncatedToString(1.0)[0]) 481 | self.assertEqual("99.9%", jbg.coverageTruncatedToString(0.99999)[0]) 482 | self.assertEqual("99.8%", jbg.coverageTruncatedToString(0.9989)[0]) 483 | self.assertEqual("99%", jbg.coverageTruncatedToString(0.99000001)[0]) 484 | self.assertEqual("99%", jbg.coverageTruncatedToString(0.9904)[0]) 485 | self.assertEqual("99%", jbg.coverageTruncatedToString(0.99009)[0]) 486 | self.assertEqual("99.1%", jbg.coverageTruncatedToString(0.991000001)[0]) 487 | 488 | def testCoverageTruncatedToString_float(self) : 489 | self.assertAlmostEqual(100.0, jbg.coverageTruncatedToString(1)[1]) 490 | self.assertAlmostEqual(100.0, jbg.coverageTruncatedToString(1.0)[1]) 491 | self.assertAlmostEqual(99.9, jbg.coverageTruncatedToString(0.99999)[1]) 492 | self.assertAlmostEqual(99.8, jbg.coverageTruncatedToString(0.9989)[1]) 493 | self.assertAlmostEqual(99.0, jbg.coverageTruncatedToString(0.99000001)[1]) 494 | self.assertAlmostEqual(99.0, jbg.coverageTruncatedToString(0.9904)[1]) 495 | self.assertAlmostEqual(99.0, jbg.coverageTruncatedToString(0.99009)[1]) 496 | self.assertAlmostEqual(99.1, jbg.coverageTruncatedToString(0.991000001)[1]) 497 | 498 | def testFormatPercentage(self) : 499 | self.assertEqual("100%", jbg.badgeCoverageStringColorPair(1)[0]) 500 | self.assertEqual("100%", jbg.badgeCoverageStringColorPair(1.0)[0]) 501 | self.assertEqual("99.9%", jbg.badgeCoverageStringColorPair(0.99999)[0]) 502 | self.assertEqual("99.8%", jbg.badgeCoverageStringColorPair(0.9989)[0]) 503 | self.assertEqual("99%", jbg.badgeCoverageStringColorPair(0.99000001)[0]) 504 | self.assertEqual("99%", jbg.badgeCoverageStringColorPair(0.9904)[0]) 505 | self.assertEqual("99%", jbg.badgeCoverageStringColorPair(0.99009)[0]) 506 | self.assertEqual("99.1%", jbg.badgeCoverageStringColorPair(0.991000001)[0]) 507 | 508 | def testColor(self) : 509 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(1)[1]) 510 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(1.0)[1]) 511 | self.assertEqual(jbg.defaultColors[1], jbg.badgeCoverageStringColorPair(0.99999)[1]) 512 | self.assertEqual(jbg.defaultColors[1], jbg.badgeCoverageStringColorPair(0.9)[1]) 513 | self.assertEqual(jbg.defaultColors[2], jbg.badgeCoverageStringColorPair(0.89999)[1]) 514 | self.assertEqual(jbg.defaultColors[2], jbg.badgeCoverageStringColorPair(0.8)[1]) 515 | self.assertEqual(jbg.defaultColors[3], jbg.badgeCoverageStringColorPair(0.79999)[1]) 516 | self.assertEqual(jbg.defaultColors[3], jbg.badgeCoverageStringColorPair(0.7)[1]) 517 | self.assertEqual(jbg.defaultColors[4], jbg.badgeCoverageStringColorPair(0.69999)[1]) 518 | self.assertEqual(jbg.defaultColors[4], jbg.badgeCoverageStringColorPair(0.6)[1]) 519 | self.assertEqual(jbg.defaultColors[5], jbg.badgeCoverageStringColorPair(0.59999)[1]) 520 | self.assertEqual(jbg.defaultColors[5], jbg.badgeCoverageStringColorPair(0.0)[1]) 521 | self.assertEqual(jbg.defaultColors[5], jbg.badgeCoverageStringColorPair(0)[1]) 522 | # extra colors 523 | colors = jbg.defaultColors[:] 524 | colors.append("#000000") 525 | colors.append("#ffffff") 526 | cutoffs=[100, 90, 80, 70, 60] 527 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(1, cutoffs, colors)[1]) 528 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(1.0, cutoffs, colors)[1]) 529 | self.assertEqual(jbg.defaultColors[1], jbg.badgeCoverageStringColorPair(0.99999, cutoffs, colors)[1]) 530 | self.assertEqual(jbg.defaultColors[1], jbg.badgeCoverageStringColorPair(0.9, cutoffs, colors)[1]) 531 | self.assertEqual(jbg.defaultColors[2], jbg.badgeCoverageStringColorPair(0.89999, cutoffs, colors)[1]) 532 | self.assertEqual(jbg.defaultColors[2], jbg.badgeCoverageStringColorPair(0.8, cutoffs, colors)[1]) 533 | self.assertEqual(jbg.defaultColors[3], jbg.badgeCoverageStringColorPair(0.79999, cutoffs, colors)[1]) 534 | self.assertEqual(jbg.defaultColors[3], jbg.badgeCoverageStringColorPair(0.7, cutoffs, colors)[1]) 535 | self.assertEqual(jbg.defaultColors[4], jbg.badgeCoverageStringColorPair(0.69999, cutoffs, colors)[1]) 536 | self.assertEqual(jbg.defaultColors[4], jbg.badgeCoverageStringColorPair(0.6, cutoffs, colors)[1]) 537 | self.assertEqual(jbg.defaultColors[5], jbg.badgeCoverageStringColorPair(0.59999, cutoffs, colors)[1]) 538 | self.assertEqual(jbg.defaultColors[5], jbg.badgeCoverageStringColorPair(0.0, cutoffs, colors)[1]) 539 | self.assertEqual(jbg.defaultColors[5], jbg.badgeCoverageStringColorPair(0, cutoffs, colors)[1]) 540 | # fewer colors 541 | colors = jbg.defaultColors[:3] 542 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(1, cutoffs, colors)[1]) 543 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(1.0, cutoffs, colors)[1]) 544 | self.assertEqual(jbg.defaultColors[1], jbg.badgeCoverageStringColorPair(0.99999, cutoffs, colors)[1]) 545 | self.assertEqual(jbg.defaultColors[1], jbg.badgeCoverageStringColorPair(0.9, cutoffs, colors)[1]) 546 | self.assertEqual(jbg.defaultColors[2], jbg.badgeCoverageStringColorPair(0.89999, cutoffs, colors)[1]) 547 | self.assertEqual(jbg.defaultColors[2], jbg.badgeCoverageStringColorPair(0.8, cutoffs, colors)[1]) 548 | self.assertEqual(jbg.defaultColors[2], jbg.badgeCoverageStringColorPair(0.79999, cutoffs, colors)[1]) 549 | self.assertEqual(jbg.defaultColors[2], jbg.badgeCoverageStringColorPair(0.7, cutoffs, colors)[1]) 550 | self.assertEqual(jbg.defaultColors[2], jbg.badgeCoverageStringColorPair(0.69999, cutoffs, colors)[1]) 551 | self.assertEqual(jbg.defaultColors[2], jbg.badgeCoverageStringColorPair(0.6, cutoffs, colors)[1]) 552 | self.assertEqual(jbg.defaultColors[2], jbg.badgeCoverageStringColorPair(0.59999, cutoffs, colors)[1]) 553 | self.assertEqual(jbg.defaultColors[2], jbg.badgeCoverageStringColorPair(0.0, cutoffs, colors)[1]) 554 | self.assertEqual(jbg.defaultColors[2], jbg.badgeCoverageStringColorPair(0, cutoffs, colors)[1]) 555 | # only 1 color 556 | colors = jbg.defaultColors[:1] 557 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(1, cutoffs, colors)[1]) 558 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(1.0, cutoffs, colors)[1]) 559 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(0.99999, cutoffs, colors)[1]) 560 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(0.9, cutoffs, colors)[1]) 561 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(0.89999, cutoffs, colors)[1]) 562 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(0.8, cutoffs, colors)[1]) 563 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(0.79999, cutoffs, colors)[1]) 564 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(0.7, cutoffs, colors)[1]) 565 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(0.69999, cutoffs, colors)[1]) 566 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(0.6, cutoffs, colors)[1]) 567 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(0.59999, cutoffs, colors)[1]) 568 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(0.0, cutoffs, colors)[1]) 569 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(0, cutoffs, colors)[1]) 570 | # empty color list should use default. 571 | colors = [] 572 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(1, cutoffs, colors)[1]) 573 | self.assertEqual(jbg.defaultColors[0], jbg.badgeCoverageStringColorPair(1.0, cutoffs, colors)[1]) 574 | self.assertEqual(jbg.defaultColors[1], jbg.badgeCoverageStringColorPair(0.99999, cutoffs, colors)[1]) 575 | self.assertEqual(jbg.defaultColors[1], jbg.badgeCoverageStringColorPair(0.9, cutoffs, colors)[1]) 576 | self.assertEqual(jbg.defaultColors[2], jbg.badgeCoverageStringColorPair(0.89999, cutoffs, colors)[1]) 577 | self.assertEqual(jbg.defaultColors[2], jbg.badgeCoverageStringColorPair(0.8, cutoffs, colors)[1]) 578 | self.assertEqual(jbg.defaultColors[3], jbg.badgeCoverageStringColorPair(0.79999, cutoffs, colors)[1]) 579 | self.assertEqual(jbg.defaultColors[3], jbg.badgeCoverageStringColorPair(0.7, cutoffs, colors)[1]) 580 | self.assertEqual(jbg.defaultColors[4], jbg.badgeCoverageStringColorPair(0.69999, cutoffs, colors)[1]) 581 | self.assertEqual(jbg.defaultColors[4], jbg.badgeCoverageStringColorPair(0.6, cutoffs, colors)[1]) 582 | self.assertEqual(jbg.defaultColors[5], jbg.badgeCoverageStringColorPair(0.59999, cutoffs, colors)[1]) 583 | self.assertEqual(jbg.defaultColors[5], jbg.badgeCoverageStringColorPair(0.0, cutoffs, colors)[1]) 584 | self.assertEqual(jbg.defaultColors[5], jbg.badgeCoverageStringColorPair(0, cutoffs, colors)[1]) 585 | 586 | 587 | def testColorIndex(self): 588 | self.assertEqual(0, jbg.computeColorIndex(100, [100, 90, 80, 70, 60], 6)); 589 | self.assertEqual(0, jbg.computeColorIndex(100.0, [100, 90, 80, 70, 60], 6)); 590 | self.assertEqual(1, jbg.computeColorIndex(99.999, [100, 90, 80, 70, 60], 6)); 591 | self.assertEqual(1, jbg.computeColorIndex(90, [100, 90, 80, 70, 60], 6)); 592 | self.assertEqual(2, jbg.computeColorIndex(89.999, [100, 90, 80, 70, 60], 6)); 593 | self.assertEqual(2, jbg.computeColorIndex(80, [100, 90, 80, 70, 60], 6)); 594 | self.assertEqual(3, jbg.computeColorIndex(79.999, [100, 90, 80, 70, 60], 6)); 595 | self.assertEqual(3, jbg.computeColorIndex(70, [100, 90, 80, 70, 60], 6)); 596 | self.assertEqual(4, jbg.computeColorIndex(69.999, [100, 90, 80, 70, 60], 6)); 597 | self.assertEqual(4, jbg.computeColorIndex(60, [100, 90, 80, 70, 60], 6)); 598 | self.assertEqual(5, jbg.computeColorIndex(59.999, [100, 90, 80, 70, 60], 6)); 599 | self.assertEqual(5, jbg.computeColorIndex(50, [100, 90, 80, 70, 60], 6)); 600 | # more cutoffs than necessary 601 | self.assertEqual(0, jbg.computeColorIndex(100, [100, 90, 80, 70, 60, 50], 6)); 602 | self.assertEqual(0, jbg.computeColorIndex(100.0, [100, 90, 80, 70, 60, 50], 6)); 603 | self.assertEqual(1, jbg.computeColorIndex(99.999, [100, 90, 80, 70, 60, 50], 6)); 604 | self.assertEqual(1, jbg.computeColorIndex(90, [100, 90, 80, 70, 60, 50], 6)); 605 | self.assertEqual(2, jbg.computeColorIndex(89.999, [100, 90, 80, 70, 60, 50], 6)); 606 | self.assertEqual(2, jbg.computeColorIndex(80, [100, 90, 80, 70, 60, 50], 6)); 607 | self.assertEqual(3, jbg.computeColorIndex(79.999, [100, 90, 80, 70, 60, 50], 6)); 608 | self.assertEqual(3, jbg.computeColorIndex(70, [100, 90, 80, 70, 60, 50], 6)); 609 | self.assertEqual(4, jbg.computeColorIndex(69.999, [100, 90, 80, 70, 60, 50], 6)); 610 | self.assertEqual(4, jbg.computeColorIndex(60, [100, 90, 80, 70, 60, 50], 6)); 611 | self.assertEqual(5, jbg.computeColorIndex(59.999, [100, 90, 80, 70, 60, 50], 6)); 612 | self.assertEqual(5, jbg.computeColorIndex(50, [100, 90, 80, 70, 60, 50], 6)); 613 | # even more cutoffs than necessary 614 | self.assertEqual(0, jbg.computeColorIndex(100, [100, 90, 80, 70, 60, 50, 0], 6)); 615 | self.assertEqual(0, jbg.computeColorIndex(100.0, [100, 90, 80, 70, 60, 50, 0], 6)); 616 | self.assertEqual(1, jbg.computeColorIndex(99.999, [100, 90, 80, 70, 60, 50, 0], 6)); 617 | self.assertEqual(1, jbg.computeColorIndex(90, [100, 90, 80, 70, 60, 50, 0], 6)); 618 | self.assertEqual(2, jbg.computeColorIndex(89.999, [100, 90, 80, 70, 60, 50, 0], 6)); 619 | self.assertEqual(2, jbg.computeColorIndex(80, [100, 90, 80, 70, 60, 50, 0], 6)); 620 | self.assertEqual(3, jbg.computeColorIndex(79.999, [100, 90, 80, 70, 60, 50, 0], 6)); 621 | self.assertEqual(3, jbg.computeColorIndex(70, [100, 90, 80, 70, 60, 50, 0], 6)); 622 | self.assertEqual(4, jbg.computeColorIndex(69.999, [100, 90, 80, 70, 60, 50, 0], 6)); 623 | self.assertEqual(4, jbg.computeColorIndex(60, [100, 90, 80, 70, 60, 50, 0], 6)); 624 | self.assertEqual(5, jbg.computeColorIndex(59.999, [100, 90, 80, 70, 60, 50, 0], 6)); 625 | self.assertEqual(5, jbg.computeColorIndex(50, [100, 90, 80, 70, 60, 50, 0], 6)); 626 | # too few cutoffs 627 | self.assertEqual(0, jbg.computeColorIndex(100, [100, 90, 80, 70], 6)); 628 | self.assertEqual(0, jbg.computeColorIndex(100.0, [100, 90, 80, 70], 6)); 629 | self.assertEqual(1, jbg.computeColorIndex(99.999, [100, 90, 80, 70], 6)); 630 | self.assertEqual(1, jbg.computeColorIndex(90, [100, 90, 80, 70], 6)); 631 | self.assertEqual(2, jbg.computeColorIndex(89.999, [100, 90, 80, 70], 6)); 632 | self.assertEqual(2, jbg.computeColorIndex(80, [100, 90, 80, 70], 6)); 633 | self.assertEqual(3, jbg.computeColorIndex(79.999, [100, 90, 80, 70], 6)); 634 | self.assertEqual(3, jbg.computeColorIndex(70, [100, 90, 80, 70], 6)); 635 | self.assertEqual(4, jbg.computeColorIndex(69.999, [100, 90, 80, 70], 6)); 636 | self.assertEqual(4, jbg.computeColorIndex(60, [100, 90, 80, 70], 6)); 637 | self.assertEqual(4, jbg.computeColorIndex(59.999, [100, 90, 80, 70], 6)); 638 | self.assertEqual(4, jbg.computeColorIndex(50, [100, 90, 80, 70], 6)); 639 | # only 1 cutoff 640 | self.assertEqual(0, jbg.computeColorIndex(100, [100], 6)); 641 | self.assertEqual(0, jbg.computeColorIndex(100.0, [100], 6)); 642 | self.assertEqual(1, jbg.computeColorIndex(99.999, [100], 6)); 643 | self.assertEqual(1, jbg.computeColorIndex(90, [100], 6)); 644 | self.assertEqual(1, jbg.computeColorIndex(89.999, [100], 6)); 645 | self.assertEqual(1, jbg.computeColorIndex(80, [100], 6)); 646 | self.assertEqual(1, jbg.computeColorIndex(79.999, [100], 6)); 647 | self.assertEqual(1, jbg.computeColorIndex(70, [100], 6)); 648 | self.assertEqual(1, jbg.computeColorIndex(69.999, [100], 6)); 649 | self.assertEqual(1, jbg.computeColorIndex(60, [100], 6)); 650 | self.assertEqual(1, jbg.computeColorIndex(59.999, [100], 6)); 651 | self.assertEqual(1, jbg.computeColorIndex(50, [100], 6)); 652 | # no cutoffs 653 | self.assertEqual(0, jbg.computeColorIndex(100, [], 6)); 654 | self.assertEqual(0, jbg.computeColorIndex(100.0, [], 6)); 655 | self.assertEqual(0, jbg.computeColorIndex(99.999, [], 6)); 656 | self.assertEqual(0, jbg.computeColorIndex(90, [], 6)); 657 | self.assertEqual(0, jbg.computeColorIndex(89.999, [], 6)); 658 | self.assertEqual(0, jbg.computeColorIndex(80, [], 6)); 659 | self.assertEqual(0, jbg.computeColorIndex(79.999, [], 6)); 660 | self.assertEqual(0, jbg.computeColorIndex(70, [], 6)); 661 | self.assertEqual(0, jbg.computeColorIndex(69.999, [], 6)); 662 | self.assertEqual(0, jbg.computeColorIndex(60, [], 6)); 663 | self.assertEqual(0, jbg.computeColorIndex(59.999, [], 6)); 664 | self.assertEqual(0, jbg.computeColorIndex(50, [], 6)); 665 | 666 | def testBadgeGeneration(self) : 667 | testPercentages = [0, 0.599, 0.6, 0.7, 0.8, 0.899, 0.9, 0.99, 0.999, 1] 668 | expectedFiles = [ "tests/0.svg", 669 | "tests/599.svg", 670 | "tests/60.svg", 671 | "tests/70.svg", 672 | "tests/80.svg", 673 | "tests/899.svg", 674 | "tests/90.svg", 675 | "tests/99.svg", 676 | "tests/999.svg", 677 | "tests/100.svg" 678 | ] 679 | for i, cov in enumerate(testPercentages) : 680 | covStr, color = jbg.badgeCoverageStringColorPair(cov) 681 | badge = jbg.generateBadge(covStr, color) 682 | with open(expectedFiles[i],"r") as f : 683 | self.assertEqual(f.read(), badge, msg=expectedFiles[i]) 684 | covStr, color = jbg.badgeCoverageStringColorPair(0.999) 685 | badge = jbg.generateBadge(covStr, color, "branches") 686 | with open("tests/999b.svg","r") as f : 687 | self.assertEqual(f.read(), badge) 688 | 689 | def testCustomBadgeLabels(self) : 690 | covStr, color = jbg.badgeCoverageStringColorPair(1.0) 691 | badge = jbg.generateBadge(covStr, color, "custom coverage label one") 692 | with open("tests/custom1.svg","r") as f : 693 | self.assertEqual(f.read(), badge) 694 | covStr, color = jbg.badgeCoverageStringColorPair(0.9) 695 | badge = jbg.generateBadge(covStr, color, "custom coverage label two") 696 | with open("tests/custom2.svg","r") as f : 697 | self.assertEqual(f.read(), badge) 698 | 699 | def testGenerateDictionaryForEndpoint(self) : 700 | testPercentages = [0, 0.599, 0.6, 0.7, 0.8, 0.899, 0.9, 0.99, 0.999, 1] 701 | expectedMsg = ["0%", "59.9%", "60%", "70%", "80%", "89.9%", "90%", "99%", "99.9%", "100%"] 702 | expectedColor = [ 703 | jbg.defaultColors[5], 704 | jbg.defaultColors[5], 705 | jbg.defaultColors[4], 706 | jbg.defaultColors[3], 707 | jbg.defaultColors[2], 708 | jbg.defaultColors[2], 709 | jbg.defaultColors[1], 710 | jbg.defaultColors[1], 711 | jbg.defaultColors[1], 712 | jbg.defaultColors[0] 713 | ] 714 | for i, cov in enumerate(testPercentages) : 715 | covStr, color = jbg.badgeCoverageStringColorPair(cov) 716 | d = jbg.generateDictionaryForEndpoint(covStr, color, "coverage") 717 | self.assertEqual(1, d["schemaVersion"]) 718 | self.assertEqual("coverage", d["label"]) 719 | self.assertEqual(expectedMsg[i], d["message"]) 720 | self.assertEqual(expectedColor[i], d["color"]) 721 | d = jbg.generateDictionaryForEndpoint(covStr, color, "branches") 722 | self.assertEqual(1, d["schemaVersion"]) 723 | self.assertEqual("branches", d["label"]) 724 | self.assertEqual(expectedMsg[i], d["message"]) 725 | self.assertEqual(expectedColor[i], d["color"]) 726 | 727 | def testSplitPath(self) : 728 | cases = [ ( "./jacoco.svg", ".", "jacoco.svg" ), 729 | ( "/jacoco.svg", ".", "jacoco.svg" ), 730 | ( "jacoco.svg", ".", "jacoco.svg" ), 731 | ( "./a/jacoco.svg", "a", "jacoco.svg" ), 732 | ( "/a/jacoco.svg", "a", "jacoco.svg" ), 733 | ( "a/jacoco.svg", "a", "jacoco.svg" ), 734 | ( "./a/b/jacoco.svg", "a/b", "jacoco.svg" ), 735 | ( "/a/b/jacoco.svg", "a/b", "jacoco.svg" ), 736 | ( "a/b/jacoco.svg", "a/b", "jacoco.svg" ) 737 | ] 738 | for testcase, directoryExpected, filenameExpected in cases : 739 | directory, filename = jbg.splitPath(testcase) 740 | self.assertEqual(directoryExpected, directory) 741 | self.assertEqual(filenameExpected, filename) 742 | 743 | def testFormPath(self) : 744 | cases = [ ( ".", "jacoco.svg", "jacoco.svg" ), 745 | ( "./", "jacoco.svg", "jacoco.svg" ), 746 | ( "/", "jacoco.svg", "jacoco.svg" ), 747 | ( "", "jacoco.svg", "jacoco.svg" ), 748 | ( ".", "/jacoco.svg", "jacoco.svg" ), 749 | ( "./", "/jacoco.svg", "jacoco.svg" ), 750 | ( "/", "/jacoco.svg", "jacoco.svg" ), 751 | ( "", "/jacoco.svg", "jacoco.svg" ), 752 | ( "./a", "jacoco.svg", "a/jacoco.svg" ), 753 | ( "./a/", "jacoco.svg", "a/jacoco.svg" ), 754 | ( "/a", "jacoco.svg", "a/jacoco.svg" ), 755 | ( "/a/", "jacoco.svg", "a/jacoco.svg" ), 756 | ( "a", "jacoco.svg", "a/jacoco.svg" ), 757 | ( "a/", "jacoco.svg", "a/jacoco.svg" ), 758 | ( "./a", "/jacoco.svg", "a/jacoco.svg" ), 759 | ( "./a/", "/jacoco.svg", "a/jacoco.svg" ), 760 | ( "/a", "/jacoco.svg", "a/jacoco.svg" ), 761 | ( "/a/", "/jacoco.svg", "a/jacoco.svg" ), 762 | ( "a", "/jacoco.svg", "a/jacoco.svg" ), 763 | ( "a/", "/jacoco.svg", "a/jacoco.svg" ), 764 | ( "./a/b", "jacoco.svg", "a/b/jacoco.svg" ), 765 | ( "./a/b/", "jacoco.svg", "a/b/jacoco.svg" ), 766 | ( "/a/b", "jacoco.svg", "a/b/jacoco.svg" ), 767 | ( "/a/b/", "jacoco.svg", "a/b/jacoco.svg" ), 768 | ( "a/b", "jacoco.svg", "a/b/jacoco.svg" ), 769 | ( "a/b/", "jacoco.svg", "a/b/jacoco.svg" ), 770 | ( "./a/b", "/jacoco.svg", "a/b/jacoco.svg" ), 771 | ( "./a/b/", "/jacoco.svg", "a/b/jacoco.svg" ), 772 | ( "/a/b", "/jacoco.svg", "a/b/jacoco.svg" ), 773 | ( "/a/b/", "/jacoco.svg", "a/b/jacoco.svg" ), 774 | ( "a/b", "/jacoco.svg", "a/b/jacoco.svg" ), 775 | ( "a/b/", "/jacoco.svg", "a/b/jacoco.svg" ) 776 | ] 777 | for directory, filename, expected in cases : 778 | self.assertEqual(expected, jbg.formFullPathToFile(directory, filename)) 779 | 780 | def testStringToFloatCutoffs(self) : 781 | self.assertEqual([100, 90, 80, 70, 60, 0], jbg.colorCutoffsStringToNumberList('100, 90, 80, 70, 60, 0')) 782 | self.assertEqual([100, 90, 80, 70, 60, 0], jbg.colorCutoffsStringToNumberList('100 90 80 70 60 0')) 783 | self.assertEqual([100, 90, 80, 70, 60, 0], jbg.colorCutoffsStringToNumberList('100,90,80,70,60,0')) 784 | self.assertEqual([], jbg.colorCutoffsStringToNumberList('')) 785 | self.assertEqual([], jbg.colorCutoffsStringToNumberList(',')) 786 | self.assertEqual([], jbg.colorCutoffsStringToNumberList(' ')) 787 | self.assertEqual([99.9], jbg.colorCutoffsStringToNumberList('99.9')) 788 | self.assertEqual([99.9], jbg.colorCutoffsStringToNumberList('99.9,')) 789 | --------------------------------------------------------------------------------