diff --git a/.github/workflows/docs-tests.yml b/.github/workflows/docs-tests.yml new file mode 100644 index 00000000..1b78e877 --- /dev/null +++ b/.github/workflows/docs-tests.yml @@ -0,0 +1,62 @@ +name: Documentation Tests + +on: + push: + branches: [main] + paths: + - 'docs/**' + - 'diff_diff/**' + - 'tests/test_doc_snippets.py' + # tests/conftest.py is auto-loaded by pytest for the snippet + # test run and mutates sys.path + MPLBACKEND (conftest.py:14, 18); + # changes there can break snippet exec without touching the test + # file itself. + - 'tests/conftest.py' + - 'pyproject.toml' + - '.github/workflows/docs-tests.yml' + pull_request: + branches: [main] + types: [opened, synchronize, reopened, labeled, unlabeled] + paths: + - 'docs/**' + - 'diff_diff/**' + - 'tests/test_doc_snippets.py' + - 'tests/conftest.py' + - 'pyproject.toml' + - '.github/workflows/docs-tests.yml' + schedule: + # Weekly Sunday 6am UTC - smoke test that snippets still execute + # against current upstream deps (mirrors notebooks.yml schedule). + - cron: '0 6 * * 0' + +permissions: + contents: read + +jobs: + doc-snippets: + name: Validate RST code snippets + if: >- + github.event_name != 'pull_request' + || contains(github.event.pull_request.labels.*.name, 'ready-for-ci') + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Set up Python + uses: actions/setup-python@v6 + with: + # 3.14 to mirror Pure Python Fallback (the only existing job + # that actually ran these tests). notebooks.yml uses 3.11 for + # nbmake compat, not relevant here. + python-version: '3.14' + + - name: Install dependencies + # Keep in sync with pyproject.toml [project.dependencies] and [project.optional-dependencies.dev] + run: pip install numpy pandas scipy pytest + + - name: Run doc snippet tests in pure Python mode + # PYTHONPATH=. lets the test import diff_diff directly from + # source without invoking the maturin/Rust build (mirrors Pure + # Python Fallback at rust-test.yml:189-193). + run: PYTHONPATH=. DIFF_DIFF_BACKEND=python pytest tests/test_doc_snippets.py -v diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index 186075fb..917390d0 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -7,6 +7,9 @@ on: - 'rust/**' - 'diff_diff/**' - 'tests/**' + # tests/test_doc_snippets.py is owned by docs-tests.yml; exclude it + # so a harness-only edit does not fan out into the Rust matrix. + - '!tests/test_doc_snippets.py' - 'pyproject.toml' - '.github/workflows/rust-test.yml' pull_request: @@ -16,6 +19,7 @@ on: - 'rust/**' - 'diff_diff/**' - 'tests/**' + - '!tests/test_doc_snippets.py' - 'pyproject.toml' - '.github/workflows/rust-test.yml' @@ -155,14 +159,18 @@ jobs: - name: Run tests with Rust backend (Unix) if: runner.os != 'Windows' working-directory: /tmp - run: DIFF_DIFF_BACKEND=rust pytest tests/ -q -n auto --dist worksteal -m '' + # Doc snippet tests own .github/workflows/docs-tests.yml; ignore + # them here to keep one workflow per surface and avoid double + # execution (the matrix copies tests/ to /tmp/tests without + # docs/, so this ignore is defensive on the Rust path). + run: DIFF_DIFF_BACKEND=rust pytest tests/ --ignore=tests/test_doc_snippets.py -q -n auto --dist worksteal -m '' - name: Run tests with Rust backend (Windows) if: runner.os == 'Windows' working-directory: ${{ runner.temp }} run: | $env:DIFF_DIFF_BACKEND="rust" - pytest tests/ -q -n auto --dist worksteal -m '' + pytest tests/ --ignore=tests/test_doc_snippets.py -q -n auto --dist worksteal -m '' shell: pwsh # Test pure Python fallback (without Rust extension) @@ -190,4 +198,8 @@ jobs: PYTHONPATH=. python -c "from diff_diff import HAS_RUST_BACKEND; print(f'HAS_RUST_BACKEND: {HAS_RUST_BACKEND}'); assert not HAS_RUST_BACKEND" - name: Run tests in pure Python mode - run: PYTHONPATH=. DIFF_DIFF_BACKEND=python pytest tests/ -q --ignore=tests/test_rust_backend.py -n auto --dist worksteal + # Doc snippet tests own .github/workflows/docs-tests.yml; ignore + # them here to keep one workflow per surface (this is the only + # invocation that actually executes test_doc_snippets.py since + # this job runs from the repo root, not /tmp/tests). + run: PYTHONPATH=. DIFF_DIFF_BACKEND=python pytest tests/ -q --ignore=tests/test_rust_backend.py --ignore=tests/test_doc_snippets.py -n auto --dist worksteal diff --git a/TODO.md b/TODO.md index 1d6cd918..eee1cf1c 100644 --- a/TODO.md +++ b/TODO.md @@ -120,7 +120,7 @@ Deferred items from PR reviews that were not addressed before merge. |-------|----------|----|----------| | ImputationDiD event-study SEs recompute full conservative variance per horizon (should cache A0/A1 factorization) | `imputation.py` | #141 | Low | | Rust faer SVD ndarray-to-faer conversion overhead (minimal vs SVD cost) | `rust/src/linalg.rs:67` | #115 | Low | -| Unrelated label events (e.g., adding `bug` label) re-trigger CI workflows when `ready-for-ci` is already present; filter `labeled`/`unlabeled` events to only `ready-for-ci` transitions | `.github/workflows/rust-test.yml`, `notebooks.yml` | #269 | Low | +| Unrelated label events (e.g., adding `bug` label) re-trigger CI workflows when `ready-for-ci` is already present; filter `labeled`/`unlabeled` events to only `ready-for-ci` transitions | `.github/workflows/rust-test.yml`, `notebooks.yml`, `docs-tests.yml` | #269 | Low | | `bread_inv` as a performance kwarg on `compute_robust_vcov` to avoid re-inverting `(X'WX)` when the caller already has it. Deferred from Phase 1a for scope. HC2 and HC2+BM both need the bread inverse, so a shared hint would save one `np.linalg.solve` per sandwich. | `linalg.py::compute_robust_vcov` | Phase 1a | Low | | Rust-backend HC2 implementation. Current Rust path only supports HC1; HC2 and CR2 Bell-McCaffrey fall through to the NumPy backend. For large-n fits this is noticeable. | `rust/src/linalg.rs` | Phase 1a | Low | | CR2 Bell-McCaffrey DOF uses a naive `O(n² k)` per-coefficient loop over cluster pairs. Pustejovsky-Tipton (2018) Appendix B has a scores-based formulation that avoids the full `n × n` `M` matrix. Switch when a user hits a large-`n` cluster-robust design. | `linalg.py::_compute_cr2_bm` | Phase 1a | Low |