Improve coverage doctest and test (#698) #1025
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
# =============================================================== | |
# 🧪 PyTest & Coverage - Quality Gate | |
# =============================================================== | |
# | |
# - runs the full test-suite across three Python versions | |
# - measures branch + line coverage (fails < 40 %) | |
# - uploads the XML/HTML coverage reports as build artifacts | |
# - (optionally) generates / commits an SVG badge - kept disabled | |
# - posts a concise per-file coverage table to the job summary | |
# - executes on every push / PR to *main* ➕ a weekly cron | |
# --------------------------------------------------------------- | |
name: Tests & Coverage | |
on: | |
push: | |
branches: ["main"] | |
pull_request: | |
branches: ["main"] | |
# schedule: | |
# - cron: '42 3 * * 1' # Monday 03:42 UTC | |
permissions: | |
contents: write # needed *only* if the badge-commit step is enabled | |
checks: write | |
actions: read | |
jobs: | |
test: | |
name: pytest (py${{ matrix.python }}) | |
runs-on: ubuntu-latest | |
strategy: | |
fail-fast: false | |
matrix: | |
python: ["3.11", "3.12"] | |
env: | |
PYTHONUNBUFFERED: "1" | |
PIP_DISABLE_PIP_VERSION_CHECK: "1" | |
steps: | |
# ----------------------------------------------------------- | |
# 0️⃣ Checkout | |
# ----------------------------------------------------------- | |
- name: ⬇️ Checkout code | |
uses: actions/checkout@v4 | |
with: | |
fetch-depth: 1 | |
# ----------------------------------------------------------- | |
# 1️⃣ Set-up Python | |
# ----------------------------------------------------------- | |
- name: 🐍 Setup Python ${{ matrix.python }} | |
uses: actions/setup-python@v5 | |
with: | |
python-version: ${{ matrix.python }} | |
cache: pip | |
# ----------------------------------------------------------- | |
# 2️⃣ Install project + dev/test dependencies | |
# ----------------------------------------------------------- | |
- name: 📦 Install dependencies (editable + dev extra) | |
run: | | |
python3 -m pip install --upgrade pip | |
# install the project itself in *editable* mode so tests import the same codebase | |
# and pull in every dev / test extra declared in pyproject.toml | |
pip install -e .[dev] | |
# belt-and-braces - keep the core test tool-chain pinned here too | |
pip install pytest pytest-cov pytest-asyncio coverage[toml] | |
# ----------------------------------------------------------- | |
# 3️⃣ Run the tests with coverage (fail under 80% coverage) | |
# ----------------------------------------------------------- | |
- name: 🧪 Run pytest | |
run: | | |
pytest \ | |
--cov=mcpgateway \ | |
--cov-report=xml \ | |
--cov-report=html \ | |
--cov-report=term \ | |
--cov-branch \ | |
--cov-fail-under=80 | |
# ----------------------------------------------------------- | |
# 4️⃣ Run doctests (fail under 55% coverage) | |
# ----------------------------------------------------------- | |
- name: 📊 Doctest coverage with threshold | |
run: | | |
# Run doctests with coverage measurement | |
pytest --doctest-modules mcpgateway/ \ | |
--cov=mcpgateway \ | |
--cov-report=term \ | |
--cov-report=json:doctest-coverage.json \ | |
--cov-fail-under=55 \ | |
--tb=short | |
# ----------------------------------------------------------- | |
# 5️⃣ Doctest coverage check | |
# ----------------------------------------------------------- | |
- name: 📊 Doctest coverage validation | |
run: | | |
python3 -c " | |
import subprocess, sys | |
result = subprocess.run(['python', '-m', 'pytest', '--doctest-modules', 'mcpgateway/', '--tb=no', '-q'], capture_output=True) | |
if result.returncode == 0: | |
print('✅ All doctests passing') | |
else: | |
print('❌ Doctest failures detected') | |
print(result.stdout.decode()) | |
print(result.stderr.decode()) | |
sys.exit(1) | |
" | |
# ----------------------------------------------------------- | |
# 4️⃣ Upload coverage artifacts (XML + HTML) | |
# --- keep disabled unless you need them --- | |
# ----------------------------------------------------------- | |
# - name: 📤 Upload coverage.xml | |
# uses: actions/upload-artifact@v4 | |
# with: | |
# name: coverage-xml-${{ matrix.python }} | |
# path: coverage.xml | |
# | |
# - name: 📤 Upload HTML coverage | |
# uses: actions/upload-artifact@v4 | |
# with: | |
# name: htmlcov-${{ matrix.python }} | |
# path: htmlcov/ | |
# ----------------------------------------------------------- | |
# 5️⃣ Generate + commit badge (main branch, highest Python) | |
# --- intentionally commented-out --- | |
# ----------------------------------------------------------- | |
# - name: 📊 Create coverage badge | |
# if: matrix.python == '3.11' && github.ref == 'refs/heads/main' | |
# id: make_badge | |
# uses: tj-actions/coverage-badge@v2 | |
# with: | |
# coverage-file: coverage.xml # input | |
# output: .github/badges/coverage.svg # output file | |
# env: | |
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
# | |
# - name: 🚀 Commit badge | |
# if: steps.make_badge.outputs.badge-updated == 'true' | |
# uses: stefanzweifel/git-auto-commit-action@v5 | |
# with: | |
# commit_message: "docs(badge): update coverage badge" | |
# file_pattern: ".github/badges/coverage.svg" | |
# ----------------------------------------------------------- | |
# 6️⃣ Publish coverage table to the job summary | |
# ----------------------------------------------------------- | |
# - name: 📝 Coverage summary | |
# if: always() | |
# run: | | |
# echo "### Coverage - Python ${{ matrix.python }}" >> "$GITHUB_STEP_SUMMARY" | |
# echo "| File | Stmts | Miss | Branch | BrMiss | Cover |" >> "$GITHUB_STEP_SUMMARY" | |
# echo "|------|------:|-----:|-------:|-------:|------:|" >> "$GITHUB_STEP_SUMMARY" | |
# coverage json -q -o cov.json | |
# python3 - <<'PY' | |
# import json, pathlib, sys, os | |
# data = json.load(open("cov.json")) | |
# root = pathlib.Path().resolve() | |
# for f in data["files"].values(): | |
# rel = pathlib.Path(f["filename"]).resolve().relative_to(root) | |
# s = f["summary"] | |
# print(f"| {rel} | {s['num_statements']} | {s['missing_lines']} | " | |
# f"{s['num_branches']} | {s['missing_branches']} | " | |
# f"{s['percent_covered']:.1f}% |") | |
# PY >> "$GITHUB_STEP_SUMMARY" |