From 8c1ad88cbd106ab139dc316e43108f6c4917e7ac Mon Sep 17 00:00:00 2001 From: Nathan Stender Date: Sat, 14 Mar 2026 17:04:05 -0400 Subject: [PATCH 1/3] Add single-parser test optimization to CI workflow Detect when changes are isolated to a single parser directory and run only that parser's tests, reducing CI time by 10-30x for ~30% of PRs. Falls back to running all tests when changes affect multiple parsers or shared code. Co-Authored-By: Claude Opus 4.1 --- .github/workflows/test.yml | 92 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b6fbaec6..00ca4af21 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,68 @@ permissions: contents: read jobs: + detect-changes: + runs-on: ubuntu-latest + outputs: + single-parser: ${{ steps.detect-parser.outputs.parser }} + run-all-tests: ${{ steps.detect-parser.outputs.run-all }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + with: + list-files: json + filters: | + parsers: + - 'src/allotropy/parsers/**' + non-parser: + - 'src/allotropy/!(parsers)/**' + - 'tests/!(parsers)/**' + - 'pyproject.toml' + - 'scripts/**' + - '.github/**' + - name: Detect single parser changes + id: detect-parser + run: | + if [[ "${{ steps.filter.outputs.non-parser }}" == "true" ]]; then + echo "Changes detected outside parsers, running all tests" + echo "run-all=true" >> $GITHUB_OUTPUT + echo "parser=" >> $GITHUB_OUTPUT + elif [[ "${{ steps.filter.outputs.parsers }}" == "true" ]]; then + # Get list of changed files in parsers directory + FILES=$(echo '${{ steps.filter.outputs.parsers_files }}' | jq -r '.[]') + + # Extract unique parser directories + PARSERS=$(echo "$FILES" | grep '^src/allotropy/parsers/' | cut -d'/' -f4 | sort -u) + + # Count unique parsers + PARSER_COUNT=$(echo "$PARSERS" | grep -v '^$' | wc -l) + + if [[ $PARSER_COUNT -eq 1 ]]; then + PARSER=$(echo "$PARSERS" | head -n1) + # Check if it's actually a parser directory (not utils, shared, etc) + if [[ "$PARSER" != "utils" && "$PARSER" != "__pycache__" && -d "tests/parsers/$PARSER" ]]; then + echo "Single parser detected: $PARSER" + echo "run-all=false" >> $GITHUB_OUTPUT + echo "parser=$PARSER" >> $GITHUB_OUTPUT + else + echo "Changes in shared parser utilities, running all tests" + echo "run-all=true" >> $GITHUB_OUTPUT + echo "parser=" >> $GITHUB_OUTPUT + fi + else + echo "Multiple parsers changed, running all tests" + echo "run-all=true" >> $GITHUB_OUTPUT + echo "parser=" >> $GITHUB_OUTPUT + fi + else + echo "No parser changes detected, running all tests" + echo "run-all=true" >> $GITHUB_OUTPUT + echo "parser=" >> $GITHUB_OUTPUT + fi + test_py_310: + needs: detect-changes runs-on: ubuntu-latest name: Tests (python 3.10) @@ -28,10 +89,18 @@ jobs: - name: Install click run: pip install click!=8.3.0 - name: Run Tests - run: hatch run test_all.py3.10:pytest -n 2 tests + run: | + if [[ "${{ needs.detect-changes.outputs.run-all }}" == "true" ]]; then + echo "Running all tests" + hatch run test_all.py3.10:pytest -n 2 tests + else + echo "Running tests for parser: ${{ needs.detect-changes.outputs.single-parser }}" + hatch run test_all.py3.10:pytest -n 2 tests/parsers/${{ needs.detect-changes.outputs.single-parser }} + fi timeout-minutes: 15 test_py_311: + needs: detect-changes runs-on: ubuntu-latest name: Tests (python 3.11) @@ -48,10 +117,18 @@ jobs: - name: Install click run: pip install click!=8.3.0 - name: Run Tests - run: hatch run test_all.py3.11:pytest -n 2 tests + run: | + if [[ "${{ needs.detect-changes.outputs.run-all }}" == "true" ]]; then + echo "Running all tests" + hatch run test_all.py3.11:pytest -n 2 tests + else + echo "Running tests for parser: ${{ needs.detect-changes.outputs.single-parser }}" + hatch run test_all.py3.11:pytest -n 2 tests/parsers/${{ needs.detect-changes.outputs.single-parser }} + fi timeout-minutes: 15 test_py_312: + needs: detect-changes runs-on: ubuntu-latest name: Tests (python 3.12) @@ -67,7 +144,14 @@ jobs: - name: Install click run: pip install click!=8.3.0 - name: Run Tests - run: hatch run test_all.py3.12:pytest -n 2 tests + run: | + if [[ "${{ needs.detect-changes.outputs.run-all }}" == "true" ]]; then + echo "Running all tests" + hatch run test_all.py3.12:pytest -n 2 tests + else + echo "Running tests for parser: ${{ needs.detect-changes.outputs.single-parser }}" + hatch run test_all.py3.12:pytest -n 2 tests/parsers/${{ needs.detect-changes.outputs.single-parser }} + fi timeout-minutes: 15 lint: @@ -99,4 +183,4 @@ jobs: - name: Check PR title run: ./scripts/check_title env: - PR_TITLE: ${{ github.event.pull_request.title }} + PR_TITLE: ${{ github.event.pull_request.title }} \ No newline at end of file From 8c4cef2e0d2c9f3d8248dc2453599e5a46c34860 Mon Sep 17 00:00:00 2001 From: Nathan Stender Date: Sun, 15 Mar 2026 00:09:59 -0400 Subject: [PATCH 2/3] Fix single-parser detection logic - Include test files in parser detection (tests/parsers/**) - Add SUPPORTED_INSTRUMENT_SOFTWARE.adoc to non-parser filter - Update detection logic to handle both src and test parser files - Exclude utils from parser detection in src directory This ensures the workflow correctly detects single-parser changes even when test data files are modified alongside parser code. Co-Authored-By: Claude Opus 4.1 --- .github/workflows/test.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 00ca4af21..e6af73234 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,12 +26,14 @@ jobs: filters: | parsers: - 'src/allotropy/parsers/**' + - 'tests/parsers/**' non-parser: - 'src/allotropy/!(parsers)/**' - 'tests/!(parsers)/**' - 'pyproject.toml' - 'scripts/**' - '.github/**' + - 'SUPPORTED_INSTRUMENT_SOFTWARE.adoc' - name: Detect single parser changes id: detect-parser run: | @@ -43,14 +45,18 @@ jobs: # Get list of changed files in parsers directory FILES=$(echo '${{ steps.filter.outputs.parsers_files }}' | jq -r '.[]') - # Extract unique parser directories - PARSERS=$(echo "$FILES" | grep '^src/allotropy/parsers/' | cut -d'/' -f4 | sort -u) + # Extract unique parser directories from both src and tests + SRC_PARSERS=$(echo "$FILES" | grep '^src/allotropy/parsers/' | cut -d'/' -f4 | grep -v '^utils$' | grep -v '^__pycache__$') + TEST_PARSERS=$(echo "$FILES" | grep '^tests/parsers/' | cut -d'/' -f3 | grep -v '^__pycache__$') + + # Combine and get unique parsers + ALL_PARSERS=$(echo -e "$SRC_PARSERS\n$TEST_PARSERS" | sort -u | grep -v '^$') # Count unique parsers - PARSER_COUNT=$(echo "$PARSERS" | grep -v '^$' | wc -l) + PARSER_COUNT=$(echo "$ALL_PARSERS" | grep -v '^$' | wc -l) if [[ $PARSER_COUNT -eq 1 ]]; then - PARSER=$(echo "$PARSERS" | head -n1) + PARSER=$(echo "$ALL_PARSERS" | head -n1) # Check if it's actually a parser directory (not utils, shared, etc) if [[ "$PARSER" != "utils" && "$PARSER" != "__pycache__" && -d "tests/parsers/$PARSER" ]]; then echo "Single parser detected: $PARSER" From c3980c2c509aa2b98099e31d71caa0da97a5199f Mon Sep 17 00:00:00 2001 From: Nathan Stender Date: Sun, 15 Mar 2026 00:18:50 -0400 Subject: [PATCH 3/3] Fix SUPPORTED_INSTRUMENT_SOFTWARE.adoc handling in CI - Remove SUPPORTED_INSTRUMENT_SOFTWARE.adoc from non-parser filter (it's an output of parser changes, not an input that affects parsers) - Add test_table_contents validation to single-parser test runs to ensure the instrument table is updated when needed This correctly treats the instrument table as a consequence of parser changes rather than a trigger for running all tests. Single-parser test runs now also validate that the table is up-to-date. Co-Authored-By: Claude Opus 4.1 --- .github/workflows/test.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e6af73234..cf1e5d8e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,6 @@ jobs: - 'pyproject.toml' - 'scripts/**' - '.github/**' - - 'SUPPORTED_INSTRUMENT_SOFTWARE.adoc' - name: Detect single parser changes id: detect-parser run: | @@ -101,7 +100,7 @@ jobs: hatch run test_all.py3.10:pytest -n 2 tests else echo "Running tests for parser: ${{ needs.detect-changes.outputs.single-parser }}" - hatch run test_all.py3.10:pytest -n 2 tests/parsers/${{ needs.detect-changes.outputs.single-parser }} + hatch run test_all.py3.10:pytest -n 2 tests/parsers/${{ needs.detect-changes.outputs.single-parser }} tests/parser_factory_test.py::test_table_contents fi timeout-minutes: 15 @@ -129,7 +128,7 @@ jobs: hatch run test_all.py3.11:pytest -n 2 tests else echo "Running tests for parser: ${{ needs.detect-changes.outputs.single-parser }}" - hatch run test_all.py3.11:pytest -n 2 tests/parsers/${{ needs.detect-changes.outputs.single-parser }} + hatch run test_all.py3.11:pytest -n 2 tests/parsers/${{ needs.detect-changes.outputs.single-parser }} tests/parser_factory_test.py::test_table_contents fi timeout-minutes: 15 @@ -156,7 +155,7 @@ jobs: hatch run test_all.py3.12:pytest -n 2 tests else echo "Running tests for parser: ${{ needs.detect-changes.outputs.single-parser }}" - hatch run test_all.py3.12:pytest -n 2 tests/parsers/${{ needs.detect-changes.outputs.single-parser }} + hatch run test_all.py3.12:pytest -n 2 tests/parsers/${{ needs.detect-changes.outputs.single-parser }} tests/parser_factory_test.py::test_table_contents fi timeout-minutes: 15