Complete CellProfiler parity and the typed OpenHCS platform cutover #1725
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
| # OpenHCS Integration Tests | |
| # | |
| # CI Strategy: Optimized boundary testing (29 jobs total) | |
| # | |
| # Group 1: Python boundary tests (6 jobs) | |
| # - Python 3.11 (oldest) and 3.13 (newest well-supported) on Linux + Windows + macOS | |
| # - 2 Python versions × 3 OSes = 6 jobs | |
| # - Python 3.14 excluded: no pre-built NumPy/zeroc-ice wheels yet | |
| # | |
| # Group 2: Backend/microscope coverage (12 jobs) | |
| # - Python 3.12 on Linux + Windows + macOS | |
| # - All backends: disk, zarr | |
| # - All microscopes: ImageXpress, OperaPhenix | |
| # - 3 OSes × 2 backends × 2 microscopes = 12 jobs | |
| # | |
| # Group 3: OMERO tests (2 jobs) | |
| # - Python 3.11 and 3.12 on Linux only | |
| # - Uses pre-built zeroc-ice wheels (no Python 3.13+ support yet) | |
| # | |
| # Group 4: PyPI installation tests (6 jobs) | |
| # - Python 3.11 and 3.12 on Linux + Windows + macOS | |
| # - Validates PyPI-style installation across all platforms | |
| # - 2 Python versions × 3 OSes = 6 jobs | |
| # | |
| # Group 5: Wheel integration test (1 job) | |
| # - Python 3.12 on Linux | |
| # - Full integration test suite against wheel installation | |
| # | |
| # Total: 29 jobs covering all Python versions (3.11-3.13), all 3 OSes, | |
| # all backends, and all microscope formats | |
| name: Integration Tests | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| inputs: | |
| test_mode: | |
| description: 'Test mode' | |
| required: true | |
| default: 'cpu-only' | |
| type: choice | |
| options: | |
| - cpu-only | |
| - full-coverage | |
| dependency_source: | |
| description: 'Dependency source for external modules' | |
| required: true | |
| default: 'submodules' | |
| type: choice | |
| options: | |
| - submodules | |
| - pypi | |
| env: | |
| OPENHCS_CI_DEP_SOURCE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dependency_source || 'submodules' }} | |
| jobs: | |
| # Group 1: Python version boundary testing (6 jobs) | |
| # Test oldest (3.11) and newest (3.13) Python on all 3 OSes | |
| # Python 3.14 excluded: no pre-built NumPy wheels yet (builds from source stall on Windows) | |
| python-boundary-tests: | |
| runs-on: ${{ matrix.os }} | |
| if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # Boundary testing: oldest and newest well-supported versions | |
| python-version: ["3.11", "3.13"] | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: ${{ env.OPENHCS_CI_DEP_SOURCE == 'submodules' && 'recursive' || 'false' }} | |
| - name: Setup Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install system dependencies (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| # ubuntu-latest (23.10+) no longer provides libgl1-mesa-glx; use libgl1. | |
| # PyQt6 requires libEGL.so.1 (EGL) and libGL.so.1 (OpenGL). | |
| sudo apt-get install -y libgl1 | |
| sudo apt-get install -y libegl1 || sudo apt-get install -y libegl1-mesa | |
| sudo apt-get install -y libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 xvfb | |
| - name: Install local external modules (submodule mode) | |
| if: env.OPENHCS_CI_DEP_SOURCE == 'submodules' | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -c "import subprocess,sys;mods=['external/ObjectState','external/python-introspect','external/metaclass-registry','external/arraybridge','external/pycodify','external/PolyStore','external/pyqt-reactive','external/zmqruntime'];[subprocess.check_call([sys.executable,'-m','pip','install','-e',m]) for m in mods]" | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev]" | |
| - name: Run boundary version tests | |
| env: | |
| OPENHCS_CPU_ONLY: true | |
| run: python -m pytest tests/integration/ --cov=openhcs --cov-report=term --it-backends disk --it-microscopes ImageXpress --it-dims 3d --it-exec-mode multiprocessing --it-zmq-mode zmq --it-visualizers none -v --tb=short | |
| - name: Upload coverage artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-python-${{ matrix.python-version }}-${{ matrix.os }} | |
| path: .coverage | |
| retention-days: 1 | |
| - name: Show ZMQ logs on failure | |
| if: failure() | |
| shell: bash | |
| run: | | |
| python - <<'PY' | |
| from pathlib import Path | |
| import os | |
| candidates = [] | |
| home = Path.home() | |
| candidates.append(home / ".local" / "share" / "openhcs" / "logs") | |
| candidates.append(home / ".local" / "state" / "openhcs" / "logs") | |
| xdg_state = os.environ.get("XDG_STATE_HOME") | |
| if xdg_state: | |
| candidates.append(Path(xdg_state) / "openhcs" / "logs") | |
| patterns = [ | |
| "openhcs_zmq_server_*.log", | |
| "zmq_worker_exec_*.log", | |
| "openhcs_unified_*.log", | |
| "openhcs_subprocess_*.log", | |
| ] | |
| seen = set() | |
| for log_dir in candidates: | |
| if log_dir in seen: | |
| continue | |
| seen.add(log_dir) | |
| if not log_dir.is_dir(): | |
| continue | |
| print(f"=== OpenHCS Logs: {log_dir} ===") | |
| for pattern in patterns: | |
| for log in sorted(log_dir.glob(pattern)): | |
| if log.is_file(): | |
| print(f"--- {log} ---") | |
| try: | |
| print(log.read_text()) | |
| except Exception as exc: | |
| print(f"[WARN] Failed to read {log}: {exc}") | |
| print("") | |
| if not any(p.is_dir() for p in candidates): | |
| print("No OpenHCS log directories found.") | |
| PY | |
| - name: Upload ZMQ logs on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: zmq-logs-python-${{ matrix.python-version }}-${{ matrix.os }} | |
| path: | | |
| ~/.local/share/openhcs/logs | |
| ~/.local/state/openhcs/logs | |
| # Group 2: Backend and microscope coverage (12 jobs) | |
| # Test all backend/microscope combinations on all 3 OSes with Python 3.12 | |
| backend-microscope-tests: | |
| runs-on: ${{ matrix.os }} | |
| if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| backend: [disk, zarr] | |
| microscope: [ImageXpress, OperaPhenix] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: ${{ env.OPENHCS_CI_DEP_SOURCE == 'submodules' && 'recursive' || 'false' }} | |
| - name: Setup Python 3.12 | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install system dependencies (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| # ubuntu-latest (23.10+) no longer provides libgl1-mesa-glx; use libgl1. | |
| # PyQt6 requires libEGL.so.1 (EGL) and libGL.so.1 (OpenGL). | |
| sudo apt-get install -y libgl1 | |
| sudo apt-get install -y libegl1 || sudo apt-get install -y libegl1-mesa | |
| sudo apt-get install -y libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 xvfb | |
| - name: Install local external modules (submodule mode) | |
| if: env.OPENHCS_CI_DEP_SOURCE == 'submodules' | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -c "import subprocess,sys;mods=['external/ObjectState','external/python-introspect','external/metaclass-registry','external/arraybridge','external/pycodify','external/PolyStore','external/pyqt-reactive','external/zmqruntime'];[subprocess.check_call([sys.executable,'-m','pip','install','-e',m]) for m in mods]" | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[dev]" | |
| - name: Run backend/microscope combination tests | |
| env: | |
| OPENHCS_CPU_ONLY: true | |
| run: python -m pytest tests/integration/ --cov=openhcs --cov-report=term --it-backends ${{ matrix.backend }} --it-microscopes ${{ matrix.microscope }} --it-dims 3d --it-exec-mode multiprocessing --it-zmq-mode zmq --it-visualizers none -v --tb=short | |
| - name: Upload coverage artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-${{ matrix.os }}-${{ matrix.backend }}-${{ matrix.microscope }} | |
| path: .coverage | |
| retention-days: 1 | |
| - name: Show ZMQ logs on failure | |
| if: failure() | |
| shell: bash | |
| run: | | |
| python - <<'PY' | |
| from pathlib import Path | |
| import os | |
| candidates = [] | |
| home = Path.home() | |
| candidates.append(home / ".local" / "share" / "openhcs" / "logs") | |
| candidates.append(home / ".local" / "state" / "openhcs" / "logs") | |
| xdg_state = os.environ.get("XDG_STATE_HOME") | |
| if xdg_state: | |
| candidates.append(Path(xdg_state) / "openhcs" / "logs") | |
| patterns = [ | |
| "openhcs_zmq_server_*.log", | |
| "zmq_worker_exec_*.log", | |
| "openhcs_unified_*.log", | |
| "openhcs_subprocess_*.log", | |
| ] | |
| seen = set() | |
| for log_dir in candidates: | |
| if log_dir in seen: | |
| continue | |
| seen.add(log_dir) | |
| if not log_dir.is_dir(): | |
| continue | |
| print(f"=== OpenHCS Logs: {log_dir} ===") | |
| for pattern in patterns: | |
| for log in sorted(log_dir.glob(pattern)): | |
| if log.is_file(): | |
| print(f"--- {log} ---") | |
| try: | |
| print(log.read_text()) | |
| except Exception as exc: | |
| print(f"[WARN] Failed to read {log}: {exc}") | |
| print("") | |
| if not any(p.is_dir() for p in candidates): | |
| print("No OpenHCS log directories found.") | |
| PY | |
| - name: Upload ZMQ logs on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: zmq-logs-${{ matrix.os }}-${{ matrix.backend }}-${{ matrix.microscope }} | |
| path: | | |
| ~/.local/share/openhcs/logs | |
| ~/.local/state/openhcs/logs | |
| # Group 3: OMERO testing on Linux (2 jobs) | |
| # Test OMERO on Linux with Python 3.11 and 3.12 | |
| # Python 3.13+ not supported yet - zeroc-ice pre-built wheels only available up to Python 3.12 | |
| # See: https://github.com/glencoesoftware/zeroc-ice-py-linux-x86_64/releases/tag/20240202 | |
| omero-tests-linux: | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.11", "3.12"] | |
| include: | |
| - python-version: "3.11" | |
| ice-wheel: "https://github.com/glencoesoftware/zeroc-ice-py-linux-x86_64/releases/download/20240202/zeroc_ice-3.6.5-cp311-cp311-manylinux_2_28_x86_64.whl" | |
| - python-version: "3.12" | |
| ice-wheel: "https://github.com/glencoesoftware/zeroc-ice-py-linux-x86_64/releases/download/20240202/zeroc_ice-3.6.5-cp312-cp312-manylinux_2_28_x86_64.whl" | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: ${{ env.OPENHCS_CI_DEP_SOURCE == 'submodules' && 'recursive' || 'false' }} | |
| - name: Setup Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install system dependencies (Linux) | |
| run: | | |
| sudo apt-get update | |
| # ubuntu-latest (23.10+) no longer provides libgl1-mesa-glx; use libgl1. | |
| # PyQt6 requires libEGL.so.1 (EGL) and libGL.so.1 (OpenGL). | |
| sudo apt-get install -y libgl1 | |
| sudo apt-get install -y libegl1 || sudo apt-get install -y libegl1-mesa | |
| sudo apt-get install -y libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 xvfb | |
| - name: Install local external modules (submodule mode) | |
| if: env.OPENHCS_CI_DEP_SOURCE == 'submodules' | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -c "import subprocess,sys;mods=['external/ObjectState','external/python-introspect','external/metaclass-registry','external/arraybridge','external/pycodify','external/PolyStore','external/pyqt-reactive','external/zmqruntime'];[subprocess.check_call([sys.executable,'-m','pip','install','-e',m]) for m in mods]" | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| # Install pre-built zeroc-ice wheel from Glencoe Software (OMERO maintainers) | |
| # Building from source fails on Python 3.11+ due to setuptools compatibility issues | |
| pip install ${{ matrix.ice-wheel }} | |
| pip install -e ".[dev,omero]" | |
| - name: Run OMERO integration tests (auto-starts Docker + OMERO) | |
| env: | |
| OPENHCS_CPU_ONLY: true | |
| run: python -m pytest tests/integration/ --cov=openhcs --cov-report=term --it-backends disk --it-microscopes OMERO --it-dims 3d --it-exec-mode multiprocessing --it-visualizers none -v --tb=short -s --log-cli-level=INFO | |
| timeout-minutes: 15 | |
| - name: Upload coverage artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-omero-python-${{ matrix.python-version }} | |
| path: .coverage | |
| retention-days: 1 | |
| - name: Show OMERO logs on failure | |
| if: failure() | |
| run: | | |
| echo "=== OMERO Server Logs ===" | |
| sudo docker compose -f openhcs/omero/docker-compose.yml logs omeroserver || true | |
| echo "=== OMERO Web Logs ===" | |
| sudo docker compose -f openhcs/omero/docker-compose.yml logs omeroweb || true | |
| echo "=== Database Logs ===" | |
| sudo docker compose -f openhcs/omero/docker-compose.yml logs database || true | |
| - name: Cleanup OMERO containers | |
| if: always() | |
| run: | | |
| cd openhcs/omero | |
| sudo docker compose down -v || true | |
| # Code quality checks (linting, formatting, type checking) | |
| code-quality: | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: ${{ env.OPENHCS_CI_DEP_SOURCE == 'submodules' && 'recursive' || 'false' }} | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install dependencies | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install ruff black mypy | |
| - name: Run ruff (linting) | |
| continue-on-error: true # Don't fail CI on linting errors initially | |
| run: | | |
| ruff check openhcs/ --output-format=github | |
| - name: Run black (formatting check) | |
| continue-on-error: true # Don't fail CI on formatting errors initially | |
| run: | | |
| black --check openhcs/ | |
| - name: Run mypy (type checking) | |
| continue-on-error: true # Don't fail CI on type errors initially | |
| run: | | |
| mypy openhcs/ --ignore-missing-imports --no-strict-optional | |
| # Test PyPI-style installation across Python versions and OSes (6 jobs) | |
| pypi-installation-test: | |
| runs-on: ${{ matrix.os }} | |
| if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.11", "3.12"] | |
| os: [ubuntu-latest, windows-latest, macos-latest] | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: ${{ env.OPENHCS_CI_DEP_SOURCE == 'submodules' && 'recursive' || 'false' }} | |
| - name: Setup Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Build wheel | |
| run: | | |
| python -m pip install --upgrade pip build | |
| python -m build | |
| - name: Test headless installation (base - no extras) | |
| shell: bash | |
| run: | | |
| python -m venv test_headless | |
| if [ "$RUNNER_OS" == "Windows" ]; then | |
| source test_headless/Scripts/activate | |
| WHEEL=$(ls dist/*.whl) | |
| else | |
| source test_headless/bin/activate | |
| WHEEL=$(ls dist/*.whl) | |
| fi | |
| pip install "$WHEEL" | |
| # Verify core OpenHCS works without GUI | |
| python -c "import openhcs; from openhcs.core.pipeline import Pipeline; print(f'[OK] OpenHCS {openhcs.__version__} headless installation OK')" | |
| - name: Test GUI dependencies installation | |
| shell: bash | |
| run: | | |
| python -m venv test_gui | |
| if [ "$RUNNER_OS" == "Windows" ]; then | |
| source test_gui/Scripts/activate | |
| WHEEL=$(ls dist/*.whl) | |
| else | |
| source test_gui/bin/activate | |
| WHEEL=$(ls dist/*.whl) | |
| fi | |
| pip install "${WHEEL}[gui]" | |
| # Verify GUI dependencies installed (can't import PyQt6 without X11/EGL in CI) | |
| python -c "import openhcs; import psutil; import GPUtil; print(f'[OK] OpenHCS {openhcs.__version__} GUI dependencies installed')" | |
| - name: Test dev dependencies installation | |
| shell: bash | |
| run: | | |
| python -m venv test_dev | |
| if [ "$RUNNER_OS" == "Windows" ]; then | |
| source test_dev/Scripts/activate | |
| WHEEL=$(ls dist/*.whl) | |
| else | |
| source test_dev/bin/activate | |
| WHEEL=$(ls dist/*.whl) | |
| fi | |
| pip install "${WHEEL}[dev]" | |
| python -c "import pytest; import openhcs; print('[OK] Dev dependencies OK')" | |
| # Run integration tests against wheel installation | |
| wheel-integration-test: | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'push' || github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/v') | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| submodules: ${{ env.OPENHCS_CI_DEP_SOURCE == 'submodules' && 'recursive' || 'false' }} | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install system dependencies (Linux) | |
| run: | | |
| sudo apt-get update | |
| # PyQt6 requires libEGL.so.1 (EGL) and libGL.so.1 (OpenGL). | |
| sudo apt-get install -y libgl1 | |
| sudo apt-get install -y libegl1 || sudo apt-get install -y libegl1-mesa | |
| sudo apt-get install -y libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 xvfb | |
| - name: Build wheel | |
| run: | | |
| python -m pip install --upgrade pip build | |
| python -m build | |
| - name: Install wheel and run integration tests | |
| env: | |
| OPENHCS_CPU_ONLY: true | |
| run: | | |
| python -m venv test_wheel | |
| source test_wheel/bin/activate | |
| WHEEL=$(ls dist/*.whl) | |
| pip install "${WHEEL}[dev]" | |
| python -m pytest tests/integration/ --cov=openhcs --cov-report=term --it-backends disk --it-microscopes ImageXpress --it-dims 3d --it-exec-mode multiprocessing --it-zmq-mode zmq --it-visualizers none -v --tb=short | |
| cp .coverage ../.coverage.wheel | |
| deactivate | |
| - name: Upload coverage artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: coverage-wheel-integration | |
| path: .coverage.wheel | |
| retention-days: 1 |