fix(rate-limiter): shared state, eviction, thread safety, config validation #8629
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 < 90% main, < 35% doctest) | |
| # - 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"] | |
| paths: | |
| - "mcpgateway/**" | |
| - "tests/**" | |
| - "pyproject.toml" | |
| - ".github/workflows/pytest.yml" | |
| pull_request: | |
| types: [opened, synchronize, ready_for_review] | |
| branches: ["main"] | |
| paths: | |
| - "mcpgateway/**" | |
| - "tests/**" | |
| - "pyproject.toml" | |
| - ".github/workflows/pytest.yml" | |
| # schedule: | |
| # - cron: '42 3 * * 1' # Monday 03:42 UTC | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: write # needed *only* if the badge-commit step is enabled | |
| checks: write | |
| actions: read | |
| jobs: | |
| test: | |
| if: github.event_name != 'pull_request' || !github.event.pull_request.draft | |
| name: pytest (py${{ matrix.python }}) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 40 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python: ["3.12"] | |
| env: | |
| PYTHONUNBUFFERED: "1" | |
| PIP_DISABLE_PIP_VERSION_CHECK: "1" | |
| steps: | |
| # ----------------------------------------------------------- | |
| # 0️⃣ Checkout | |
| # ----------------------------------------------------------- | |
| - name: ⬇️ Checkout code | |
| uses: actions/checkout@v5 | |
| with: | |
| fetch-depth: 0 # diff-cover needs full history to compare branches | |
| # ----------------------------------------------------------- | |
| # 1️⃣ Set-up Python | |
| # ----------------------------------------------------------- | |
| - name: 🐍 Setup Python ${{ matrix.python }} | |
| uses: actions/setup-python@v6 | |
| with: | |
| python-version: ${{ matrix.python }} | |
| cache: pip | |
| # ----------------------------------------------------------- | |
| # 2 Install uv | |
| # ----------------------------------------------------------- | |
| - name: ⚡ Install uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| version: "0.9.2" | |
| python-version: ${{ matrix.python }} | |
| # ----------------------------------------------------------- | |
| # 2.5 Setup Rust toolchain for Rust plugins | |
| # ----------------------------------------------------------- | |
| - name: 🦀 Install Rust stable | |
| run: rustup default stable | |
| - name: 📦 Cache Cargo dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cargo/registry | |
| ~/.cargo/git | |
| plugins_rust/*/target | |
| key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-cargo- | |
| - name: 🔨 Build Rust plugins (clean, install, verify stubs) | |
| run: make rust-clean-stubs && make rust-install && make rust-verify-stubs | |
| # ----------------------------------------------------------- | |
| # 3️⃣ Run the tests with coverage (fail under 95% coverage) | |
| # ----------------------------------------------------------- | |
| - name: 🧪 Run pytest | |
| run: | | |
| uv run pytest -n auto \ | |
| --durations=5 \ | |
| --ignore=tests/fuzz \ | |
| --ignore=tests/e2e/test_entra_id_integration.py \ | |
| --cov=mcpgateway \ | |
| --cov-report=xml \ | |
| --cov-report=html \ | |
| --cov-report=term \ | |
| --cov-branch \ | |
| --cov-fail-under=95 | |
| env: | |
| REQUIRE_RUST: "1" | |
| # ----------------------------------------------------------- | |
| # 3.5 Diff-cover: enforce 95% coverage on changed lines (PRs only) | |
| # ----------------------------------------------------------- | |
| - name: 📈 Diff-cover (changed lines) | |
| if: github.event_name == 'pull_request' | |
| run: | | |
| uv run diff-cover coverage.xml \ | |
| --compare-branch=origin/${{ github.base_ref }} \ | |
| --fail-under=95 | |
| # ----------------------------------------------------------- | |
| # 4️⃣ Run doctests (fail under 30% coverage) | |
| # ----------------------------------------------------------- | |
| - name: 📊 Doctest coverage with threshold | |
| run: | | |
| # Run doctests with coverage measurement | |
| uv run pytest -n auto \ | |
| --doctest-modules mcpgateway/ \ | |
| --cov=mcpgateway \ | |
| --cov-report=term \ | |
| --cov-report=json:doctest-coverage.json \ | |
| --cov-fail-under=30 \ | |
| --tb=short | |
| # ----------------------------------------------------------- | |
| # 5️⃣ Doctest coverage check | |
| # ----------------------------------------------------------- | |
| - name: 📊 Doctest coverage validation | |
| run: | | |
| uv run python3 -c " | |
| import subprocess, sys | |
| result = subprocess.run(['uv', 'run', 'python3', '-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.6.2 | |
| # with: | |
| # name: coverage-xml-${{ matrix.python }} | |
| # path: coverage.xml | |
| # | |
| # - name: 📤 Upload HTML coverage | |
| # uses: actions/upload-artifact@v4.6.2 | |
| # with: | |
| # name: htmlcov-${{ matrix.python }} | |
| # path: htmlcov/ | |
| # ----------------------------------------------------------- | |
| # 5️⃣ 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" |