Improve statistics #215
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Python Tests | ||
| on: | ||
| workflow_dispatch: | ||
| push: | ||
| branches: [ main ] | ||
| pull_request: | ||
| branches: [ main ] | ||
| concurrency: | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
| cancel-in-progress: true | ||
| defaults: | ||
| run: | ||
| shell: bash | ||
| permissions: | ||
| contents: read | ||
| checks: write | ||
| pull-requests: write | ||
| jobs: | ||
| test: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| env: | ||
| COVERAGE_THRESHOLD: "85" | ||
| PIP_DISABLE_PIP_VERSION_CHECK: "1" | ||
| PIP_PROGRESS_BAR: "off" | ||
| PYTHONUNBUFFERED: "1" | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| python-version: [ '3.12', '3.13' , '3.14' ] | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - name: Set up Python ${{ matrix.python-version }} | ||
| uses: actions/setup-python@v5 | ||
| with: | ||
| python-version: ${{ matrix.python-version }} | ||
| cache: pip | ||
| cache-dependency-path: requirements.txt | ||
| - name: Install dependencies | ||
| run: | | ||
| python -m pip install --upgrade pip | ||
| python -m pip install -r requirements.txt | ||
| - name: Validate dependency metadata | ||
| run: | | ||
| python -m pip check | ||
| - name: Run pylint analysis | ||
| run: | | ||
| bash tests/python/run_pylint.sh | ||
| - name: Upload pylint reports | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: pylint-reports-${{ matrix.python-version }} | ||
| path: tests/python/pylint/reports/ | ||
| - name: Verify bytecode compilation | ||
| run: | | ||
| python -m compileall infrastructure samples setup shared tests | ||
| # Run tests with continue-on-error so that coverage and PR comments are always published. | ||
| # The final step will explicitly fail the job if any test failed, ensuring PRs cannot be merged with failing tests. | ||
| - name: Run pytest with coverage and generate JUnit XML | ||
| id: pytest | ||
| run: | | ||
| PYTHONPATH=$(pwd) COVERAGE_FILE=tests/python/.coverage-${{ matrix.python-version }} pytest --cov=shared/python --cov-config=tests/python/.coveragerc --cov-report=html:tests/python/htmlcov-${{ matrix.python-version }} --junitxml=tests/python/junit-${{ matrix.python-version }}.xml tests/python/ | ||
| continue-on-error: true | ||
| - name: Upload coverage HTML report | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: coverage-html-${{ matrix.python-version }} | ||
| path: tests/python/htmlcov-${{ matrix.python-version }}/ | ||
| if-no-files-found: warn | ||
| - name: Upload JUnit test results | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| continue-on-error: true | ||
| with: | ||
| name: junit-results-${{ matrix.python-version }} | ||
| path: tests/python/junit-${{ matrix.python-version }}.xml | ||
| if-no-files-found: warn | ||
| - name: Append pylint summary to job summary | ||
| if: always() | ||
| run: | | ||
| REPORT_JSON="tests/python/pylint/reports/latest.json" | ||
| { | ||
| echo "### Python ${{ matrix.python-version }} – Pylint summary" | ||
| echo "" | ||
| if [ -f "$REPORT_JSON" ] && command -v jq >/dev/null; then | ||
| TOTAL=$(jq 'length' "$REPORT_JSON") | ||
| echo "- Diagnostics: $TOTAL" | ||
| echo "" | ||
| jq -r ' | ||
| group_by(.type) | | ||
| map({label: (.[0].type), count: length}) | | ||
| .[] | " - \(.label): \(.count)" | ||
| ' "$REPORT_JSON" | ||
| else | ||
| echo "Pylint summary unavailable (install jq for statistics)." | ||
| fi | ||
| echo "" | ||
| echo "- Reports artifact: pylint-reports-${{ matrix.python-version }}" | ||
| echo "" | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| - name: Append coverage summary to job summary | ||
| if: always() | ||
| run: | | ||
| COVERAGE_FILE="tests/python/.coverage-${{ matrix.python-version }}" | ||
| HTML_DIR="tests/python/htmlcov-${{ matrix.python-version }}" | ||
| COVERAGE_TXT="$(mktemp)" | ||
| if [ -f "$COVERAGE_FILE" ]; then | ||
| python -m coverage report --data-file="$COVERAGE_FILE" --rcfile=tests/python/.coveragerc --skip-covered --fail-under=0 > "$COVERAGE_TXT" || true | ||
| fi | ||
| OVERALL="" | ||
| if [ -s "$COVERAGE_TXT" ]; then | ||
| OVERALL=$(tail -n 1 "$COVERAGE_TXT" | awk '{print $NF}') | ||
| fi | ||
| { | ||
| echo "### Python ${{ matrix.python-version }} – Coverage summary" | ||
| echo "" | ||
| echo "- Overall: ${OVERALL:-Unavailable}" | ||
| echo "" | ||
| echo "- HTML coverage artifact: coverage-html-${{ matrix.python-version }}" | ||
| if [ -d "$HTML_DIR" ]; then | ||
| echo "- Local path: $HTML_DIR/index.html" | ||
| fi | ||
| echo "" | ||
| } >> "$GITHUB_STEP_SUMMARY" | ||
| rm -f "$COVERAGE_TXT" | ||
| - name: Enforce coverage threshold | ||
| if: steps.pytest.outcome == 'success' | ||
| env: | ||
| COVERAGE_FILE: tests/python/.coverage-${{ matrix.python-version }} | ||
| run: | | ||
| if [ ! -f "$COVERAGE_FILE" ]; then | ||
| echo "Coverage file not found; skipping threshold enforcement." | ||
| exit 0 | ||
| fi | ||
| python - <<'PY' | ||
| import os | ||
| import re | ||
| import subprocess | ||
| import sys | ||
| threshold = float(os.getenv("COVERAGE_THRESHOLD", "0")) | ||
| coverage_file = os.environ["COVERAGE_FILE"] | ||
| rcfile = "tests/python/.coveragerc" | ||
| result = subprocess.run([ | ||
| sys.executable, | ||
| "-m", | ||
| "coverage", | ||
| "report", | ||
| f"--data-file={coverage_file}", | ||
| f"--rcfile={rcfile}", | ||
| "--skip-covered", | ||
| "--fail-under=0", | ||
| ], capture_output=True, text=True, check=False) | ||
| output = result.stdout.strip().splitlines() | ||
| if not output: | ||
| sys.exit(0) | ||
| match = re.search(r"(\d+(?:\.\d+)?)%$", output[-1]) | ||
| if not match: | ||
| sys.exit(0) | ||
| percent = float(match.group(1)) | ||
| if percent < threshold: | ||
| print(f"::error ::Coverage {percent:.2f}% is below required threshold {threshold:.2f}%.") | ||
| sys.exit(1) | ||
| print(f"Coverage {percent:.2f}% meets the threshold {threshold:.2f}%.") | ||
| PY | ||
| - name: Publish Unit Test Results to PR | ||
| if: always() | ||
| uses: EnricoMi/publish-unit-test-result-action@v2 | ||
| with: | ||
| files: tests/python/junit-${{ matrix.python-version }}.xml | ||
| comment_title: Python ${{ matrix.python-version }} Test Results | ||
| # Explicitly fail the job if any test failed (so PRs cannot be merged with failing tests). | ||
| # This runs after all reporting steps, meaning coverage and PR comments are always published. | ||
| - name: Fail if tests failed | ||
| if: steps.pytest.outcome == 'failure' | ||
| run: | | ||
| echo "::error ::Unit tests failed. See above for details." | ||
| exit 1 | ||