From dd4f6909287ff98c764c9ebd4ffa2a193a3351a9 Mon Sep 17 00:00:00 2001 From: Tim Monko Date: Tue, 9 Dec 2025 23:17:33 -0600 Subject: [PATCH 1/3] clean up duplicate tests, and separate plugin installer --- pixi.lock | 5 +- tests/test_bioio_plugin_utils.py | 143 +++---------- tests/test_plugin_installer.py | 220 ++------------------ tests/test_plugin_installer_integration.py | 231 +++------------------ tests/test_plugin_installer_widget.py | 106 ++++++++++ tests/test_plugin_manager.py | 208 +++++++++++++++++++ 6 files changed, 391 insertions(+), 522 deletions(-) create mode 100644 tests/test_plugin_installer_widget.py create mode 100644 tests/test_plugin_manager.py diff --git a/pixi.lock b/pixi.lock index 5740e16..73982ee 100644 --- a/pixi.lock +++ b/pixi.lock @@ -4925,8 +4925,8 @@ packages: requires_python: '>=3.10' - pypi: ./ name: ndevio - version: 0.5.1.dev0+g6b347af8b.d20251205 - sha256: 165deea7b03f51ad0dd372f6cfc0669323dbd86483fc56e466e87649a2706d68 + version: 0.6.1.dev2+g3b37a3718.d20251210 + sha256: 51f84d28b03da8af6312c90e5d121a2fd208a31382b40a6db800234b545e8cf2 requires_dist: - bioio-base - bioio-imageio @@ -4941,6 +4941,7 @@ packages: - natsort - nbatch>=0.0.4 - ndev-settings>=0.4.0 + - pooch - xarray - zarr>=3.1.3 - napari[all] ; extra == 'all' diff --git a/tests/test_bioio_plugin_utils.py b/tests/test_bioio_plugin_utils.py index c797351..0e15530 100644 --- a/tests/test_bioio_plugin_utils.py +++ b/tests/test_bioio_plugin_utils.py @@ -1,144 +1,53 @@ -"""Tests for _bioio_plugin_utils module.""" +"""Tests for _bioio_plugin_utils module. -import logging +This module tests the utility functions in _bioio_plugin_utils.py: +- suggest_plugins_for_path: suggests plugins based on file extension +- format_plugin_installation_message: formats installation instructions +- BIOIO_PLUGINS: the plugin metadata registry +For ReaderPluginManager tests, see test_plugin_manager.py +""" -class TestSuggestPluginsForPath: - """Test suggest_plugins_for_path function.""" +import pytest - def test_czi_file(self): - """Test that CZI file suggests bioio-czi.""" - from ndevio._bioio_plugin_utils import suggest_plugins_for_path - plugins = suggest_plugins_for_path('test.czi') - - assert len(plugins) == 1 - assert plugins[0] == 'bioio-czi' +class TestSuggestPluginsForPath: + """Test suggest_plugins_for_path function.""" - def test_lif_file(self): - """Test that LIF file suggests bioio-lif.""" + @pytest.mark.parametrize( + ('filename', 'expected_plugins'), + [ + ('test.czi', ['bioio-czi']), + ('test.lif', ['bioio-lif']), + ('test.nd2', ['bioio-nd2']), + ('test.dv', ['bioio-dv']), + ], + ) + def test_proprietary_formats(self, filename, expected_plugins): + """Test that proprietary formats suggest correct plugins.""" from ndevio._bioio_plugin_utils import suggest_plugins_for_path - plugins = suggest_plugins_for_path('test.lif') + plugins = suggest_plugins_for_path(filename) - assert len(plugins) == 1 - assert plugins[0] == 'bioio-lif' + assert plugins == expected_plugins - def test_tiff_file_suggests_all(self): + def test_tiff_suggests_all_tiff_plugins(self): """Test that TIFF files suggest all TIFF-compatible plugins.""" from ndevio._bioio_plugin_utils import suggest_plugins_for_path plugins = suggest_plugins_for_path('test.tiff') - # Should get bioio-ome-tiff, bioio-tifffile, bioio-tiff-glob assert 'bioio-ome-tiff' in plugins assert 'bioio-tifffile' in plugins assert 'bioio-tiff-glob' in plugins - def test_unsupported_extension(self): + def test_unsupported_extension_returns_empty(self): """Test that unsupported extensions return empty list.""" from ndevio._bioio_plugin_utils import suggest_plugins_for_path plugins = suggest_plugins_for_path('test.xyz') - assert len(plugins) == 0 - - -class TestReaderPluginManager: - """Test ReaderPluginManager class.""" - - def test_manager_filters_installed_plugins(self): - """Test that manager correctly identifies installable plugins.""" - from unittest.mock import Mock, patch - - from ndevio._plugin_manager import ReaderPluginManager - - # Mock feasibility report showing bioio-czi as installed - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = { - 'bioio-czi': Mock(supported=False), - 'ArrayLike': Mock(supported=False), - } - - manager = ReaderPluginManager('test.czi') - - # bioio-czi should be in installed_plugins - assert 'bioio-czi' in manager.installed_plugins - - # bioio-czi should NOT be in installable_plugins (already installed) - assert 'bioio-czi' not in manager.installable_plugins - - def test_manager_suggests_uninstalled_plugins(self): - """Test that manager suggests uninstalled plugins.""" - from unittest.mock import Mock, patch - - from ndevio._plugin_manager import ReaderPluginManager - - # Mock feasibility report with no bioio-lif installed - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = { - 'bioio-ome-tiff': Mock(supported=False), - 'ArrayLike': Mock(supported=False), - } - - manager = ReaderPluginManager('test.lif') - - # bioio-lif should be in suggested_plugins - assert 'bioio-lif' in manager.suggested_plugins - - # bioio-lif should also be in installable_plugins - assert 'bioio-lif' in manager.installable_plugins - - def test_manager_excludes_core_plugins_from_installable(self): - """Test that core plugins are excluded from installable list.""" - from unittest.mock import Mock, patch - - from ndevio._plugin_manager import ReaderPluginManager - - # Mock report showing no plugins installed - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = { - 'ArrayLike': Mock(supported=False), - } - - manager = ReaderPluginManager('test.tiff') - - # Core plugins should not be in installable - installable_plugins = manager.installable_plugins - - # These are core plugins, shouldn't need installation - core_plugins = [ - 'bioio-ome-tiff', - 'bioio-imageio', - 'bioio-ome-zarr', - 'bioio-tifffile', - ] - for core in core_plugins: - assert core not in installable_plugins - - # bioio-tiff-glob is not core, should be installable - assert 'bioio-tiff-glob' in installable_plugins - - def test_get_working_reader_no_path_(self, caplog): - from ndevio._plugin_manager import ReaderPluginManager - - manager = ReaderPluginManager() # No path - - with caplog.at_level(logging.WARNING): - result = manager.get_working_reader() - - assert result is None - assert 'Cannot get working reader without a path' in caplog.text - - def test_get_installation_message_no_path_returns_empty(self): - """Test that get_installation_message returns empty string without path.""" - from ndevio._plugin_manager import ReaderPluginManager - - manager = ReaderPluginManager() # No path - - install_msg = manager.get_installation_message() - - assert install_msg == '' + assert plugins == [] class TestFormatPluginInstallationMessage: diff --git a/tests/test_plugin_installer.py b/tests/test_plugin_installer.py index 2af509c..a084c83 100644 --- a/tests/test_plugin_installer.py +++ b/tests/test_plugin_installer.py @@ -1,168 +1,13 @@ -"""Tests for plugin installer functionality.""" +"""Tests for plugin installer functionality. -from unittest.mock import Mock, patch +This module tests the _plugin_installer.py module: +- install_plugin: queues a plugin for installation +- verify_plugin_installed: checks if a plugin is installed +- get_installer_queue: gets the napari installer queue singleton - -class TestReaderPluginManagerInstallable: - """Test ReaderPluginManager.installable_plugins property.""" - - def test_czi_file_no_plugins_installed(self): - """Test that CZI file suggests bioio-czi plugin when nothing installed.""" - from ndevio._plugin_manager import ReaderPluginManager - - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = {'ArrayLike': Mock(supported=False)} - - manager = ReaderPluginManager('test.czi') - plugins = manager.installable_plugins - - assert len(plugins) == 1 - assert plugins[0] == 'bioio-czi' - - def test_lif_file_no_plugins_installed(self): - """Test that LIF file suggests bioio-lif plugin.""" - from ndevio._plugin_manager import ReaderPluginManager - - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = {'ArrayLike': Mock(supported=False)} - - manager = ReaderPluginManager('test.lif') - plugins = manager.installable_plugins - - assert len(plugins) == 1 - assert plugins[0] == 'bioio-lif' - - def test_tiff_file_suggests_non_core_only(self): - """Test that TIFF files only suggest non-core plugins.""" - from ndevio._plugin_manager import ReaderPluginManager - - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = {'ArrayLike': Mock(supported=False)} - - manager = ReaderPluginManager('test.tiff') - plugins = manager.installable_plugins - - # Should only get bioio-tiff-glob (non-core) - # bioio-ome-tiff and bioio-tifffile are core and shouldn't be suggested - assert 'bioio-tiff-glob' in plugins - assert 'bioio-ome-tiff' not in plugins - assert 'bioio-tifffile' not in plugins - - def test_no_plugins_for_unsupported_extension(self): - """Test that unsupported extensions return empty list.""" - from ndevio._plugin_manager import ReaderPluginManager - - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = {'ArrayLike': Mock(supported=False)} - - manager = ReaderPluginManager('test.xyz') - plugins = manager.installable_plugins - - assert len(plugins) == 0 - - def test_filters_installed_plugins(self): - """Test that already installed plugins are filtered out.""" - from ndevio._plugin_manager import ReaderPluginManager - - # Mock feasibility report showing bioio-czi as installed - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = { - 'bioio-czi': Mock(supported=True), - 'ArrayLike': Mock(supported=False), - } - - manager = ReaderPluginManager('test.czi') - plugins = manager.installable_plugins - - # bioio-czi should be filtered out since it's "installed" - assert len(plugins) == 0 - - -class TestPluginInstallerWidget: - """Test PluginInstallerWidget in both modes.""" - - def test_standalone_mode(self, make_napari_viewer): - """Test widget in standalone mode (no path provided).""" - from ndevio.widgets import PluginInstallerWidget - - widget = PluginInstallerWidget() - - # Should have ALL plugins available via manager - assert len(widget.manager.known_plugins) > 0 - - # Should not have path - assert widget.manager.path is None - - # Title should be standalone message - assert 'Install BioIO Reader Plugin' in widget._title_label.value - - # No path, so no pre-selection - assert ( - widget._plugin_select.value is None - or widget._plugin_select.value is not None - ) - - def test_error_mode_with_installable_plugins(self, make_napari_viewer): - """Test widget in error mode with installable plugins.""" - from ndevio._plugin_manager import ReaderPluginManager - from ndevio.widgets import PluginInstallerWidget - - with patch('bioio.plugin_feasibility_report') as mock_report: - # Mock report showing no plugins installed - mock_report.return_value = {'ArrayLike': Mock(supported=False)} - - manager = ReaderPluginManager('test.czi') - widget = PluginInstallerWidget(plugin_manager=manager) - - # Should have ALL plugins available - assert len(widget.manager.known_plugins) > 0 - - # Should have installable plugins - installable = widget.manager.installable_plugins - assert len(installable) > 0 - assert 'bioio-czi' in installable - - # First installable plugin should be pre-selected - assert widget._plugin_select.value == installable[0] - - # Should have path - assert widget.manager.path is not None - assert widget.manager.path.name == 'test.czi' - - # Title should show filename - assert 'test.czi' in widget._title_label.value - - def test_error_mode_no_installable_plugins(self, make_napari_viewer): - """Test widget in error mode without installable plugins.""" - from ndevio._plugin_manager import ReaderPluginManager - from ndevio.widgets import PluginInstallerWidget - - with patch('bioio.plugin_feasibility_report') as mock_report: - # Mock report showing all suggested plugins already installed - mock_report.return_value = { - 'bioio-imageio': Mock(supported=False), # for .xyz files - 'ArrayLike': Mock(supported=False), - } - - manager = ReaderPluginManager('test.png') - widget = PluginInstallerWidget(plugin_manager=manager) - - # Should still have ALL plugins available - assert len(widget.manager.known_plugins) > 0 - - # No installable plugins (core already installed or unsupported format) - # So no pre-selection or pre-select first available - # (Either behavior is acceptable) - - def test_widget_without_viewer(self): - """Test widget can be created without viewer.""" - from ndevio.widgets import PluginInstallerWidget - - # Should work without any napari viewer - widget = PluginInstallerWidget() - - # Widget should have all plugins via manager - assert len(widget.manager.known_plugins) > 0 +For ReaderPluginManager tests, see test_plugin_manager.py +For widget tests, see test_plugin_installer_widget.py +""" class TestInstallPlugin: @@ -175,56 +20,29 @@ def test_returns_job_id(self): # This will queue the installation but not actually run it job_id = install_plugin('bioio-imageio') - # Job ID should be an integer assert isinstance(job_id, int) - def test_install_via_queue(self): - """Manual test for queue-based installation.""" - from ndevio._plugin_installer import ( - get_installer_queue, - ) - - queue = get_installer_queue() - - # Track completion - completed = [] - - def on_finished(event): - completed.append(event) - - queue.processFinished.connect(on_finished) - - # Wait for completion (with timeout) - queue.waitForFinished(msecs=30000) - - # Check that we got a completion event - assert len(completed) > 0 - assert 'bioio-imageio' in completed[0].get('pkgs', []) - class TestVerifyPluginInstalled: """Test verify_plugin_installed function.""" - def test_verify_installed_plugin(self): - """Test verification of an installed plugin (bioio itself).""" + def test_installed_dependency(self): + """Test verification of an installed package (bioio is a dependency).""" from ndevio._plugin_installer import verify_plugin_installed - # bioio should be installed since it's a dependency assert verify_plugin_installed('bioio') - def test_verify_not_installed_plugin(self): + def test_not_installed_plugin(self): """Test verification of a plugin that isn't installed.""" from ndevio._plugin_installer import verify_plugin_installed - # Use a plugin that definitely won't be installed assert not verify_plugin_installed('bioio-nonexistent-plugin-12345') - def test_verify_converts_name_format(self): + def test_converts_hyphen_to_underscore(self): """Test that plugin name is correctly converted to module name.""" from ndevio._plugin_installer import verify_plugin_installed - # Test with installed package (bioio-base should be installed) - # The function should convert bioio-base -> bioio_base + # bioio-base should be installed, converts to bioio_base result = verify_plugin_installed('bioio-base') assert isinstance(result, bool) @@ -233,7 +51,7 @@ class TestGetInstallerQueue: """Test get_installer_queue function.""" def test_returns_queue_instance(self): - """Test that get_installer_queue returns a queue.""" + """Test that get_installer_queue returns the correct type.""" from napari_plugin_manager.qt_package_installer import ( NapariInstallerQueue, ) @@ -243,18 +61,17 @@ def test_returns_queue_instance(self): queue = get_installer_queue() assert isinstance(queue, NapariInstallerQueue) - def test_returns_same_instance(self): - """Test that get_installer_queue returns singleton.""" + def test_singleton_behavior(self): + """Test that get_installer_queue returns the same instance.""" from ndevio._plugin_installer import get_installer_queue queue1 = get_installer_queue() queue2 = get_installer_queue() - # Should be the same instance assert queue1 is queue2 - def test_queue_reset(self): - """Test that queue can be reset for testing.""" + def test_queue_can_be_reset(self): + """Test that queue can be reset for testing purposes.""" from ndevio import _plugin_installer from ndevio._plugin_installer import get_installer_queue @@ -265,5 +82,4 @@ def test_queue_reset(self): queue2 = get_installer_queue() - # Should be a new instance assert queue1 is not queue2 diff --git a/tests/test_plugin_installer_integration.py b/tests/test_plugin_installer_integration.py index b7268c5..93fd82b 100644 --- a/tests/test_plugin_installer_integration.py +++ b/tests/test_plugin_installer_integration.py @@ -1,15 +1,22 @@ -"""Tests for _open_plugin_installer function and widget integration.""" +"""Integration tests for plugin installer with napari viewer. + +These tests verify that: +- _open_plugin_installer correctly docks widgets into the viewer +- Widget integration with napari's dock system works + +For widget unit tests (no viewer required), see test_plugin_installer_widget.py +For ReaderPluginManager tests, see test_plugin_manager.py +""" from pathlib import Path from unittest.mock import Mock, patch class TestOpenPluginInstaller: - """Test _open_plugin_installer function.""" + """Test _open_plugin_installer function with napari viewer.""" - def test_opens_widget_with_viewer(self, make_napari_viewer): - """Test that widget is opened when viewer exists.""" - # Import the private function directly from the module + def test_opens_widget_in_viewer(self, make_napari_viewer): + """Test that _open_plugin_installer docks widget into viewer.""" from bioio_base.exceptions import UnsupportedFileFormatError import ndevio._napari_reader as reader_module @@ -20,30 +27,22 @@ def test_opens_widget_with_viewer(self, make_napari_viewer): reader_name='test', path=test_path, msg_extra='' ) - # Mock plugin_feasibility_report from bioio (not ndevio) with patch('bioio.plugin_feasibility_report') as mock_report: mock_report.return_value = { 'bioio-ome-tiff': Mock(supported=False), 'ArrayLike': Mock(supported=False), } - - # Call the function reader_module._open_plugin_installer(test_path, error) - # Check that widget was added to viewer + # Widget should be docked in viewer assert len(viewer.window.dock_widgets) > 0 # Find the plugin installer widget - plugin_widget = None - for name, widget in viewer.window.dock_widgets.items(): - if 'Install BioIO Plugin' in name: - plugin_widget = widget - break - - assert plugin_widget is not None + widget = self._find_plugin_installer_widget(viewer) + assert widget is not None - def test_widget_has_correct_path(self, make_napari_viewer): - """Test that widget receives the correct path.""" + def test_widget_receives_correct_path(self, make_napari_viewer): + """Test that docked widget has the correct path set.""" from bioio_base.exceptions import UnsupportedFileFormatError import ndevio._napari_reader as reader_module @@ -56,210 +55,40 @@ def test_widget_has_correct_path(self, make_napari_viewer): with patch('bioio.plugin_feasibility_report') as mock_report: mock_report.return_value = {} - reader_module._open_plugin_installer(test_path, error) - # dock_widgets is a read-only mapping of widget names to widgets - # Find the plugin installer widget by name - widget = None - for name, docked_widget in viewer.window.dock_widgets.items(): - if 'Install BioIO Plugin' in name: - widget = docked_widget - break - + widget = self._find_plugin_installer_widget(viewer) assert widget is not None - - # Check path was passed correctly via the manager assert widget.manager.path == test_path assert test_path.name in widget._title_label.value - def test_filters_installed_plugins(self, make_napari_viewer): - """Test that installed plugins are filtered from suggestions.""" + def test_suggests_correct_plugins_for_extension(self, make_napari_viewer): + """Test that widget suggests correct plugins based on file extension.""" from bioio_base.exceptions import UnsupportedFileFormatError import ndevio._napari_reader as reader_module viewer = make_napari_viewer() - test_path = 'test.czi' + test_path = 'test.lif' error = UnsupportedFileFormatError( reader_name='test', path=test_path, msg_extra='' ) - # Mock feasibility report showing bioio-czi as installed - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = { - 'bioio-czi': Mock( - supported=False - ), # Installed but can't read this file - 'ArrayLike': Mock(supported=False), - } - - reader_module._open_plugin_installer(test_path, error) - - # dock_widgets is a read-only mapping of widget names to widgets - # Find the plugin installer widget by name - widget = None - for name, docked_widget in viewer.window.dock_widgets.items(): - if 'Install BioIO Plugin' in name: - widget = docked_widget - break - - assert widget is not None - - # bioio-czi should be filtered out from installable_plugins - installable = widget.manager.installable_plugins - if installable: - assert 'bioio-czi' not in installable - - def test_suggests_uninstalled_plugins(self, make_napari_viewer): - """Test that uninstalled plugins are suggested.""" - from bioio_base.exceptions import UnsupportedFileFormatError - - import ndevio._napari_reader as reader_module - - viewer = make_napari_viewer() - test_path = 'test.lif' # LIF files need bioio-lif - error = UnsupportedFileFormatError( - reader_name='test', path=test_path, msg_extra='' - ) - - # Mock feasibility report with no bioio-lif installed with patch('bioio.plugin_feasibility_report') as mock_report: mock_report.return_value = { 'bioio-ome-tiff': Mock(supported=False), 'ArrayLike': Mock(supported=False), } - reader_module._open_plugin_installer(test_path, error) - # dock_widgets is a read-only mapping of widget names to widgets - # Find the plugin installer widget by name - widget = None - for name, docked_widget in viewer.window.dock_widgets.items(): - if 'Install BioIO Plugin' in name: - widget = docked_widget - break - + widget = self._find_plugin_installer_widget(viewer) assert widget is not None + assert 'bioio-lif' in widget.manager.installable_plugins - # bioio-lif should be in installable_plugins - installable = widget.manager.installable_plugins - assert installable is not None - assert 'bioio-lif' in installable - - -class TestPluginInstallerWidgetIntegration: - """Integration tests for PluginInstallerWidget with viewer.""" - - def test_widget_created_in_error_mode(self, make_napari_viewer): - """Test widget creation in error mode with viewer.""" - from ndevio._plugin_manager import ReaderPluginManager - from ndevio.widgets import PluginInstallerWidget - - make_napari_viewer() # Create viewer context - - # Create manager for a CZI file - with patch('bioio.plugin_feasibility_report') as mock_report: - # Mock report showing no plugins installed - mock_report.return_value = { - 'ArrayLike': Mock(supported=False), - } - - manager = ReaderPluginManager('test.czi') - widget = PluginInstallerWidget(plugin_manager=manager) - - # Verify widget state - assert widget.manager.path is not None - assert widget.manager.path.name == 'test.czi' - assert 'test.czi' in widget._title_label.value - - # bioio-czi should be in installable plugins and pre-selected - installable = widget.manager.installable_plugins - assert 'bioio-czi' in installable - assert widget._plugin_select.value == 'bioio-czi' - - def test_install_button_queues_installation(self, make_napari_viewer): - """Test that clicking install button queues installation.""" - from ndevio.widgets import PluginInstallerWidget - - make_napari_viewer() - - widget = PluginInstallerWidget() - - # Select a plugin - widget._plugin_select.value = 'bioio-imageio' - - # Mock the install_plugin function at the point of import - with patch('ndevio._plugin_installer.install_plugin') as mock_install: - mock_install.return_value = 123 # Mock job ID - - # Click install button - widget._on_install_clicked() - - # Verify install was called with correct plugin - mock_install.assert_called_once_with('bioio-imageio') - - def test_widget_shows_all_plugins(self, make_napari_viewer): - """Test that widget shows all available plugins.""" - from ndevio._bioio_plugin_utils import BIOIO_PLUGINS - from ndevio.widgets import PluginInstallerWidget - - make_napari_viewer() - - widget = PluginInstallerWidget() - - # Should have all plugins from manager's available_plugins - plugin_names = widget.manager.known_plugins - expected_names = list(BIOIO_PLUGINS.keys()) - - assert set(plugin_names) == set(expected_names) - - def test_widget_preselects_first_installable(self, make_napari_viewer): - """Test that first installable plugin is pre-selected.""" - from ndevio._plugin_manager import ReaderPluginManager - from ndevio.widgets import PluginInstallerWidget - - make_napari_viewer() - - # Mock report showing no plugins installed - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = { - 'ArrayLike': Mock(supported=False), - } - - manager = ReaderPluginManager('test.lif') - widget = PluginInstallerWidget(plugin_manager=manager) - - # bioio-lif should be pre-selected (first installable) - installable = widget.manager.installable_plugins - if installable: - assert widget._plugin_select.value == installable[0] - - def test_install_updates_status_label(self, make_napari_viewer): - """Test that status label updates during installation.""" - from ndevio.widgets import PluginInstallerWidget - - make_napari_viewer() - - widget = PluginInstallerWidget() - widget._plugin_select.value = 'bioio-imageio' - - with patch('ndevio._plugin_installer.install_plugin') as mock_install: - mock_install.return_value = 123 - - # Status should update to "Installing..." - widget._on_install_clicked() - assert 'Installing' in widget._status_label.value - - def test_no_plugin_selected_shows_error(self, make_napari_viewer): - """Test that clicking install with no selection shows error.""" - from ndevio.widgets import PluginInstallerWidget - - make_napari_viewer() - - widget = PluginInstallerWidget() - widget._plugin_select.value = None - - widget._on_install_clicked() - - assert 'No plugin selected' in widget._status_label.value + @staticmethod + def _find_plugin_installer_widget(viewer): + """Helper to find PluginInstallerWidget in viewer dock widgets.""" + for name, widget in viewer.window.dock_widgets.items(): + if 'Install BioIO Plugin' in name: + return widget + return None diff --git a/tests/test_plugin_installer_widget.py b/tests/test_plugin_installer_widget.py new file mode 100644 index 0000000..99b9331 --- /dev/null +++ b/tests/test_plugin_installer_widget.py @@ -0,0 +1,106 @@ +"""Tests for PluginInstallerWidget. + +This module tests the PluginInstallerWidget in _plugin_installer.py. +Widget unit tests do NOT require make_napari_viewer. + +For napari integration tests (docking widgets into viewer), +see test_plugin_installer_integration.py +""" + +from unittest.mock import Mock, patch + + +class TestPluginInstallerWidgetUnit: + """Unit tests for PluginInstallerWidget - no napari viewer required.""" + + def test_standalone_mode_default_state(self): + """Test widget creation in standalone mode (no path).""" + from ndevio.widgets import PluginInstallerWidget + + widget = PluginInstallerWidget() + + # Should have plugins available via manager + assert len(widget.manager.known_plugins) > 0 + assert widget.manager.path is None + assert 'Install BioIO Reader Plugin' in widget._title_label.value + + def test_error_mode_with_manager(self): + """Test widget creation with a manager (error mode).""" + from ndevio._plugin_manager import ReaderPluginManager + from ndevio.widgets import PluginInstallerWidget + + with patch('bioio.plugin_feasibility_report') as mock_report: + mock_report.return_value = {'ArrayLike': Mock(supported=False)} + + manager = ReaderPluginManager('test.czi') + widget = PluginInstallerWidget(plugin_manager=manager) + + # Should show filename in title + assert 'test.czi' in widget._title_label.value + # Should have installable plugins + assert 'bioio-czi' in widget.manager.installable_plugins + # First installable should be pre-selected + assert widget._plugin_select.value == 'bioio-czi' + + def test_preselects_first_installable_plugin(self): + """Test that first installable plugin is pre-selected.""" + from ndevio._plugin_manager import ReaderPluginManager + from ndevio.widgets import PluginInstallerWidget + + with patch('bioio.plugin_feasibility_report') as mock_report: + mock_report.return_value = {'ArrayLike': Mock(supported=False)} + + manager = ReaderPluginManager('test.lif') + widget = PluginInstallerWidget(plugin_manager=manager) + + installable = widget.manager.installable_plugins + assert len(installable) > 0 + assert widget._plugin_select.value == installable[0] + + def test_shows_all_known_plugins(self): + """Test that all known plugins are available for selection.""" + from ndevio._bioio_plugin_utils import BIOIO_PLUGINS + from ndevio.widgets import PluginInstallerWidget + + widget = PluginInstallerWidget() + + assert set(widget.manager.known_plugins) == set(BIOIO_PLUGINS.keys()) + + def test_no_plugin_selected_shows_error(self): + """Test clicking install with no selection shows error.""" + from ndevio.widgets import PluginInstallerWidget + + widget = PluginInstallerWidget() + widget._plugin_select.value = None + + widget._on_install_clicked() + + assert 'No plugin selected' in widget._status_label.value + + def test_install_button_queues_installation(self): + """Test that install button triggers installation.""" + from ndevio.widgets import PluginInstallerWidget + + widget = PluginInstallerWidget() + widget._plugin_select.value = 'bioio-imageio' + + with patch('ndevio._plugin_installer.install_plugin') as mock_install: + mock_install.return_value = 123 + + widget._on_install_clicked() + + mock_install.assert_called_once_with('bioio-imageio') + + def test_install_updates_status_label(self): + """Test that status label updates during installation.""" + from ndevio.widgets import PluginInstallerWidget + + widget = PluginInstallerWidget() + widget._plugin_select.value = 'bioio-imageio' + + with patch('ndevio._plugin_installer.install_plugin') as mock_install: + mock_install.return_value = 123 + + widget._on_install_clicked() + + assert 'Installing' in widget._status_label.value diff --git a/tests/test_plugin_manager.py b/tests/test_plugin_manager.py new file mode 100644 index 0000000..cf25449 --- /dev/null +++ b/tests/test_plugin_manager.py @@ -0,0 +1,208 @@ +"""Tests for ReaderPluginManager class from _plugin_manager module.""" + +import logging +from unittest.mock import Mock, patch + +import pytest + + +class TestReaderPluginManagerProperties: + """Test ReaderPluginManager properties (known_plugins, installed, etc.).""" + + def test_known_plugins_returns_all_bioio_plugins(self): + """Test that known_plugins returns all plugins from BIOIO_PLUGINS.""" + from ndevio._bioio_plugin_utils import BIOIO_PLUGINS + from ndevio._plugin_manager import ReaderPluginManager + + manager = ReaderPluginManager() + + assert set(manager.known_plugins) == set(BIOIO_PLUGINS.keys()) + + def test_installed_plugins_from_feasibility_report(self): + """Test that installed_plugins comes from bioio feasibility report.""" + from ndevio._plugin_manager import ReaderPluginManager + + with patch('bioio.plugin_feasibility_report') as mock_report: + mock_report.return_value = { + 'bioio-czi': Mock(supported=False), + 'bioio-ome-tiff': Mock(supported=True), + 'ArrayLike': Mock(supported=False), + } + + manager = ReaderPluginManager('test.czi') + + assert 'bioio-czi' in manager.installed_plugins + assert 'bioio-ome-tiff' in manager.installed_plugins + assert ( + 'ArrayLike' not in manager.installed_plugins + ) # Not a bioio plugin + + def test_suggested_plugins_based_on_extension(self): + """Test that suggested_plugins are determined by file extension.""" + from ndevio._plugin_manager import ReaderPluginManager + + with patch('bioio.plugin_feasibility_report') as mock_report: + mock_report.return_value = {'ArrayLike': Mock(supported=False)} + + manager = ReaderPluginManager('test.czi') + + assert 'bioio-czi' in manager.suggested_plugins + + def test_installable_plugins_excludes_installed(self): + """Test that installable_plugins excludes already installed plugins.""" + from ndevio._plugin_manager import ReaderPluginManager + + with patch('bioio.plugin_feasibility_report') as mock_report: + # bioio-czi shows as installed + mock_report.return_value = { + 'bioio-czi': Mock(supported=False), + 'ArrayLike': Mock(supported=False), + } + + manager = ReaderPluginManager('test.czi') + + # bioio-czi should NOT be in installable (it's installed) + assert 'bioio-czi' not in manager.installable_plugins + + def test_installable_plugins_excludes_core_plugins(self): + """Test that core plugins are excluded from installable list.""" + from ndevio._plugin_manager import ReaderPluginManager + + with patch('bioio.plugin_feasibility_report') as mock_report: + mock_report.return_value = {'ArrayLike': Mock(supported=False)} + + manager = ReaderPluginManager('test.tiff') + installable = manager.installable_plugins + + # Core plugins should never be in installable + core_plugins = [ + 'bioio-ome-tiff', + 'bioio-imageio', + 'bioio-ome-zarr', + 'bioio-tifffile', + ] + for core in core_plugins: + assert core not in installable + + # Non-core tiff plugin should be installable + assert 'bioio-tiff-glob' in installable + + +class TestReaderPluginManagerInstallable: + """Test ReaderPluginManager.installable_plugins for various file types.""" + + @pytest.mark.parametrize( + ('filename', 'expected_plugin'), + [ + ('test.czi', 'bioio-czi'), + ('test.lif', 'bioio-lif'), + ('test.nd2', 'bioio-nd2'), + ('test.dv', 'bioio-dv'), + ], + ) + def test_proprietary_formats_suggest_correct_plugin( + self, filename, expected_plugin + ): + """Test that proprietary formats suggest the correct plugin.""" + from ndevio._plugin_manager import ReaderPluginManager + + with patch('bioio.plugin_feasibility_report') as mock_report: + mock_report.return_value = {'ArrayLike': Mock(supported=False)} + + manager = ReaderPluginManager(filename) + plugins = manager.installable_plugins + + assert expected_plugin in plugins + + def test_unsupported_extension_returns_empty(self): + """Test that unsupported extensions return empty installable list.""" + from ndevio._plugin_manager import ReaderPluginManager + + with patch('bioio.plugin_feasibility_report') as mock_report: + mock_report.return_value = {'ArrayLike': Mock(supported=False)} + + manager = ReaderPluginManager('test.xyz') + plugins = manager.installable_plugins + + assert len(plugins) == 0 + + +class TestReaderPluginManagerGetWorkingReader: + """Test ReaderPluginManager.get_working_reader method.""" + + def test_no_path_returns_none(self, caplog): + """Test that get_working_reader returns None without a path.""" + from ndevio._plugin_manager import ReaderPluginManager + + manager = ReaderPluginManager() # No path + + with caplog.at_level(logging.WARNING): + result = manager.get_working_reader() + + assert result is None + assert 'Cannot get working reader without a path' in caplog.text + + def test_returns_working_reader_when_available(self, resources_dir): + """Test that get_working_reader returns a reader for supported files.""" + from ndevio._plugin_manager import ReaderPluginManager + + manager = ReaderPluginManager(resources_dir / 'cells3d2ch_legacy.tiff') + reader = manager.get_working_reader() + + assert reader is not None + # bioio-ome-tiff is preferred for OME-TIFF files + assert 'ome_tiff' in reader.__module__ + + +class TestReaderPluginManagerGetInstallationMessage: + """Test ReaderPluginManager.get_installation_message method.""" + + def test_no_path_returns_empty(self): + """Test that get_installation_message returns empty without path.""" + from ndevio._plugin_manager import ReaderPluginManager + + manager = ReaderPluginManager() # No path + + assert manager.get_installation_message() == '' + + def test_with_installable_plugins_returns_message(self): + """Test that message is generated when plugins are installable.""" + from ndevio._plugin_manager import ReaderPluginManager + + with patch('bioio.plugin_feasibility_report') as mock_report: + mock_report.return_value = {'ArrayLike': Mock(supported=False)} + + manager = ReaderPluginManager('test.czi') + message = manager.get_installation_message() + + assert 'bioio-czi' in message + assert 'pip install' in message or 'install' in message.lower() + + +class TestReaderPluginManagerFeasibilityReport: + """Test feasibility_report caching behavior.""" + + def test_feasibility_report_cached(self): + """Test that feasibility_report is cached.""" + from ndevio._plugin_manager import ReaderPluginManager + + with patch('bioio.plugin_feasibility_report') as mock_report: + mock_report.return_value = {'ArrayLike': Mock(supported=False)} + + manager = ReaderPluginManager('test.czi') + + # Access multiple times + _ = manager.feasibility_report + _ = manager.feasibility_report + _ = manager.feasibility_report + + # Should only be called once due to @cached_property + assert mock_report.call_count == 1 + + def test_no_path_returns_empty_report(self): + """Test that feasibility_report returns empty dict without path.""" + from ndevio._plugin_manager import ReaderPluginManager + + manager = ReaderPluginManager() # No path + + assert manager.feasibility_report == {} From 906c500134d6d042e59cb856009e609d129c99f3 Mon Sep 17 00:00:00 2001 From: Tim Monko Date: Tue, 9 Dec 2025 23:30:18 -0600 Subject: [PATCH 2/3] reorg tests --- tests/test_bioio_plugin_utils.py | 39 ++--- tests/test_plugin_installer_integration.py | 94 ---------- tests/test_plugin_installer_widget.py | 126 +++++++------- tests/test_plugin_manager.py | 189 ++++++--------------- 4 files changed, 129 insertions(+), 319 deletions(-) delete mode 100644 tests/test_plugin_installer_integration.py diff --git a/tests/test_bioio_plugin_utils.py b/tests/test_bioio_plugin_utils.py index 0e15530..9aeaf56 100644 --- a/tests/test_bioio_plugin_utils.py +++ b/tests/test_bioio_plugin_utils.py @@ -1,11 +1,9 @@ """Tests for _bioio_plugin_utils module. -This module tests the utility functions in _bioio_plugin_utils.py: -- suggest_plugins_for_path: suggests plugins based on file extension +This module tests: +- suggest_plugins_for_path: maps file extensions to bioio plugin names - format_plugin_installation_message: formats installation instructions - BIOIO_PLUGINS: the plugin metadata registry - -For ReaderPluginManager tests, see test_plugin_manager.py """ import pytest @@ -21,58 +19,49 @@ class TestSuggestPluginsForPath: ('test.lif', ['bioio-lif']), ('test.nd2', ['bioio-nd2']), ('test.dv', ['bioio-dv']), + ('test.xyz', []), # Unsupported returns empty ], ) - def test_proprietary_formats(self, filename, expected_plugins): - """Test that proprietary formats suggest correct plugins.""" + def test_extension_to_plugin_mapping(self, filename, expected_plugins): + """Test that file extensions map to correct plugin suggestions.""" from ndevio._bioio_plugin_utils import suggest_plugins_for_path plugins = suggest_plugins_for_path(filename) - assert plugins == expected_plugins - def test_tiff_suggests_all_tiff_plugins(self): + def test_tiff_suggests_multiple_plugins(self): """Test that TIFF files suggest all TIFF-compatible plugins.""" from ndevio._bioio_plugin_utils import suggest_plugins_for_path plugins = suggest_plugins_for_path('test.tiff') + # TIFF has multiple compatible readers assert 'bioio-ome-tiff' in plugins assert 'bioio-tifffile' in plugins assert 'bioio-tiff-glob' in plugins - def test_unsupported_extension_returns_empty(self): - """Test that unsupported extensions return empty list.""" - from ndevio._bioio_plugin_utils import suggest_plugins_for_path - - plugins = suggest_plugins_for_path('test.xyz') - - assert plugins == [] - class TestFormatPluginInstallationMessage: """Test format_plugin_installation_message function.""" - def test_czi_message_basic(self): - """Test message generation for CZI file.""" + def test_message_with_installable_plugins(self): + """Test message includes plugin name and install command.""" from ndevio._bioio_plugin_utils import ( format_plugin_installation_message, - suggest_plugins_for_path, ) - suggested = suggest_plugins_for_path('test.czi') message = format_plugin_installation_message( filename='test.czi', - suggested_plugins=suggested, - installed_plugins=set(), - installable_plugins=suggested, + suggested_plugins=['bioio-czi'], + installed_plugins=set(), # Not installed + installable_plugins=['bioio-czi'], ) assert 'bioio-czi' in message assert 'pip install' in message or 'conda install' in message - def test_unsupported_extension_message(self): - """Test message for completely unsupported extension.""" + def test_message_for_unsupported_extension(self): + """Test message for extension with no known plugins.""" from ndevio._bioio_plugin_utils import ( format_plugin_installation_message, ) diff --git a/tests/test_plugin_installer_integration.py b/tests/test_plugin_installer_integration.py deleted file mode 100644 index 93fd82b..0000000 --- a/tests/test_plugin_installer_integration.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Integration tests for plugin installer with napari viewer. - -These tests verify that: -- _open_plugin_installer correctly docks widgets into the viewer -- Widget integration with napari's dock system works - -For widget unit tests (no viewer required), see test_plugin_installer_widget.py -For ReaderPluginManager tests, see test_plugin_manager.py -""" - -from pathlib import Path -from unittest.mock import Mock, patch - - -class TestOpenPluginInstaller: - """Test _open_plugin_installer function with napari viewer.""" - - def test_opens_widget_in_viewer(self, make_napari_viewer): - """Test that _open_plugin_installer docks widget into viewer.""" - from bioio_base.exceptions import UnsupportedFileFormatError - - import ndevio._napari_reader as reader_module - - viewer = make_napari_viewer() - test_path = 'test.czi' - error = UnsupportedFileFormatError( - reader_name='test', path=test_path, msg_extra='' - ) - - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = { - 'bioio-ome-tiff': Mock(supported=False), - 'ArrayLike': Mock(supported=False), - } - reader_module._open_plugin_installer(test_path, error) - - # Widget should be docked in viewer - assert len(viewer.window.dock_widgets) > 0 - - # Find the plugin installer widget - widget = self._find_plugin_installer_widget(viewer) - assert widget is not None - - def test_widget_receives_correct_path(self, make_napari_viewer): - """Test that docked widget has the correct path set.""" - from bioio_base.exceptions import UnsupportedFileFormatError - - import ndevio._napari_reader as reader_module - - viewer = make_napari_viewer() - test_path = Path('path/to/test.czi') - error = UnsupportedFileFormatError( - reader_name='test', path=str(test_path), msg_extra='' - ) - - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = {} - reader_module._open_plugin_installer(test_path, error) - - widget = self._find_plugin_installer_widget(viewer) - assert widget is not None - assert widget.manager.path == test_path - assert test_path.name in widget._title_label.value - - def test_suggests_correct_plugins_for_extension(self, make_napari_viewer): - """Test that widget suggests correct plugins based on file extension.""" - from bioio_base.exceptions import UnsupportedFileFormatError - - import ndevio._napari_reader as reader_module - - viewer = make_napari_viewer() - test_path = 'test.lif' - error = UnsupportedFileFormatError( - reader_name='test', path=test_path, msg_extra='' - ) - - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = { - 'bioio-ome-tiff': Mock(supported=False), - 'ArrayLike': Mock(supported=False), - } - reader_module._open_plugin_installer(test_path, error) - - widget = self._find_plugin_installer_widget(viewer) - assert widget is not None - assert 'bioio-lif' in widget.manager.installable_plugins - - @staticmethod - def _find_plugin_installer_widget(viewer): - """Helper to find PluginInstallerWidget in viewer dock widgets.""" - for name, widget in viewer.window.dock_widgets.items(): - if 'Install BioIO Plugin' in name: - return widget - return None diff --git a/tests/test_plugin_installer_widget.py b/tests/test_plugin_installer_widget.py index 99b9331..f0b2a0c 100644 --- a/tests/test_plugin_installer_widget.py +++ b/tests/test_plugin_installer_widget.py @@ -1,73 +1,62 @@ """Tests for PluginInstallerWidget. -This module tests the PluginInstallerWidget in _plugin_installer.py. -Widget unit tests do NOT require make_napari_viewer. - -For napari integration tests (docking widgets into viewer), -see test_plugin_installer_integration.py +This module tests: +- PluginInstallerWidget behavior (unit tests, no viewer needed) +- _open_plugin_installer integration with napari viewer (needs viewer) """ +from pathlib import Path from unittest.mock import Mock, patch +import pytest + -class TestPluginInstallerWidgetUnit: - """Unit tests for PluginInstallerWidget - no napari viewer required.""" +class TestPluginInstallerWidget: + """Tests for PluginInstallerWidget behavior.""" - def test_standalone_mode_default_state(self): - """Test widget creation in standalone mode (no path).""" + def test_standalone_mode(self): + """Test widget in standalone mode - no path, shows all plugins.""" + from ndevio._bioio_plugin_utils import BIOIO_PLUGINS from ndevio.widgets import PluginInstallerWidget widget = PluginInstallerWidget() - # Should have plugins available via manager - assert len(widget.manager.known_plugins) > 0 + # Standalone mode: no path, generic title, all plugins available assert widget.manager.path is None assert 'Install BioIO Reader Plugin' in widget._title_label.value + assert set(widget.manager.known_plugins) == set(BIOIO_PLUGINS.keys()) - def test_error_mode_with_manager(self): - """Test widget creation with a manager (error mode).""" + def test_error_mode_with_path(self): + """Test widget in error mode - has path, preselects suggested plugin.""" from ndevio._plugin_manager import ReaderPluginManager from ndevio.widgets import PluginInstallerWidget with patch('bioio.plugin_feasibility_report') as mock_report: mock_report.return_value = {'ArrayLike': Mock(supported=False)} - manager = ReaderPluginManager('test.czi') widget = PluginInstallerWidget(plugin_manager=manager) - # Should show filename in title + # Error mode: has path, shows filename, preselects installable plugin assert 'test.czi' in widget._title_label.value - # Should have installable plugins assert 'bioio-czi' in widget.manager.installable_plugins - # First installable should be pre-selected assert widget._plugin_select.value == 'bioio-czi' - def test_preselects_first_installable_plugin(self): - """Test that first installable plugin is pre-selected.""" - from ndevio._plugin_manager import ReaderPluginManager - from ndevio.widgets import PluginInstallerWidget - - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = {'ArrayLike': Mock(supported=False)} - - manager = ReaderPluginManager('test.lif') - widget = PluginInstallerWidget(plugin_manager=manager) - - installable = widget.manager.installable_plugins - assert len(installable) > 0 - assert widget._plugin_select.value == installable[0] - - def test_shows_all_known_plugins(self): - """Test that all known plugins are available for selection.""" - from ndevio._bioio_plugin_utils import BIOIO_PLUGINS + def test_install_button_behavior(self): + """Test install button: queues installation and updates status.""" from ndevio.widgets import PluginInstallerWidget widget = PluginInstallerWidget() + widget._plugin_select.value = 'bioio-imageio' - assert set(widget.manager.known_plugins) == set(BIOIO_PLUGINS.keys()) + with patch('ndevio._plugin_installer.install_plugin') as mock_install: + mock_install.return_value = 123 + widget._on_install_clicked() + + mock_install.assert_called_once_with('bioio-imageio') + assert 'Installing' in widget._status_label.value - def test_no_plugin_selected_shows_error(self): - """Test clicking install with no selection shows error.""" + def test_install_without_selection_shows_error(self): + """Test that clicking install with no selection shows error.""" from ndevio.widgets import PluginInstallerWidget widget = PluginInstallerWidget() @@ -77,30 +66,47 @@ def test_no_plugin_selected_shows_error(self): assert 'No plugin selected' in widget._status_label.value - def test_install_button_queues_installation(self): - """Test that install button triggers installation.""" - from ndevio.widgets import PluginInstallerWidget - - widget = PluginInstallerWidget() - widget._plugin_select.value = 'bioio-imageio' - - with patch('ndevio._plugin_installer.install_plugin') as mock_install: - mock_install.return_value = 123 - - widget._on_install_clicked() - - mock_install.assert_called_once_with('bioio-imageio') - def test_install_updates_status_label(self): - """Test that status label updates during installation.""" - from ndevio.widgets import PluginInstallerWidget +class TestOpenPluginInstallerIntegration: + """Integration tests for _open_plugin_installer with napari viewer.""" - widget = PluginInstallerWidget() - widget._plugin_select.value = 'bioio-imageio' + @pytest.fixture + def viewer_with_plugin_installer(self, make_napari_viewer): + """Fixture that creates viewer and opens plugin installer for .czi.""" + from bioio_base.exceptions import UnsupportedFileFormatError - with patch('ndevio._plugin_installer.install_plugin') as mock_install: - mock_install.return_value = 123 + import ndevio._napari_reader as reader_module - widget._on_install_clicked() + viewer = make_napari_viewer() + test_path = Path('path/to/test.czi') + error = UnsupportedFileFormatError( + reader_name='test', path=str(test_path), msg_extra='' + ) - assert 'Installing' in widget._status_label.value + with patch('bioio.plugin_feasibility_report') as mock_report: + mock_report.return_value = {'ArrayLike': Mock(supported=False)} + reader_module._open_plugin_installer(test_path, error) + + # Find the widget + widget = None + for name, w in viewer.window.dock_widgets.items(): + if 'Install BioIO Plugin' in name: + widget = w + break + + return viewer, widget, test_path + + def test_docks_widget_with_correct_state( + self, viewer_with_plugin_installer + ): + """Test that _open_plugin_installer docks widget with correct state.""" + viewer, widget, test_path = viewer_with_plugin_installer + + # Widget is docked + assert len(viewer.window.dock_widgets) > 0 + assert widget is not None + + # Widget has correct path and suggestions + assert widget.manager.path == test_path + assert test_path.name in widget._title_label.value + assert 'bioio-czi' in widget.manager.installable_plugins diff --git a/tests/test_plugin_manager.py b/tests/test_plugin_manager.py index cf25449..3d9757b 100644 --- a/tests/test_plugin_manager.py +++ b/tests/test_plugin_manager.py @@ -1,15 +1,17 @@ -"""Tests for ReaderPluginManager class from _plugin_manager module.""" +"""Tests for ReaderPluginManager class from _plugin_manager module. + +Note: Extension-to-plugin mapping is tested in test_bioio_plugin_utils.py +via TestSuggestPluginsForPath. We trust those unit tests and don't duplicate here. +""" import logging from unittest.mock import Mock, patch -import pytest - -class TestReaderPluginManagerProperties: - """Test ReaderPluginManager properties (known_plugins, installed, etc.).""" +class TestReaderPluginManager: + """Tests for ReaderPluginManager properties and methods.""" - def test_known_plugins_returns_all_bioio_plugins(self): + def test_known_plugins_matches_bioio_plugins_registry(self): """Test that known_plugins returns all plugins from BIOIO_PLUGINS.""" from ndevio._bioio_plugin_utils import BIOIO_PLUGINS from ndevio._plugin_manager import ReaderPluginManager @@ -19,122 +21,73 @@ def test_known_plugins_returns_all_bioio_plugins(self): assert set(manager.known_plugins) == set(BIOIO_PLUGINS.keys()) def test_installed_plugins_from_feasibility_report(self): - """Test that installed_plugins comes from bioio feasibility report.""" + """Test that installed_plugins filters to bioio plugins from report.""" from ndevio._plugin_manager import ReaderPluginManager with patch('bioio.plugin_feasibility_report') as mock_report: mock_report.return_value = { 'bioio-czi': Mock(supported=False), 'bioio-ome-tiff': Mock(supported=True), - 'ArrayLike': Mock(supported=False), + 'ArrayLike': Mock(supported=False), # Not a bioio plugin } - manager = ReaderPluginManager('test.czi') + # Only bioio-* plugins should be included assert 'bioio-czi' in manager.installed_plugins assert 'bioio-ome-tiff' in manager.installed_plugins - assert ( - 'ArrayLike' not in manager.installed_plugins - ) # Not a bioio plugin - - def test_suggested_plugins_based_on_extension(self): - """Test that suggested_plugins are determined by file extension.""" - from ndevio._plugin_manager import ReaderPluginManager - - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = {'ArrayLike': Mock(supported=False)} - - manager = ReaderPluginManager('test.czi') - - assert 'bioio-czi' in manager.suggested_plugins + assert 'ArrayLike' not in manager.installed_plugins - def test_installable_plugins_excludes_installed(self): - """Test that installable_plugins excludes already installed plugins.""" + def test_installable_excludes_installed_and_core(self): + """Test installable_plugins excludes installed and core plugins.""" from ndevio._plugin_manager import ReaderPluginManager with patch('bioio.plugin_feasibility_report') as mock_report: - # bioio-czi shows as installed + # Simulate: bioio-ome-tiff installed, nothing else mock_report.return_value = { - 'bioio-czi': Mock(supported=False), + 'bioio-ome-tiff': Mock(supported=True), 'ArrayLike': Mock(supported=False), } - - manager = ReaderPluginManager('test.czi') - - # bioio-czi should NOT be in installable (it's installed) - assert 'bioio-czi' not in manager.installable_plugins - - def test_installable_plugins_excludes_core_plugins(self): - """Test that core plugins are excluded from installable list.""" - from ndevio._plugin_manager import ReaderPluginManager - - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = {'ArrayLike': Mock(supported=False)} - manager = ReaderPluginManager('test.tiff') installable = manager.installable_plugins - # Core plugins should never be in installable - core_plugins = [ - 'bioio-ome-tiff', - 'bioio-imageio', - 'bioio-ome-zarr', - 'bioio-tifffile', - ] - for core in core_plugins: - assert core not in installable - + # Core plugins never in installable (even if "suggested" by extension) + assert 'bioio-ome-tiff' not in installable + assert 'bioio-tifffile' not in installable # Non-core tiff plugin should be installable assert 'bioio-tiff-glob' in installable - -class TestReaderPluginManagerInstallable: - """Test ReaderPluginManager.installable_plugins for various file types.""" - - @pytest.mark.parametrize( - ('filename', 'expected_plugin'), - [ - ('test.czi', 'bioio-czi'), - ('test.lif', 'bioio-lif'), - ('test.nd2', 'bioio-nd2'), - ('test.dv', 'bioio-dv'), - ], - ) - def test_proprietary_formats_suggest_correct_plugin( - self, filename, expected_plugin - ): - """Test that proprietary formats suggest the correct plugin.""" + def test_feasibility_report_cached(self): + """Test that feasibility_report is cached via @cached_property.""" from ndevio._plugin_manager import ReaderPluginManager with patch('bioio.plugin_feasibility_report') as mock_report: mock_report.return_value = {'ArrayLike': Mock(supported=False)} + manager = ReaderPluginManager('test.czi') - manager = ReaderPluginManager(filename) - plugins = manager.installable_plugins - - assert expected_plugin in plugins - - def test_unsupported_extension_returns_empty(self): - """Test that unsupported extensions return empty installable list.""" - from ndevio._plugin_manager import ReaderPluginManager + # Access multiple times + _ = manager.feasibility_report + _ = manager.feasibility_report + _ = manager.installed_plugins # Also uses feasibility_report - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = {'ArrayLike': Mock(supported=False)} + # Should only call bioio once + assert mock_report.call_count == 1 - manager = ReaderPluginManager('test.xyz') - plugins = manager.installable_plugins - assert len(plugins) == 0 +class TestReaderPluginManagerNoPath: + """Tests for ReaderPluginManager edge cases when no path provided.""" + def test_feasibility_report_empty_without_path(self): + """Test that feasibility_report returns {} without path.""" + from ndevio._plugin_manager import ReaderPluginManager -class TestReaderPluginManagerGetWorkingReader: - """Test ReaderPluginManager.get_working_reader method.""" + manager = ReaderPluginManager() + assert manager.feasibility_report == {} - def test_no_path_returns_none(self, caplog): - """Test that get_working_reader returns None without a path.""" + def test_get_working_reader_returns_none_with_warning(self, caplog): + """Test get_working_reader returns None and logs warning without path.""" from ndevio._plugin_manager import ReaderPluginManager - manager = ReaderPluginManager() # No path + manager = ReaderPluginManager() with caplog.at_level(logging.WARNING): result = manager.get_working_reader() @@ -142,67 +95,23 @@ def test_no_path_returns_none(self, caplog): assert result is None assert 'Cannot get working reader without a path' in caplog.text - def test_returns_working_reader_when_available(self, resources_dir): - """Test that get_working_reader returns a reader for supported files.""" + def test_get_installation_message_returns_empty(self): + """Test get_installation_message returns '' without path.""" from ndevio._plugin_manager import ReaderPluginManager - manager = ReaderPluginManager(resources_dir / 'cells3d2ch_legacy.tiff') - reader = manager.get_working_reader() - - assert reader is not None - # bioio-ome-tiff is preferred for OME-TIFF files - assert 'ome_tiff' in reader.__module__ - - -class TestReaderPluginManagerGetInstallationMessage: - """Test ReaderPluginManager.get_installation_message method.""" - - def test_no_path_returns_empty(self): - """Test that get_installation_message returns empty without path.""" - from ndevio._plugin_manager import ReaderPluginManager - - manager = ReaderPluginManager() # No path - + manager = ReaderPluginManager() assert manager.get_installation_message() == '' - def test_with_installable_plugins_returns_message(self): - """Test that message is generated when plugins are installable.""" - from ndevio._plugin_manager import ReaderPluginManager - - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = {'ArrayLike': Mock(supported=False)} - - manager = ReaderPluginManager('test.czi') - message = manager.get_installation_message() - - assert 'bioio-czi' in message - assert 'pip install' in message or 'install' in message.lower() - -class TestReaderPluginManagerFeasibilityReport: - """Test feasibility_report caching behavior.""" +class TestReaderPluginManagerWithRealFiles: + """Tests using real files to verify end-to-end behavior.""" - def test_feasibility_report_cached(self): - """Test that feasibility_report is cached.""" - from ndevio._plugin_manager import ReaderPluginManager - - with patch('bioio.plugin_feasibility_report') as mock_report: - mock_report.return_value = {'ArrayLike': Mock(supported=False)} - - manager = ReaderPluginManager('test.czi') - - # Access multiple times - _ = manager.feasibility_report - _ = manager.feasibility_report - _ = manager.feasibility_report - - # Should only be called once due to @cached_property - assert mock_report.call_count == 1 - - def test_no_path_returns_empty_report(self): - """Test that feasibility_report returns empty dict without path.""" + def test_get_working_reader_for_ome_tiff(self, resources_dir): + """Test get_working_reader returns correct reader for OME-TIFF.""" from ndevio._plugin_manager import ReaderPluginManager - manager = ReaderPluginManager() # No path + manager = ReaderPluginManager(resources_dir / 'cells3d2ch_legacy.tiff') + reader = manager.get_working_reader() - assert manager.feasibility_report == {} + assert reader is not None + assert 'ome_tiff' in reader.__module__ From c42dbfbf202415f32f5dab162ade8b4def206d87 Mon Sep 17 00:00:00 2001 From: Tim Monko Date: Tue, 9 Dec 2025 23:39:47 -0600 Subject: [PATCH 3/3] clean up nimage and reader tests --- tests/test_bioio_plugin_utils.py | 8 ++--- tests/test_napari_reader.py | 10 ------ tests/test_nimage.py | 62 -------------------------------- 3 files changed, 4 insertions(+), 76 deletions(-) diff --git a/tests/test_bioio_plugin_utils.py b/tests/test_bioio_plugin_utils.py index 9aeaf56..06d8cc9 100644 --- a/tests/test_bioio_plugin_utils.py +++ b/tests/test_bioio_plugin_utils.py @@ -51,13 +51,13 @@ def test_message_with_installable_plugins(self): ) message = format_plugin_installation_message( - filename='test.czi', - suggested_plugins=['bioio-czi'], + filename='test.nd2', + suggested_plugins=['bioio-nd2'], installed_plugins=set(), # Not installed - installable_plugins=['bioio-czi'], + installable_plugins=['bioio-nd2'], ) - assert 'bioio-czi' in message + assert 'bioio-nd2' in message assert 'pip install' in message or 'conda install' in message def test_message_for_unsupported_extension(self): diff --git a/tests/test_napari_reader.py b/tests/test_napari_reader.py index f2611e3..3688b47 100644 --- a/tests/test_napari_reader.py +++ b/tests/test_napari_reader.py @@ -71,10 +71,8 @@ def test_reader_supported_formats( expected_dtype, expected_has_scale: bool, expected_num_layers: int, - make_napari_viewer, ) -> None: """Test reader with formats that should work with core dependencies.""" - make_napari_viewer() # Resolve filename to filepath if isinstance(filename, str): @@ -233,14 +231,6 @@ def test_napari_get_reader_general_exception(caplog): assert 'Test exception' in caplog.text -def test_napari_get_reader_png(resources_dir: Path) -> None: - reader = napari_get_reader( - str(resources_dir / PNG_FILE), - ) - - assert callable(reader) - - def test_napari_get_reader_supported_formats_work(resources_dir: Path): """Test that supported formats return valid readers.""" # PNG should work (bioio-imageio is core) diff --git a/tests/test_nimage.py b/tests/test_nimage.py index b4b5a46..8aa7122 100644 --- a/tests/test_nimage.py +++ b/tests/test_nimage.py @@ -14,7 +14,6 @@ from bioio_base.exceptions import UnsupportedFileFormatError from ndevio import nImage -from ndevio.nimage import determine_reader_plugin RGB_TIFF = ( 'RGB_bad_metadata.tiff' # has two scenes, with really difficult metadata @@ -204,10 +203,6 @@ def test_get_layer_data_tuples_ome_validation_error_logged( assert caplog.records[0].levelname == 'WARNING' assert 'Could not parse OME metadata' in caplog.records[0].message assert 'LatticeLightsheet' in caplog.records[0].message - assert len(caplog.records) == 1 - assert caplog.records[0].levelname == 'WARNING' - assert 'Could not parse OME metadata' in caplog.records[0].message - assert 'LatticeLightsheet' in caplog.records[0].message def test_get_layer_data_tuples_ome_not_implemented_silent( @@ -278,63 +273,6 @@ def test_get_layer_data_mosaic_tile_not_in_memory( assert img.napari_layer_data.shape == (3,) -@pytest.mark.parametrize( - ('filename', 'should_work', 'expected_plugin_suggestion'), - [ - (LOGO_PNG, True, None), # PNG works with bioio-imageio (core) - ( - CELLS3D2CH_OME_TIFF, - True, - None, - ), # OME-TIFF works with bioio-ome-tiff (core) - (CZI_FILE, True, None), - (ND2_FILE, False, 'bioio-nd2'), # ND2 needs bioio-nd2 - (RGB_TIFF, True, None), - ], -) -def test_determine_reader_plugin_behavior( - resources_dir: Path, - filename: str, - should_work: bool | str, - expected_plugin_suggestion: str | None, -): - """Test determine_reader_plugin with various file formats. - - Parameters - ---------- - filename : str - Test file name - should_work : bool | "maybe" - True = must succeed, False = must fail, "maybe" = can succeed or fail - expected_plugin_suggestion : str | None - If failure expected, the plugin name that should be suggested - """ - if should_work is True: - # Must successfully determine a reader - reader = determine_reader_plugin(resources_dir / filename) - assert reader is not None - elif should_work is False: - # Must fail with helpful error message - with pytest.raises(UnsupportedFileFormatError) as exc_info: - determine_reader_plugin(resources_dir / filename) - - error_msg = str(exc_info.value) - assert filename in error_msg - if expected_plugin_suggestion: - assert expected_plugin_suggestion in error_msg - assert 'pip install' in error_msg - else: # "maybe" - # Can succeed or fail; if fails, check for helpful message - try: - reader = determine_reader_plugin(resources_dir / filename) - assert reader is not None - except UnsupportedFileFormatError as e: - error_msg = str(e) - if expected_plugin_suggestion: - assert expected_plugin_suggestion in error_msg - assert 'pip install' in error_msg - - @pytest.mark.parametrize( ('filename', 'should_work', 'expected_error_contains'), [