├── .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 |
--------------------------------------------------------------------------------
/tests/60.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/70.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/78.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/80.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/87b.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/90.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/90b.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/99.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/100.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/599.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/899.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/999.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/999b.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/custom1.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tests/custom2.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 = ''
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 |
--------------------------------------------------------------------------------