From 79b39baf1ed646b4001276e9fe0bb0aef7834afb Mon Sep 17 00:00:00 2001 From: Hemanth Kapa Date: Tue, 30 Jun 2026 11:25:36 -0600 Subject: [PATCH 1/2] Add CI workflow, fix dependency manifests, clean up test suite - Add GitHub Actions CI (lint + pytest matrix on py3.9/3.11, headless Qt) - Replace incomplete requirement.txt with complete requirements.txt + requirements-dev.txt (pytest, pytest-qt) - Remove stale/no-op tests; strengthen Bug6 and Bug8 regression tests - Document test setup in CONTRIBUTING Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 59 +++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 16 +++++++++++ requirement.txt | 8 ------ requirements-dev.txt | 5 ++++ requirements.txt | 24 ++++++++++++++++ tests/test_config_yaml.py | 24 ---------------- tests/test_known_bugs.py | 29 ++++++++++--------- 7 files changed, 120 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/ci.yml delete mode 100644 requirement.txt create mode 100644 requirements-dev.txt create mode 100644 requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..57321ff --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,59 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +# Cancel superseded runs on the same ref (e.g. rapid pushes to a PR). +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint: + name: Lint (syntax / undefined names) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install ruff + run: pip install ruff + # Only fail on real errors: syntax errors and undefined names. + # This is a fast safety net, not a style gate. + - name: Check for fatal errors + run: ruff check --select E9,F63,F7,F82 src tests + + test: + name: Tests (py${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.9", "3.11"] + env: + # Run Qt without a display. + QT_QPA_PLATFORM: offscreen + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + cache-dependency-path: requirements-dev.txt + + # System libraries needed by PyQt5 / napari / VTK in a headless runner. + - name: Install Qt system dependencies + uses: tlambert03/setup-qt-libs@v1 + + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-dev.txt + + - name: Run test suite + run: pytest tests/ -v diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e258495..3a9175d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -63,6 +63,22 @@ If you encounter issues, please open an issue or reach out for help. - Add tests for new features and bug fixes when possible. - If adding new modules, consider including a corresponding test file. +Install the development dependencies and run the suite: + +```bash +pip install -r requirements-dev.txt + +# GUI tests need a Qt platform; use the offscreen backend when headless. +QT_QPA_PLATFORM=offscreen pytest tests/ +``` + +Markers: +- `gui` — needs a Qt application (deselect with `-m "not gui"`) +- `integration` — needs real data paths + +The full suite runs automatically on every push and pull request via GitHub +Actions (`.github/workflows/ci.yml`). + ## Reporting Bugs If you find a bug: 1. Search [existing issues](https://github.com/baradlab/surface-morphometrics-gui/issues) to see if it has already been reported. diff --git a/requirement.txt b/requirement.txt deleted file mode 100644 index 13064a2..0000000 --- a/requirement.txt +++ /dev/null @@ -1,8 +0,0 @@ -napari-threedee -magicgui -qtpy -ruamel.yaml -vtk -numpy -pathlib -libigl<2.5.0 \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..317c2c7 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +# Development / test dependencies. +-r requirements.txt + +pytest +pytest-qt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..6966171 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,24 @@ +# Runtime dependencies for the Surface Morphometrics GUI. +# These sit on top of the upstream `morphometrics` conda environment +# (see CONTRIBUTING.md), but the full set is listed here so the GUI can +# also be installed into a plain virtualenv / used in CI. + +napari +magicgui +qtpy +PyQt5 +napari-tomoslice==0.2.1 + +ruamel.yaml +PyYAML + +numpy +pandas +matplotlib +scikit-image + +mrcfile +starfile + +vtk +libigl<2.5.0 diff --git a/tests/test_config_yaml.py b/tests/test_config_yaml.py index 0ca1e18..a90a3e8 100644 --- a/tests/test_config_yaml.py +++ b/tests/test_config_yaml.py @@ -102,30 +102,6 @@ def test_save_roundtrip(self, tmp_path): cp2 = ConfigYAMLPreserver(p) assert cp2.yaml_data["key"] == "updated" - def test_save_creates_backup(self, tmp_path): - p = tmp_path / "test.yml" - p.write_text("key: value\n") - cp = ConfigYAMLPreserver(p) - cp.save() - # After bug fix, backup should be cleaned up - backup = tmp_path / "test.bak" - # This test documents current behavior (backup left behind - bug #9) - # After fix, backup should not exist - # For now just ensure the file was saved correctly - assert p.exists() - content = yaml.safe_load(p.read_text()) - assert content["key"] == "value" - - def test_save_restores_on_failure(self, tmp_path): - p = tmp_path / "test.yml" - p.write_text("key: original\n") - cp = ConfigYAMLPreserver(p) - # Corrupt content to cause write issue indirectly - # (can't easily force write failure, so just test normal path) - cp.update({"key": "new"}) - cp.save() - assert yaml.safe_load(p.read_text())["key"] == "new" - def test_empty_file(self, tmp_path): p = tmp_path / "test.yml" p.write_text("") diff --git a/tests/test_known_bugs.py b/tests/test_known_bugs.py index c6f69df..c47a1a9 100644 --- a/tests/test_known_bugs.py +++ b/tests/test_known_bugs.py @@ -138,18 +138,19 @@ def test_remove_after_rename(self, qapp): editor._add_entry("original_key", ["val1"]) assert "original_key" in editor.entries - # Simulate rename: change the key_edit widget value + # The entry is keyed by its original name; renaming the widget must not + # change the dict key (which is what the remove callback looks up). key_edit, value_editor, container = editor.entries["original_key"] key_edit.value = "renamed_key" - # The remove button callback should still work - # After fix, this should not leave stale entries - # Get the remove button and simulate click - # (The remove function captures key_edit.value which is now "renamed_key") - # After fix, it should remove using original key from entries dict - initial_count = len(editor.entries) - # The entries dict still has "original_key" as key - assert "original_key" in editor.entries + # Simulate clicking Remove. The remove button lives in the header + # container: container[0] is the header, container[0][1] the button. + remove_button = container[0][1] + remove_button.clicked.emit() + + # After the fix the entry is removed despite the rename — no stale key. + assert "original_key" not in editor.entries + assert editor.entries == {} class TestBug7_UpdateConfigPathsOverwrite: @@ -193,12 +194,14 @@ def test_no_clear_during_programmatic_clear(self, qapp): em.data_dir.value = "/user/data" em.cores_input.setValue(8) - # Simulate programmatic clear (index goes to -1) + # Simulate programmatic clear (index goes to -1); this fires + # _on_experiment_selected via the currentIndexChanged signal. em.experiment_name.setCurrentIndex(-1) - # _on_experiment_selected fires via signal - # After fix, user values should be preserved during programmatic clears - # (The fix should guard against clearing during non-user-initiated changes) + # The fix guards on currentIndex() >= 0, so a negative index + # (non-user-initiated change) must not wipe the user's values. + assert str(em.data_dir.value) == "/user/data" + assert em.cores_input.value() == 8 class TestBug9_BackupNotCleaned: From 6344c9da889e6a30018c7616f47c4ad7c525975c Mon Sep 17 00:00:00 2001 From: Hemanth Kapa Date: Tue, 30 Jun 2026 11:39:28 -0600 Subject: [PATCH 2/2] Fix GUI launch path in CONTRIBUTING (main.py is in src/) Co-Authored-By: Claude Opus 4.8 (1M context) --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a9175d..48d117d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,6 +40,7 @@ To set up your development environment: 5. **Run the GUI** ```bash + cd src python main.py ```