Skip to content

Merge pull request #13 from DanMeon/build/cargo-test-feature-flag #63

Merge pull request #13 from DanMeon/build/cargo-test-feature-flag

Merge pull request #13 from DanMeon/build/cargo-test-feature-flag #63

Workflow file for this run

name: CI
# 트리거: PR / main 푸시 — test 계열만 (wheel 빌드 + 배포는 publish.yml)
# 문서·라이선스·gitignore 전용 변경은 changes job + job-level if 패턴으로 if-skip
# (status: success). paths-ignore 안 씀 — required check "All tests passed" 가
# expected 로 stuck 되는 GitHub 함정 회피.
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch: {}
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# * 코드 변경 감지 — docs / LICENSE / .gitignore 전용 변경이면 test 잡 if-skip
# (status: success). 모든 변경 파일이 exclude 패턴에 매치돼야 code=false.
# base SHA 부재 / zero SHA 면 안전 측면으로 code=true (= 전체 실행).
changes:
name: Detect code changes
runs-on: ubuntu-latest
outputs:
code: ${{ steps.filter.outputs.code }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- id: filter
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
run: |
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
echo "code=true" >> "$GITHUB_OUTPUT"
exit 0
fi
# ^ three-dot diff = base..HEAD 공통 조상부터의 변경만 (PR diff 와 동일)
if git diff --name-only "$BASE_SHA"...HEAD \
| grep -qvE '(^|/)([^/]+\.md|LICENSE[^/]*|\.gitignore)$|^docs/'; then
echo "code=true" >> "$GITHUB_OUTPUT"
else
echo "code=false" >> "$GITHUB_OUTPUT"
fi
# * Linux abi3 wheel 1회 빌드 → 모든 Linux 잡(test×4 / slow / core-only)이 공유
# abi3-py310 이라 py3.10/3.11/3.12/3.13 가 동일 wheel 재사용 가능.
# macOS/Windows 는 단일 잡이라 빌드/테스트 분리 이득이 없어 그대로 매번 빌드.
build-linux-wheel:
name: Build Linux abi3 wheel
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@v8.1.0
with:
python-version: "3.12"
- run: uv sync --no-install-project --group all
- run: uv run maturin build --release --out dist
- uses: actions/upload-artifact@v7
with:
name: rhwp-python-linux-wheel
path: dist/*.whl
retention-days: 1
# * 메인 테스트 + 린트 + 타입체크 (Linux × 전 Python 버전 — wheel 공유)
test:
name: Test (Linux / py${{ matrix.python }})
needs: [build-linux-wheel, changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- { python: "3.10", lint: true }
- { python: "3.11" }
- { python: "3.12" }
- { python: "3.13" }
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: astral-sh/setup-uv@v8.1.0
with:
python-version: ${{ matrix.python }}
- run: uv sync --no-install-project --group all
- uses: actions/download-artifact@v8
with:
name: rhwp-python-linux-wheel
path: dist/
- run: uv pip install --reinstall dist/*.whl
- name: Run pytest (not slow) with coverage
run: uv run pytest tests/ -m "not slow" --cov=rhwp --cov-report=term-missing -v
- name: Run pyright (normal)
if: matrix.lint
run: |
uv run pyright python/ \
tests/test_smoke.py tests/test_parse.py tests/test_text_extraction.py \
tests/test_errors.py tests/test_svg_rendering.py tests/test_pdf_rendering.py \
tests/test_langchain_loader.py tests/test_langchain_loader_ir.py \
tests/test_ir_schema.py tests/test_ir_roundtrip.py tests/test_ir_tables.py \
tests/test_ir_iter_blocks.py tests/test_ir_schema_export.py \
tests/test_ir_picture.py tests/test_ir_furniture.py \
tests/test_ir_formula.py tests/test_ir_footnote.py \
tests/test_ir_list.py tests/test_ir_caption.py \
tests/test_ir_toc.py tests/test_ir_field.py \
tests/test_v0_3_1_marker_char_offset.py \
tests/test_cli.py \
tests/conftest.py tests/type_check_samples.py
- name: Run pyright (intentional errors — expect 4)
if: matrix.lint
run: |
set +e
uv run pyright --outputjson tests/type_check_errors.py > pyright-errors.json
count=$(uv run python -c "import json; print(json.load(open('pyright-errors.json'))['summary']['errorCount'])")
echo "intentional error count: $count"
if [ "$count" != "4" ]; then
echo "::error::expected 4 intentional errors, got $count"
exit 1
fi
# * macOS / Windows 스모크 — 단일 잡이라 wheel 분리 이득 없음 → 직접 maturin develop
test-other-os:
name: Test (${{ matrix.os }} / py3.12)
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-latest, windows-latest]
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- uses: astral-sh/setup-uv@v8.1.0
with:
python-version: "3.12"
- run: uv sync --no-install-project --group all
- run: uv run maturin develop --release
- run: uv run pytest tests/ -m "not slow" -v
# * PDF 렌더링 — 느려서 별도 잡, Linux wheel 재사용
test-slow:
name: Test slow (Linux / py3.12 — PDF)
needs: [build-linux-wheel, changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: astral-sh/setup-uv@v8.1.0
with:
python-version: "3.12"
- run: uv sync --no-install-project --group testing
- uses: actions/download-artifact@v8
with:
name: rhwp-python-linux-wheel
path: dist/
- run: uv pip install --reinstall dist/*.whl
- run: uv run pytest tests/ -m slow -v
# * Rust unit tests — src/ir.rs 의 #[cfg(test)] 모듈 실행
# Cargo.toml 의 default features 에서 extension-module 이 빠져 있어 libpython 링크 시도 안 함.
# utf16_to_cp / simple_eq_text_alt / field_type_to_str / caption_direction_to_str /
# assert_position_invariant (#[should_panic]) 검증 — Python 측에서 우회 불가능한 Rust 도메인 로직.
cargo-test:
name: Cargo test (Rust unit tests)
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run cargo test (default features — extension-module disabled)
run: cargo test --lib
# * extras 미설치 시 langchain 테스트가 importorskip 로 auto-skip 되는지 검증
test-core-only:
name: Test without extras (importorskip auto-skip)
needs: [build-linux-wheel, changes]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: astral-sh/setup-uv@v8.1.0
with:
python-version: "3.12"
- name: Install pytest only (no langchain extras — intentional)
run: |
uv venv
uv pip install pytest
- uses: actions/download-artifact@v8
with:
name: rhwp-python-linux-wheel
path: dist/
- run: uv pip install dist/*.whl
- name: Run pytest — extras-gated tests must auto-skip via importorskip
# ^ 파일-레벨 importorskip 은 해당 파일 전체를 skip 1개로 카운트.
# v0.3.0 기준 gated 파일: test_langchain_loader.py + test_langchain_loader_ir.py
# (langchain-core), test_ir_schema_export.py (jsonschema), test_cli.py (typer)
# → 총 4 파일. test_async.py 는 v0.3.0 부터 stdlib 만 사용 (aiofiles 의존성 제거)
run: |
uv run pytest tests/ -m "not slow" -v | tee pytest-output.txt
if ! grep -qE '(^|[^0-9])4 skipped([^0-9]|$)' pytest-output.txt; then
echo "::error::expected 4 extras-gated files to auto-skip via importorskip (langchain×2, jsonschema, typer)"
exit 1
fi
all-tests-passed:
name: All tests passed
if: always()
runs-on: ubuntu-latest
needs: [changes, build-linux-wheel, test, test-other-os, test-slow, test-core-only, cargo-test]
steps:
- uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}
allowed-skips: build-linux-wheel, test, test-other-os, test-slow, test-core-only, cargo-test