diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..eed4b30 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,50 @@ +name: Lint + +# Lint runs on a cheap GitHub-hosted runner (ubuntu-latest). It is one of the +# REQUIRED status checks that gate merges into develop/master; the self-hosted +# Windows unit tier (run-unittests-windows.yml) needs lab hardware + USB drivers +# and is unavailable on forks, so it must NOT be a required check. black and +# flake8 read their config from pyproject.toml ([tool.black] / [tool.flake8] via +# Flake8-pyproject); the Black version is pinned to match .pre-commit-config.yaml +# so CI and the local pre-commit hook agree. + +on: + push: + branches: + - master + - develop + pull_request: + branches: + - master + - develop + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: lint (black + flake8) + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install lint tools + # Pin Black to the version the pre-commit hook / local devs run so CI + # and pre-commit agree. flake8 reads [tool.flake8] via Flake8-pyproject. + run: | + python -m pip install --upgrade pip + pip install "black==25.9.0" flake8 Flake8-pyproject + + - name: black --check + run: black --check . + + - name: flake8 + run: flake8 . diff --git a/.github/workflows/run-unittests-windows.yml b/.github/workflows/run-unittests-windows.yml new file mode 100644 index 0000000..63f10d1 --- /dev/null +++ b/.github/workflows/run-unittests-windows.yml @@ -0,0 +1,66 @@ +name: Python Unit Tests (Windows) + +# NOTE: auto-run DEACTIVATED (2026-08-04). This Windows unit-test job mirrors the +# lab target platform (PycroFlow targets Windows 10), but the required merge gate +# is now the GitHub-hosted "Unit Tests (hosted)" job (unit-tests-hosted.yml, +# ubuntu-latest) + "Lint" (lint.yml). Windows runners are slower/scarcer and this +# tier is unavailable on forks, so it must NOT be a required check that can block +# merges. Manual-trigger only for now (Actions tab -> this workflow -> "Run +# workflow"). To reactivate the automatic run, uncomment the push / pull_request +# triggers below (and keep it OUT of the required-checks list in branch +# protection). +on: + workflow_dispatch: + # push: + # branches: + # - master + # - develop + # pull_request: + # branches: + # - master + # - develop + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + unittest: + name: unittest on Windows (Python ${{ matrix.python-version }}) + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10"] + env: + QT_QPA_PLATFORM: offscreen + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install package with dev + gui extras + # Intentionally NOT installing [hardware] — the conftest.py mocks + # pycromanager / monet / pycobolt / nidaqmx so CI can run without + # any vendor SDKs or USB drivers. [gui] adds PyQt6 so `unittest + # discover` actually exercises the Stage 5 GUI tests (headless via + # QT_QPA_PLATFORM=offscreen) instead of skipping them — matching the + # hosted "Unit Tests (hosted)" job. + run: | + python -m pip install --upgrade pip + pip install -e ".[dev,gui]" + + - name: Run unit tests + run: python -m unittest discover -v + + - name: Run pyHamilton unit tests + run: python -m unittest discover -s PycroFlow/pyHamilton -v + # Soft failure: pyHamilton tests may not exist yet. + continue-on-error: true diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 219bf0d..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: Tests - -on: - push: - branches: [master, main] - pull_request: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - lint: - name: lint (black + flake8) - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.10" - - - name: Install lint tools - # Pin Black to the version the pre-commit hook / local devs run so CI - # and pre-commit agree. flake8 reads [tool.flake8] via Flake8-pyproject. - run: | - python -m pip install --upgrade pip - pip install "black==25.9.0" flake8 Flake8-pyproject - - - name: black --check - run: black --check . - - - name: flake8 - run: flake8 . - - unittest: - name: unittest on Windows (Python ${{ matrix.python-version }}) - runs-on: windows-latest - strategy: - fail-fast: false - matrix: - python-version: ["3.10"] - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: pip - cache-dependency-path: pyproject.toml - - - name: Install package with dev extras - # Intentionally NOT installing [hardware] — the conftest.py mocks - # pycromanager / monet / pycobolt / nidaqmx so CI can run without - # any vendor SDKs or USB drivers. - run: | - python -m pip install --upgrade pip - pip install -e ".[dev]" - - - name: Run unit tests - run: python -m unittest discover -v - - - name: Run pyHamilton unit tests - run: python -m unittest discover -s PycroFlow/pyHamilton -v - # Soft failure: pyHamilton tests may not exist yet. - continue-on-error: true diff --git a/.github/workflows/unit-tests-hosted.yml b/.github/workflows/unit-tests-hosted.yml new file mode 100644 index 0000000..5f39128 --- /dev/null +++ b/.github/workflows/unit-tests-hosted.yml @@ -0,0 +1,69 @@ +name: Unit Tests (hosted) + +# Hardware-independent unit tests on a cheap GitHub-hosted runner +# (ubuntu-latest), meant to be the REQUIRED status check that gates merges into +# develop/master. The deep Windows tier ("Python Unit Tests (Windows)", +# run-unittests-windows.yml) needs the lab box with real instruments + USB +# drivers and is unavailable on forks — so it must NOT be a required check. +# +# The suite runs with no vendor SDKs: tests/__init__.py + the top-level +# conftest.py install sys.modules mocks for pycromanager / monet / pycobolt / +# nidaqmx, so no hardware, data, or network is needed. +# +# Headless setup: +# * base `pip install -e .` is wheel-only (pyserial / loguru / pyyaml / +# pydantic); the heavy instrument stack lives in the [hardware] extra and is +# intentionally NOT installed. The [dev] extra adds pytest; [gui] adds PyQt6 +# (also a wheel) so the Stage 5 GUI unit tests actually run. +# * the Qt runtime libs + QT_QPA_PLATFORM=offscreen let the GUI unit tests +# import PyQt6 without a display (the package is import-safe without PyQt6; +# Qt is imported lazily). + +on: + push: + branches: + - master + - develop + pull_request: + branches: + - master + - develop + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + unit-tests: + name: Unit Tests (hosted) + runs-on: ubuntu-latest + env: + QT_QPA_PLATFORM: offscreen + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + cache: pip + cache-dependency-path: pyproject.toml + + - name: Install Qt runtime libraries (headless PyQt6 import) + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libegl1 libgl1 libxkbcommon0 libdbus-1-3 + + - name: Install package with dev + gui extras + # Intentionally NOT installing [hardware] — the conftest.py mocks + # pycromanager / monet / pycobolt / nidaqmx so CI runs without any + # vendor SDKs or USB drivers. [dev] adds pytest; [gui] adds PyQt6. + run: | + python -m pip install --upgrade pip + pip install -e ".[dev,gui]" + + - name: Run unit tests + run: python -m unittest discover -v diff --git a/CHANGELOG.md b/CHANGELOG.md index a401f8e..592e5ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 (line-length 79, `target-version = ["py310"]`) and `[tool.flake8]` (`extend-ignore = E203,E501,W503` — Black owns line length), replacing the standalone `.flake8`. +- CI runner strategy (S0A-3): required checks now run on GitHub-hosted runners. + Split the old combined `tests.yml` into hosted `lint.yml` + hosted + `unit-tests-hosted.yml` (both trigger on push/PR to `master`/`develop`), and + demoted the Windows unit tier to `run-unittests-windows.yml` triggered by + `workflow_dispatch` only so a runner-less self-hosted/Windows check can't + block merges. Branch protection should list only the hosted checks as + required. ### Added @@ -28,6 +35,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Shared `.pre-commit-config.yaml` (pre-commit-hooks + Black + flake8 via Flake8-pyproject), matching the rest of the DNA-PAINT stack. - `black --check` and `flake8` lint job in CI. +- Hosted (`ubuntu-latest`) `Unit Tests (hosted)` CI job + (`unit-tests-hosted.yml`) intended as the required merge gate alongside the + hosted `Lint` job: installs Qt runtime libs, `pip install -e ".[dev,gui]"` + (base install stays wheel-only; the hardware stack is mocked), and runs the + unit suite with `QT_QPA_PLATFORM=offscreen` so the GUI tests run headlessly. - This changelog. ### Removed diff --git a/CLAUDE.md b/CLAUDE.md index 5ce00c3..b361463 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -87,7 +87,7 @@ PYCROFLOW_UPDATE_SNAPSHOTS=1 python -m unittest PycroFlow.tests.test_regression_ ``` Commit the updated JSON in `PycroFlow/tests/fixtures/snapshots/`. -CI runs `python -m unittest discover -v` on Windows / Python 3.10 (`.github/workflows/tests.yml`). There are no configured linters/formatters yet (`ruff`/`mypy` are in the `[dev]` extra). +CI runs on GitHub-hosted `ubuntu-latest` runners as the required merge gate: a `Lint` job (`black --check` + `flake8`, `.github/workflows/lint.yml`) and a `Unit Tests (hosted)` job (`python -m unittest discover -v` with the Qt runtime libs + `QT_QPA_PLATFORM=offscreen` so the GUI tests run headlessly, `.github/workflows/unit-tests-hosted.yml`), both on push/PR to `master`/`develop`. The Windows unit tier (`.github/workflows/run-unittests-windows.yml`, mirroring the lab target platform) is demoted to `workflow_dispatch` only so a scarce/forks-unavailable runner can't block merges — run it manually from the Actions tab. `ruff`/`mypy` are in the `[dev]` extra but not yet wired into CI. ### Hardware emulators (`tests/emulators/`) Behavioral hardware fakes for tests, in three fidelity layers (vs. the import-only `MagicMock` shims in `tests/_mock_hardware.py`): diff --git a/docs/quickstart.md b/docs/quickstart.md index d5b0828..3d16d48 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -127,7 +127,12 @@ Commit the new JSON files in `PycroFlow/tests/fixtures/snapshots/`. ## 7. Linting / CI -GitHub Actions runs `python -m unittest discover -v` on Windows / Python -3.10 for every push and PR. See `.github/workflows/tests.yml`. The -workflow installs only `.[dev]`; vendor SDKs are mocked at test-discovery -time by `PycroFlow/tests/_mock_hardware.py`. +GitHub Actions gates every push and PR to `master`/`develop` with two +GitHub-hosted (`ubuntu-latest`) jobs: `Lint` (`black --check` + `flake8`, +`.github/workflows/lint.yml`) and `Unit Tests (hosted)` +(`python -m unittest discover -v`, `.github/workflows/unit-tests-hosted.yml`). +The hosted test job installs `.[dev,gui]` plus the Qt runtime libs and runs +with `QT_QPA_PLATFORM=offscreen`; vendor SDKs are mocked at test-discovery +time by `PycroFlow/tests/_mock_hardware.py`. The Windows tier +(`.github/workflows/run-unittests-windows.yml`) is `workflow_dispatch` only — +trigger it manually from the Actions tab.