From bb855d572d81158ebd9f0a14ba17f963dce17cc4 Mon Sep 17 00:00:00 2001 From: Patrick Lehmann Date: Tue, 21 Dec 2021 19:00:11 +0100 Subject: [PATCH 01/13] Pytest using pyproject.toml. --- .github/workflows/CoverageCollection.yml | 21 ++++++++++++++++----- .github/workflows/UnitTesting.yml | 7 ++++++- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/CoverageCollection.yml b/.github/workflows/CoverageCollection.yml index ff95257..7c0d61b 100644 --- a/.github/workflows/CoverageCollection.yml +++ b/.github/workflows/CoverageCollection.yml @@ -35,6 +35,16 @@ on: required: false default: '-r tests/requirements.txt' type: string + unittest_directory: + description: 'Path to the directory containing unit tests.' + required: false + default: 'tests/unit' + type: string + coverage_config: + description: 'Path to the .coveragerc file. Use pyproject.toml if empty.' + required: false + default: '' + type: string artifact: description: 'Name of the coverage artifact.' required: true @@ -67,7 +77,8 @@ jobs: - name: Collect coverage continue-on-error: true run: | - python -m pytest -rA --cov=.. --cov-config=tests/.coveragerc tests/unit --color=yes + [ 'x${{ inputs.coverage_config }}' != 'x' ] && PYCOV_ARGS='--cov-config=${{ inputs.coverage_config }}' || unset PYCOV_ARGS + python -m pytest -rA --cov=.. $PYCOV_ARGS ${{ inputs.unittest_directory }} --color=yes - name: Convert to cobertura format run: coverage xml @@ -75,14 +86,14 @@ jobs: - name: Convert to HTML format run: | coverage html - rm htmlcov/.gitignore + rm report/coverage/html/.gitignore - name: 📤 Upload 'Coverage Report' artifact continue-on-error: true uses: actions/upload-artifact@v2 with: name: ${{ inputs.artifact }} - path: htmlcov + path: report/coverage/html if-no-files-found: error retention-days: 1 @@ -90,7 +101,7 @@ jobs: continue-on-error: true uses: codecov/codecov-action@v1 with: - file: ./coverage.xml + file: report/coverage/coverage.xml flags: unittests env_vars: PYTHON @@ -99,4 +110,4 @@ jobs: uses: codacy/codacy-coverage-reporter-action@master with: project-token: ${{ secrets.codacy_token }} - coverage-reports: ./coverage.xml + coverage-reports: report/coverage/coverage.xml diff --git a/.github/workflows/UnitTesting.yml b/.github/workflows/UnitTesting.yml index 4155898..ada82b4 100644 --- a/.github/workflows/UnitTesting.yml +++ b/.github/workflows/UnitTesting.yml @@ -34,6 +34,11 @@ on: required: false default: '-r tests/requirements.txt' type: string + unittest_directory: + description: 'Path to the directory containing unit tests.' + required: false + default: 'tests/unit' + type: string artifact: description: "Generate unit test report with junitxml and upload results as an artifact." required: false @@ -68,7 +73,7 @@ jobs: - name: ☑ Run unit tests run: | [ 'x${{ inputs.artifact }}' != 'x' ] && PYTEST_ARGS='--junitxml=TestReport.xml' || unset PYTEST_ARGS - python -m pytest -rA tests/unit $PYTEST_ARGS --color=yes + python -m pytest -rA ${{ inputs.unittest_directory }} $PYTEST_ARGS --color=yes - name: 📤 Upload 'TestReport.xml' artifact if: inputs.artifact != '' From 6ad23eabf5585aac3f30aabfa964a643e20ad754 Mon Sep 17 00:00:00 2001 From: Patrick Lehmann Date: Tue, 21 Dec 2021 22:48:44 +0100 Subject: [PATCH 02/13] Extract information from TOML file. --- .github/workflows/CoverageCollection.yml | 28 ++++++++++++++++++++---- .github/workflows/Parameters.yml | 2 +- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/workflows/CoverageCollection.yml b/.github/workflows/CoverageCollection.yml index 7c0d61b..8d0a0e8 100644 --- a/.github/workflows/CoverageCollection.yml +++ b/.github/workflows/CoverageCollection.yml @@ -64,6 +64,26 @@ jobs: - name: ⏬ Checkout repository uses: actions/checkout@v2 + - name: 🔁 Extract configurations from pyproject.toml + id: getVariables + run: | + function ReadToml() { + state=0; + RegExp="$2 = \"(.*)\"" + while IFS=$'\r\n' read -r Line; do + if [[ $state -eq 0 && "$Line" == "[$1]" ]]; then + state=1; + elif [[ $state -eq 1 && "$Line" =~ $RegExp ]]; then + echo "${BASH_REMATCH[1]}"; + break; + fi + done < <(cat "pyproject.toml") + } + + # write to step outputs + echo ::set-output name=coverage_report_html_directory::$(ReadToml "tool.coverage.html" "directory") + echo ::set-output name=coverage_report_xml::$(ReadToml "tool.coverage.xml" "output") + - name: 🐍 Setup Python ${{ inputs.python_version }} uses: actions/setup-python@v2 with: @@ -86,14 +106,14 @@ jobs: - name: Convert to HTML format run: | coverage html - rm report/coverage/html/.gitignore + rm ${{ steps.getVariables.outputs.coverage_report_html_directory }}/.gitignore - name: 📤 Upload 'Coverage Report' artifact continue-on-error: true uses: actions/upload-artifact@v2 with: name: ${{ inputs.artifact }} - path: report/coverage/html + path: ${{ steps.getVariables.outputs.coverage_report_html_directory }} if-no-files-found: error retention-days: 1 @@ -101,7 +121,7 @@ jobs: continue-on-error: true uses: codecov/codecov-action@v1 with: - file: report/coverage/coverage.xml + file: ${{ steps.getVariables.outputs.coverage_report_xml }} flags: unittests env_vars: PYTHON @@ -110,4 +130,4 @@ jobs: uses: codacy/codacy-coverage-reporter-action@master with: project-token: ${{ secrets.codacy_token }} - coverage-reports: report/coverage/coverage.xml + coverage-reports: ${{ steps.getVariables.outputs.coverage_report_xml }} diff --git a/.github/workflows/Parameters.yml b/.github/workflows/Parameters.yml index f7b217d..6892cd9 100644 --- a/.github/workflows/Parameters.yml +++ b/.github/workflows/Parameters.yml @@ -76,7 +76,7 @@ jobs: print(params) data = { - '3.6': { 'icon': '🔴', 'until': '23.12.2021' }, + '3.6': { 'icon': '🔴', 'until': '23.12.2021' }, # Black circle ⚫ for EOL versions. '3.7': { 'icon': '🟠', 'until': '27.06.2023' }, '3.8': { 'icon': '🟡', 'until': 'Oct. 2024' }, '3.9': { 'icon': '🟢', 'until': 'Oct. 2025' }, From 09f7504de4d56770cbe6772eb7bbd8872a984448 Mon Sep 17 00:00:00 2001 From: Patrick Lehmann Date: Tue, 21 Dec 2021 23:17:33 +0100 Subject: [PATCH 03/13] Fixed path to project root. --- .github/workflows/CoverageCollection.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CoverageCollection.yml b/.github/workflows/CoverageCollection.yml index 8d0a0e8..09ee31a 100644 --- a/.github/workflows/CoverageCollection.yml +++ b/.github/workflows/CoverageCollection.yml @@ -98,7 +98,7 @@ jobs: continue-on-error: true run: | [ 'x${{ inputs.coverage_config }}' != 'x' ] && PYCOV_ARGS='--cov-config=${{ inputs.coverage_config }}' || unset PYCOV_ARGS - python -m pytest -rA --cov=.. $PYCOV_ARGS ${{ inputs.unittest_directory }} --color=yes + python -m pytest -rA --cov=. $PYCOV_ARGS ${{ inputs.unittest_directory }} --color=yes - name: Convert to cobertura format run: coverage xml From 9dfafd588e906a04729797d4f4ff464ed188b6d0 Mon Sep 17 00:00:00 2001 From: Patrick Lehmann Date: Wed, 22 Dec 2021 14:29:09 +0100 Subject: [PATCH 04/13] Changed scripting from bash to Python. Also use .coveragerc as fallback. --- .github/workflows/CoverageCollection.yml | 47 ++++++++++++++++-------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/.github/workflows/CoverageCollection.yml b/.github/workflows/CoverageCollection.yml index 09ee31a..6cdf29a 100644 --- a/.github/workflows/CoverageCollection.yml +++ b/.github/workflows/CoverageCollection.yml @@ -66,23 +66,40 @@ jobs: - name: 🔁 Extract configurations from pyproject.toml id: getVariables + shell: python run: | - function ReadToml() { - state=0; - RegExp="$2 = \"(.*)\"" - while IFS=$'\r\n' read -r Line; do - if [[ $state -eq 0 && "$Line" == "[$1]" ]]; then - state=1; - elif [[ $state -eq 1 && "$Line" =~ $RegExp ]]; then - echo "${BASH_REMATCH[1]}"; - break; - fi - done < <(cat "pyproject.toml") - } + from pathlib import Path + from tomli import load as tomli_load - # write to step outputs - echo ::set-output name=coverage_report_html_directory::$(ReadToml "tool.coverage.html" "directory") - echo ::set-output name=coverage_report_xml::$(ReadToml "tool.coverage.xml" "output") + coverageRC = "${{ inputs.coverage_config }}".strip() + + # Read output paths from 'pyproject.toml' file + if coverageRC == "": + pyProjectFile = Path("pyproject.toml") + if pyProjectFile.exists(): + with pyProjectFile.open("rb") as file: + pyProjectSettings = tomli_load(file) + + htmlDirectory = pyProjectSettings["tool.coverage.html"]["directory"] + xmlFile = pyProjectSettings["tool.coverage.xml"]["output"] + else: + print(f"File '{pyProjectFile}' not found and no ' .coveragerc' file specified.") + + # Read output paths from '.coveragerc' file + else: + coverageRCFile = Path(coverageRC) + if coverageRCFile.exists(): + with coverageRCFile.open("rb") as file: + coverageRCSettings = tomli_load(file) + + htmlDirectory = pyProjectSettings["html"]["directory"] + xmlFile = pyProjectSettings["xml"]["output"] + else: + print(f"File '{coverageRCFile}' not found.") + + print(f"::set-output name=coverage_report_html_directory::{htmlDirectory}") + print(f"::set-output name=coverage_report_xml::{xmlFile}") + print(f"DEBUG:\n html={htmlDirectory}\n xml={xmlFile}") - name: 🐍 Setup Python ${{ inputs.python_version }} uses: actions/setup-python@v2 From fa10ed076ca7cb2985b827a78fc23e0a95678170 Mon Sep 17 00:00:00 2001 From: Patrick Lehmann Date: Fri, 24 Dec 2021 12:51:38 +0100 Subject: [PATCH 05/13] Install dependency `tomli` before script execution. --- .github/workflows/CoverageCollection.yml | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/CoverageCollection.yml b/.github/workflows/CoverageCollection.yml index 6cdf29a..71dabf5 100644 --- a/.github/workflows/CoverageCollection.yml +++ b/.github/workflows/CoverageCollection.yml @@ -64,6 +64,17 @@ jobs: - name: ⏬ Checkout repository uses: actions/checkout@v2 + - name: 🐍 Setup Python ${{ inputs.python_version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ inputs.python_version }} + + - name: 🗂 Install dependencies + run: | + python -m pip install -U pip + python -m pip install tomli + python -m pip install ${{ inputs.requirements }} + - name: 🔁 Extract configurations from pyproject.toml id: getVariables shell: python @@ -101,16 +112,6 @@ jobs: print(f"::set-output name=coverage_report_xml::{xmlFile}") print(f"DEBUG:\n html={htmlDirectory}\n xml={xmlFile}") - - name: 🐍 Setup Python ${{ inputs.python_version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ inputs.python_version }} - - - name: 🗂 Install dependencies - run: | - python -m pip install -U pip - python -m pip install ${{ inputs.requirements }} - - name: Collect coverage continue-on-error: true run: | From d7c765ba7919d0fa9f411b3f616bcb4bbee4a09a Mon Sep 17 00:00:00 2001 From: Patrick Lehmann Date: Fri, 24 Dec 2021 13:04:31 +0100 Subject: [PATCH 06/13] Fixed how to access complex nested key-value pairs. --- .github/workflows/CoverageCollection.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CoverageCollection.yml b/.github/workflows/CoverageCollection.yml index 71dabf5..fa72e8c 100644 --- a/.github/workflows/CoverageCollection.yml +++ b/.github/workflows/CoverageCollection.yml @@ -91,8 +91,8 @@ jobs: with pyProjectFile.open("rb") as file: pyProjectSettings = tomli_load(file) - htmlDirectory = pyProjectSettings["tool.coverage.html"]["directory"] - xmlFile = pyProjectSettings["tool.coverage.xml"]["output"] + htmlDirectory = pyProjectSettings["tool"]["coverage"]["html"]["directory"] + xmlFile = pyProjectSettings["tool"]["coverage"]["xml"]["output"] else: print(f"File '{pyProjectFile}' not found and no ' .coveragerc' file specified.") From 925b44a8a8f8ed07cb04304da1bce5513173ff75 Mon Sep 17 00:00:00 2001 From: umarcor Date: Fri, 24 Dec 2021 15:54:09 +0100 Subject: [PATCH 07/13] CoverageCollection: fix variable name --- .github/workflows/CoverageCollection.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/CoverageCollection.yml b/.github/workflows/CoverageCollection.yml index fa72e8c..37be0b2 100644 --- a/.github/workflows/CoverageCollection.yml +++ b/.github/workflows/CoverageCollection.yml @@ -81,7 +81,7 @@ jobs: run: | from pathlib import Path from tomli import load as tomli_load - + coverageRC = "${{ inputs.coverage_config }}".strip() # Read output paths from 'pyproject.toml' file @@ -90,24 +90,24 @@ jobs: if pyProjectFile.exists(): with pyProjectFile.open("rb") as file: pyProjectSettings = tomli_load(file) - + htmlDirectory = pyProjectSettings["tool"]["coverage"]["html"]["directory"] xmlFile = pyProjectSettings["tool"]["coverage"]["xml"]["output"] else: print(f"File '{pyProjectFile}' not found and no ' .coveragerc' file specified.") - + # Read output paths from '.coveragerc' file else: coverageRCFile = Path(coverageRC) if coverageRCFile.exists(): with coverageRCFile.open("rb") as file: coverageRCSettings = tomli_load(file) - - htmlDirectory = pyProjectSettings["html"]["directory"] - xmlFile = pyProjectSettings["xml"]["output"] + + htmlDirectory = coverageRCSettings["html"]["directory"] + xmlFile = coverageRCSettings["xml"]["output"] else: print(f"File '{coverageRCFile}' not found.") - + print(f"::set-output name=coverage_report_html_directory::{htmlDirectory}") print(f"::set-output name=coverage_report_xml::{xmlFile}") print(f"DEBUG:\n html={htmlDirectory}\n xml={xmlFile}") From 9bd8004dfb5112e61616e119de5cd52374c2dd86 Mon Sep 17 00:00:00 2001 From: umarcor Date: Fri, 24 Dec 2021 15:59:23 +0100 Subject: [PATCH 08/13] CoverageCollection: pass output directory to coverage html --- .github/workflows/CoverageCollection.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CoverageCollection.yml b/.github/workflows/CoverageCollection.yml index 37be0b2..9946179 100644 --- a/.github/workflows/CoverageCollection.yml +++ b/.github/workflows/CoverageCollection.yml @@ -123,7 +123,7 @@ jobs: - name: Convert to HTML format run: | - coverage html + coverage html -d ${{ steps.getVariables.outputs.coverage_report_html_directory }} rm ${{ steps.getVariables.outputs.coverage_report_html_directory }}/.gitignore - name: 📤 Upload 'Coverage Report' artifact From 62cd2d1d0f499b1ba5a11ba2d4f9ed5db033f8b3 Mon Sep 17 00:00:00 2001 From: umarcor Date: Fri, 24 Dec 2021 16:22:11 +0100 Subject: [PATCH 09/13] CoverageCollection: htmlDirectory defaults to 'htmlcov' --- .github/workflows/CoverageCollection.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/CoverageCollection.yml b/.github/workflows/CoverageCollection.yml index 9946179..3dacfe4 100644 --- a/.github/workflows/CoverageCollection.yml +++ b/.github/workflows/CoverageCollection.yml @@ -82,6 +82,7 @@ jobs: from pathlib import Path from tomli import load as tomli_load + htmlDirectory = 'htmlcov' coverageRC = "${{ inputs.coverage_config }}".strip() # Read output paths from 'pyproject.toml' file From b8564eb389e4c86a8c0cbb11bd240dde202b94e6 Mon Sep 17 00:00:00 2001 From: umarcor Date: Fri, 24 Dec 2021 16:24:55 +0100 Subject: [PATCH 10/13] CoverageCollection: xmlFile defaults to './coverage.xml' --- .github/workflows/CoverageCollection.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/CoverageCollection.yml b/.github/workflows/CoverageCollection.yml index 3dacfe4..d42621d 100644 --- a/.github/workflows/CoverageCollection.yml +++ b/.github/workflows/CoverageCollection.yml @@ -83,6 +83,7 @@ jobs: from tomli import load as tomli_load htmlDirectory = 'htmlcov' + xmlFile = './coverage.xml' coverageRC = "${{ inputs.coverage_config }}".strip() # Read output paths from 'pyproject.toml' file From 9846c9e60c4f8d3b1edf29eaebbb2e220ee0af5c Mon Sep 17 00:00:00 2001 From: umarcor Date: Fri, 24 Dec 2021 16:31:47 +0100 Subject: [PATCH 11/13] CoverageCollection: use 'pyproject.toml' by default --- .github/workflows/CoverageCollection.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/CoverageCollection.yml b/.github/workflows/CoverageCollection.yml index d42621d..cfccc79 100644 --- a/.github/workflows/CoverageCollection.yml +++ b/.github/workflows/CoverageCollection.yml @@ -41,9 +41,9 @@ on: default: 'tests/unit' type: string coverage_config: - description: 'Path to the .coveragerc file. Use pyproject.toml if empty.' + description: 'Path to the .coveragerc file. Use pyproject.toml by default.' required: false - default: '' + default: 'pyproject.toml' type: string artifact: description: 'Name of the coverage artifact.' @@ -87,7 +87,7 @@ jobs: coverageRC = "${{ inputs.coverage_config }}".strip() # Read output paths from 'pyproject.toml' file - if coverageRC == "": + if coverageRC == "pyproject.toml": pyProjectFile = Path("pyproject.toml") if pyProjectFile.exists(): with pyProjectFile.open("rb") as file: From 1fbeef36d6b993772691898bfa10f29bc6ea85ac Mon Sep 17 00:00:00 2001 From: umarcor Date: Fri, 24 Dec 2021 16:34:56 +0100 Subject: [PATCH 12/13] CoverageCollection: skip config file if empty --- .github/workflows/CoverageCollection.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CoverageCollection.yml b/.github/workflows/CoverageCollection.yml index cfccc79..f5bde28 100644 --- a/.github/workflows/CoverageCollection.yml +++ b/.github/workflows/CoverageCollection.yml @@ -99,7 +99,7 @@ jobs: print(f"File '{pyProjectFile}' not found and no ' .coveragerc' file specified.") # Read output paths from '.coveragerc' file - else: + elif len(coverageRC) > 0: coverageRCFile = Path(coverageRC) if coverageRCFile.exists(): with coverageRCFile.open("rb") as file: From 78b225195f34f56c482dc9dac8407570af6e1beb Mon Sep 17 00:00:00 2001 From: Patrick Lehmann Date: Fri, 24 Dec 2021 21:05:04 +0100 Subject: [PATCH 13/13] Updated README according to latest changes. --- README.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 120ed78..495acfc 100644 --- a/README.md +++ b/README.md @@ -95,13 +95,16 @@ As shown in the screenshot above, the expected order is: - Global: - [Parameters](.github/workflows/Parameters.yml): a workaround for the limitations to handle global variables in GitHub Actions workflows (see [actions/runner#480](https://github.com/actions/runner/issues/480)). - It generates outputs with artifact names and job matrices to be used in other jobs. + It generates outputs with artifact names and job matrices to be used in later running jobs. - Code testing/analysis: - [UnitTesting](.github/workflows/UnitTesting.yml): run unit test with `pytest` using multiple versions of Python, and - optionally upload results as XML reports. - - [CoverageCollection](.github/workflows/CoverageCollection.yml): collect coverage data with `pytest` using a single - version of Python, generate HTML and Cobertura (XML) reports, upload the HTML report as an artifact, and upload the - results to Codecov and Codacy. + optionally upload results as XML reports. Configuration options to `pytest` should be given via section + `[tool.pytest.ini_options]` in a `pyproject.toml` file. + - [CoverageCollection](.github/workflows/CoverageCollection.yml): collect code coverage data (incl. branch coverage) + with `pytest`/`pytest-cov`/`coverage.py` using a single version of Python (latest). It generates HTML and Cobertura + (XML)reports, upload the HTML report as an artifact, and upload the test results to Codecov and Codacy. Configuration + options to `pytest` and `coverage.py` should be given via section `[tool.pytest.ini_options]` and `[tool.coverage.*]` + in a `pyproject.toml` file. - [StaticTypeCheck](.github/workflows/StaticTypeCheck.yml): collect static type check result with `mypy`, and optionally upload results as an HTML report. Example `commands`: @@ -128,7 +131,7 @@ As shown in the screenshot above, the expected order is: mypy --html-report ../htmlmypy -p ToolName ``` - - [VerifyDocs](.github/workflows/VerifyDocs.yml): extract code examples from the README and test. + - [VerifyDocs](.github/workflows/VerifyDocs.yml): extract code examples from the README and test these code snippets. - Packaging and releasing: - [Release](.github/workflows/Release.yml): publish GitHub Release. - [Package](.github/workflows/Package.yml): generate source and wheel packages, and upload them as an artifact.