Skip to content

Complete CellProfiler parity and the typed OpenHCS platform cutover #1725

Complete CellProfiler parity and the typed OpenHCS platform cutover

Complete CellProfiler parity and the typed OpenHCS platform cutover #1725

# 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