Merge branch 'master' of https://github.com/elkins/chempy #52
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: CI | |
| on: | |
| push: | |
| branches: [ master ] | |
| pull_request: | |
| branches: [ master ] | |
| permissions: | |
| contents: read | |
| actions: read | |
| jobs: | |
| test-and-type: | |
| runs-on: macos-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e . | |
| pip install pytest pytest-benchmark mypy | |
| # Open Babel wheel for macOS/Linux | |
| pip install openbabel-wheel || true | |
| - name: Type check (strict for key modules) | |
| run: | | |
| mypy chempy/graph.py chempy/molecule.py --show-error-codes --check-untyped-defs | |
| - name: Run tests and save benchmarks | |
| run: | | |
| # Create a dummy benchmark that has no dependencies to verify pytest-benchmark is working | |
| echo "def test_simple_bench(benchmark): benchmark(lambda: sum(range(100)))" > benchmarks/test_simple_bench.py | |
| # Print environment info for debugging | |
| python --version | |
| pip list | grep -E "pytest|benchmark|openbabel" | |
| python -c "import openbabel; print('OpenBabel OK') if hasattr(openbabel, 'OBConversion') else print('OpenBabel partial')" || echo "OpenBabel import failed" | |
| # Run pytest with explicit config and multiple output formats | |
| # We use --benchmark-json to force a file creation we can definitely find | |
| python -m pytest -v tests/ unittest/ benchmarks/ --benchmark-autosave --benchmark-json=benchmarks-result.json | |
| - name: List benchmark files (diagnostic) | |
| run: | | |
| echo "Checking for .benchmarks directory:" | |
| ls -R .benchmarks || echo ".benchmarks directory not found" | |
| echo "Checking for explicit JSON output:" | |
| ls -l benchmarks-result.json || echo "benchmarks-result.json not found" | |
| echo "Searching for any JSON files generated:" | |
| find . -name "*.json" -not -path "./.git/*" | |
| - name: Upload benchmark artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: benchmarks-json | |
| path: | | |
| .benchmarks/ | |
| benchmarks-result.json | |
| if-no-files-found: warn | |
| compare-artifacts: | |
| runs-on: macos-latest | |
| needs: test-and-type | |
| env: | |
| REGRESS_THRESHOLD_OPS: "-10.0" | |
| REGRESS_THRESHOLD_MEAN: "-10.0" | |
| REGRESS_THRESHOLD_MEDIAN: "-10.0" | |
| REGRESS_FILTER_REGEX: "" | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.12' | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e . | |
| pip install pytest pytest-benchmark | |
| - name: Download benchmark artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: benchmarks-json | |
| path: .benchmarks | |
| - name: Generate CSV and JSON comparisons | |
| shell: bash | |
| run: | | |
| # Latest summary JSON for 'states' benchmarks | |
| python3 scripts/compare_benchmarks.py --latest 1 --regex 'states_' --output json --save benchmark_states.json || true | |
| # Two-run comparison CSV for molecule benchmarks | |
| python3 scripts/compare_benchmarks.py --latest 2 --group molecule --output csv --save benchmark_molecule.csv || true | |
| - name: Fail on significant performance regressions | |
| shell: bash | |
| run: | | |
| python - <<'PY' | |
| import csv, sys, os, re | |
| try: | |
| with open('benchmark_molecule.csv', newline='') as f: | |
| reader = csv.DictReader(f) | |
| worst_ops = 0.0 | |
| worst_mean = 0.0 | |
| worst_median = 0.0 | |
| rows = list(reader) | |
| # Optional name regex filter | |
| pattern = os.environ.get('REGRESS_FILTER_REGEX', '') | |
| if pattern: | |
| try: | |
| rx = re.compile(pattern) | |
| rows = [r for r in rows if rx.search(r.get('name',''))] | |
| except re.error: | |
| print(f"Invalid REGRESS_FILTER_REGEX: {pattern}") | |
| for r in rows: | |
| for key, target in ( | |
| ('ops_delta', 'ops'), | |
| ('mean_delta', 'mean'), | |
| ('median_delta', 'median'), | |
| ): | |
| s = r.get(key, '') | |
| if s.endswith('%'): | |
| val = float(s.strip('%')) | |
| if target == 'ops' and val < worst_ops: | |
| worst_ops = val | |
| if target == 'mean' and val < worst_mean: | |
| worst_mean = val | |
| if target == 'median' and val < worst_median: | |
| worst_median = val | |
| def get_thr(name, default): | |
| try: | |
| return float(os.environ.get(name, str(default))) | |
| except Exception: | |
| return default | |
| threshold_ops = get_thr('REGRESS_THRESHOLD_OPS', -10.0) | |
| threshold_mean = get_thr('REGRESS_THRESHOLD_MEAN', -10.0) | |
| threshold_median = get_thr('REGRESS_THRESHOLD_MEDIAN', -10.0) | |
| msg = ( | |
| f"Worst deltas — ops: {worst_ops:.2f}%, mean: {worst_mean:.2f}%, median: {worst_median:.2f}%" | |
| ) | |
| print(msg) | |
| if worst_ops < threshold_ops or worst_mean < threshold_mean or worst_median < threshold_median: | |
| print("Regression detected beyond thresholds.") | |
| sys.exit(1) | |
| print("No regressions beyond thresholds.") | |
| except FileNotFoundError: | |
| print('No benchmark_molecule.csv found; skipping regression check') | |
| PY | |
| - name: Upload comparison artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: benchmark-comparisons | |
| path: | | |
| benchmark_states.json | |
| benchmark_molecule.csv | |
| if-no-files-found: ignore |