Test #62
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: Test | |
| on: | |
| pull_request: | |
| branches: | |
| - "*" | |
| push: | |
| branches: | |
| - main | |
| workflow_call: | |
| workflow_dispatch: | |
| jobs: | |
| test: | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| activate-environment: "true" | |
| - name: Install the project | |
| run: uv sync --dev | |
| - name: Run pre-commit hooks | |
| run: uv run pre-commit run --all-files | |
| - name: Run unit tests | |
| run: uv run pytest tests/unit/ --junitxml=test-results/unit.xml | |
| - name: Run integration tests | |
| run: uv run pytest tests/integration/ --integration --junitxml=test-results/integration.xml | |
| continue-on-error: true | |
| - name: Collect results | |
| if: always() | |
| shell: python | |
| run: | | |
| import json, os, subprocess, xml.etree.ElementTree as ET | |
| def parse_xml(path): | |
| try: | |
| root = ET.parse(path).getroot() | |
| suite = root if root.tag == "testsuite" else root.find("testsuite") | |
| t = int(suite.get("tests", 0)) | |
| f = int(suite.get("failures", 0)) | |
| e = int(suite.get("errors", 0)) | |
| s = int(suite.get("skipped", 0)) | |
| return {"tests": t, "failures": f, "errors": e, | |
| "skipped": s, "passed": t - f - e - s, | |
| "time": float(suite.get("time", 0))} | |
| except (FileNotFoundError, ET.ParseError): | |
| return None | |
| try: | |
| audit = subprocess.check_output( | |
| ["uv", "audit"], text=True, stderr=subprocess.STDOUT | |
| ).strip() | |
| except subprocess.CalledProcessError as ex: | |
| audit = (ex.output or "").strip() or "audit failed" | |
| data = { | |
| "version": "${{ matrix.python-version }}", | |
| "unit": parse_xml("test-results/unit.xml"), | |
| "integration": parse_xml("test-results/integration.xml"), | |
| "audit": audit, | |
| } | |
| os.makedirs("results", exist_ok=True) | |
| with open("results/${{ matrix.python-version }}.json", "w") as fp: | |
| json.dump(data, fp) | |
| - name: Upload results artifact | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-results-${{ matrix.python-version }} | |
| path: results/ | |
| retention-days: 1 | |
| # ── Combine all matrix results into one summary table ──────────────────── | |
| summary: | |
| needs: test | |
| if: always() | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| steps: | |
| - name: Download all results | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: test-results-* | |
| path: results/ | |
| merge-multiple: true | |
| - name: Build summary table | |
| shell: python | |
| run: | | |
| import glob, json, os | |
| ORDER = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] | |
| results = {} | |
| for f in glob.glob("results/*.json"): | |
| with open(f) as fp: | |
| d = json.load(fp) | |
| results[d["version"]] = d | |
| if not results: | |
| exit(0) | |
| versions = sorted(results, key=lambda v: ORDER.index(v) if v in ORDER else 99) | |
| def col_icon(d): | |
| if d is None: | |
| return "⚪" | |
| return "✅" if not (d["failures"] or d["errors"]) else "❌" | |
| def make_table(title, key): | |
| cols = [results[v][key] for v in versions] | |
| if all(d is None for d in cols): | |
| return [] | |
| header_vals = " | ".join(f"{col_icon(d)} **{v}**" for v, d in zip(versions, cols)) | |
| sep = " | ".join(":---:" for _ in versions) | |
| def row(label, field, fmt=str): | |
| cells = [fmt(d[field]) if d is not None else "—" for d in cols] | |
| return f"| {label} | {' | '.join(cells)} |" | |
| return [ | |
| f"### {title}", | |
| "", | |
| f"| | {header_vals} |", | |
| f"|---|{sep}|", | |
| row("✅ Passed", "passed"), | |
| row("❌ Failed", "failures"), | |
| row("💥 Errors", "errors"), | |
| row("⏭ Skipped", "skipped"), | |
| row("**Total**", "tests"), | |
| row("⏱ Time", "time", lambda t: f"{t:.1f}s"), | |
| "", | |
| ] | |
| lines = ["## 🧪 Test Results", ""] | |
| lines += make_table("Unit Tests", "unit") | |
| lines += make_table("Integration Tests", "integration") | |
| # audit — take from the highest Python version available | |
| for v in reversed(versions): | |
| audit = results[v].get("audit", "").strip() | |
| if audit: | |
| lines += ["### 🔒 Security Audit", "", "```", audit, "```", ""] | |
| break | |
| with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as fp: | |
| fp.write("\n".join(lines) + "\n") |