diff --git a/spyder_unittest/backend/noserunner.py b/spyder_unittest/backend/noserunner.py index 3ced2055..c4ce8b96 100644 --- a/spyder_unittest/backend/noserunner.py +++ b/spyder_unittest/backend/noserunner.py @@ -25,6 +25,20 @@ class NoseRunner(RunnerBase): module = 'nose' name = 'nose' + def get_versions(self): + """Return versions of framework and its plugins.""" + import nose + from pkg_resources import iter_entry_points + + versions = ['nose {}'.format(nose.__version__)] + + for entry_point, _ in (nose.plugins.manager.EntryPointPluginManager + .entry_points): + for ep in iter_entry_points(entry_point): + versions.append( + " {} {}".format(ep.dist.project_name, ep.dist.version)) + return versions + def create_argument_list(self): """Create argument list for testing process.""" return [ diff --git a/spyder_unittest/backend/pytestrunner.py b/spyder_unittest/backend/pytestrunner.py index d6b064b3..b9799a42 100644 --- a/spyder_unittest/backend/pytestrunner.py +++ b/spyder_unittest/backend/pytestrunner.py @@ -20,6 +20,26 @@ class PyTestRunner(RunnerBase): module = 'pytest' name = 'pytest' + def get_versions(self): + """Return versions of framework and its plugins.""" + import pytest + versions = ['pytest {}'.format(pytest.__version__)] + + class GetPluginVersionsPlugin(): + def pytest_cmdline_main(self, config): + nonlocal versions + plugininfo = config.pluginmanager.list_plugin_distinfo() + if plugininfo: + for plugin, dist in plugininfo: + versions.append(" {} {}".format(dist.project_name, + dist.version)) + + # --capture=sys needed on Windows to avoid + # ValueError: saved filedescriptor not valid anymore + pytest.main(['-V', '--capture=sys'], + plugins=[GetPluginVersionsPlugin()]) + return versions + def create_argument_list(self): """Create argument list for testing process.""" pyfile = os.path.join(os.path.dirname(__file__), 'pytestworker.py') diff --git a/spyder_unittest/backend/runnerbase.py b/spyder_unittest/backend/runnerbase.py index 63cbcfc8..a9f36043 100644 --- a/spyder_unittest/backend/runnerbase.py +++ b/spyder_unittest/backend/runnerbase.py @@ -147,6 +147,20 @@ def is_installed(cls): """ return find_spec_or_loader(cls.module) is not None + def get_versions(self): + """ + Return versions of framework and its installed plugins. + + This function must only be called for installed frameworks. + + Returns + ------- + list of str + Strings with framework or plugin name, followed by + its version. + """ + raise NotImplementedError + def create_argument_list(self): """ Create argument list for testing process (dummy). diff --git a/spyder_unittest/backend/tests/test_noserunner.py b/spyder_unittest/backend/tests/test_noserunner.py index 7bd92cb7..284efd2e 100644 --- a/spyder_unittest/backend/tests/test_noserunner.py +++ b/spyder_unittest/backend/tests/test_noserunner.py @@ -73,3 +73,28 @@ def test_noserunner_load_data_passing_test_with_stdout(tmpdir): runner = NoseRunner(None, result_file.strpath) results = runner.load_data() assert results[0].extra_text == ['----- Captured stdout -----', 'stdout text'] + + +def test_get_versions_without_plugins(monkeypatch): + import nose + import pkg_resources + monkeypatch.setattr(nose, '__version__', '1.2.3') + monkeypatch.setattr(pkg_resources, 'iter_entry_points', lambda x: ()) + runner = NoseRunner(None) + assert runner.get_versions() == ['nose 1.2.3'] + + +def test_get_versions_with_plugins(monkeypatch): + import nose + import pkg_resources + monkeypatch.setattr(nose, '__version__', '1.2.3') + dist = pkg_resources.Distribution(project_name='myPlugin', + version='4.5.6') + ep = pkg_resources.EntryPoint('name', 'module_name', dist=dist) + monkeypatch.setattr(pkg_resources, + 'iter_entry_points', + lambda ept: (x for x in (ep,) if ept == nose.plugins + .manager.EntryPointPluginManager + .entry_points[0][0])) + runner = NoseRunner(None) + assert runner.get_versions() == ['nose 1.2.3', ' myPlugin 4.5.6'] diff --git a/spyder_unittest/backend/tests/test_pytestrunner.py b/spyder_unittest/backend/tests/test_pytestrunner.py index ee0830f7..69768268 100644 --- a/spyder_unittest/backend/tests/test_pytestrunner.py +++ b/spyder_unittest/backend/tests/test_pytestrunner.py @@ -178,3 +178,32 @@ def test_logreport_to_testresult_with_output(): extra_text=txt, filename=osp.join('ham', 'foo.py'), lineno=24) assert logreport_to_testresult(report, Config(wdir='ham')) == expected + + +def test_get_versions_without_plugins(monkeypatch): + import pytest + monkeypatch.setattr(pytest, '__version__', '1.2.3') + from _pytest.config import PytestPluginManager + monkeypatch.setattr( + PytestPluginManager, + 'list_plugin_distinfo', lambda _: ()) + + runner = PyTestRunner(None) + assert runner.get_versions() == ['pytest 1.2.3'] + + +def test_get_versions_with_plugins(monkeypatch): + import pytest + import pkg_resources + monkeypatch.setattr(pytest, '__version__', '1.2.3') + dist1 = pkg_resources.Distribution(project_name='myPlugin1', + version='4.5.6') + dist2 = pkg_resources.Distribution(project_name='myPlugin2', + version='7.8.9') + from _pytest.config import PytestPluginManager + monkeypatch.setattr( + PytestPluginManager, + 'list_plugin_distinfo', lambda _: (('1', dist1), ('2', dist2))) + runner = PyTestRunner(None) + assert runner.get_versions() == ['pytest 1.2.3', ' myPlugin1 4.5.6', + ' myPlugin2 7.8.9'] diff --git a/spyder_unittest/backend/tests/test_unittestrunner.py b/spyder_unittest/backend/tests/test_unittestrunner.py index 1393cd8f..cfb71899 100644 --- a/spyder_unittest/backend/tests/test_unittestrunner.py +++ b/spyder_unittest/backend/tests/test_unittestrunner.py @@ -167,3 +167,10 @@ def test_try_parse_header_starting_with_digit(): lines = ['0est_isupper (testfoo.TestStringMethods) ... ok'] res = runner.try_parse_result(lines, 0) assert res is None + + +def test_get_versions(monkeypatch): + import platform + monkeypatch.setattr(platform, 'python_version', lambda: '1.2.3') + runner = UnittestRunner(None) + assert runner.get_versions() == ['unittest 1.2.3'] diff --git a/spyder_unittest/backend/unittestrunner.py b/spyder_unittest/backend/unittestrunner.py index f251ff87..d97f070d 100644 --- a/spyder_unittest/backend/unittestrunner.py +++ b/spyder_unittest/backend/unittestrunner.py @@ -18,6 +18,15 @@ class UnittestRunner(RunnerBase): module = 'unittest' name = 'unittest' + def get_versions(self): + """ + Return versions of framework and its plugins. + + As 'unittest' is a built-in framework, we return the python version. + """ + import platform + return ['unittest {}'.format(platform.python_version())] + def create_argument_list(self): """Create argument list for testing process.""" return ['-m', self.module, 'discover', '-v'] diff --git a/spyder_unittest/widgets/tests/test_unittestgui.py b/spyder_unittest/widgets/tests/test_unittestgui.py index c83f77c5..803f31a7 100644 --- a/spyder_unittest/widgets/tests/test_unittestgui.py +++ b/spyder_unittest/widgets/tests/test_unittestgui.py @@ -266,3 +266,30 @@ def test_stop_running_tests_before_testresult_is_received(qtbot, tmpdir): assert widget.testdatamodel.rowCount() == 0 assert widget.status_label.text() == '' + + +def test_show_versions(monkeypatch): + mockQMessageBox = Mock() + monkeypatch.setattr('spyder_unittest.widgets.unittestgui.QMessageBox', + mockQMessageBox) + widget = UnitTestWidget(None) + monkeypatch.setattr(widget.framework_registry.frameworks['nose'], + 'is_installed', lambda: False) + monkeypatch.setattr(widget.framework_registry.frameworks['pytest'], + 'is_installed', lambda: True) + monkeypatch.setattr(widget.framework_registry.frameworks['unittest'], + 'is_installed', lambda: True) + monkeypatch.setattr(widget.framework_registry.frameworks['nose'], + 'get_versions', lambda _: []) + monkeypatch.setattr(widget.framework_registry.frameworks['pytest'], + 'get_versions', + lambda _: ['pytest 1.2.3', ' plugin1 4.5.6', + ' plugin2 7.8.9']) + monkeypatch.setattr(widget.framework_registry.frameworks['unittest'], + 'get_versions', lambda _: ['unittest 1.2.3']) + widget.show_versions() + expected = ('Versions of frameworks and their installed plugins:\n\n' + 'nose: not available\n\npytest 1.2.3\n plugin1 4.5.6\n ' + 'plugin2 7.8.9\n\nunittest 1.2.3') + mockQMessageBox.information.assert_called_with(widget, 'Dependencies', + expected) diff --git a/spyder_unittest/widgets/unittestgui.py b/spyder_unittest/widgets/unittestgui.py index c15bb503..104c06f6 100644 --- a/spyder_unittest/widgets/unittestgui.py +++ b/spyder_unittest/widgets/unittestgui.py @@ -112,6 +112,7 @@ def __init__(self, parent, options_button=None, options_menu=None): self.options_menu.addAction(self.log_action) self.options_menu.addAction(self.collapse_action) self.options_menu.addAction(self.expand_action) + self.options_menu.addAction(self.versions_action) self.options_button = options_button or QToolButton(self) self.options_button.setIcon(ima.icon('tooloptions')) @@ -173,9 +174,14 @@ def create_actions(self): text=_('Expand all'), icon=ima.icon('expand'), triggered=self.testdataview.expandAll) + self.versions_action = create_action( + self, + text=_('Dependencies'), + icon=ima.icon('advanced'), + triggered=self.show_versions) return [ self.config_action, self.log_action, self.collapse_action, - self.expand_action + self.expand_action, self.versions_action ] def show_log(self): @@ -188,6 +194,17 @@ def show_log(self): te.show() te.exec_() + def show_versions(self): + """Show versions of frameworks and their plugins""" + versions = [_('Versions of frameworks and their installed plugins:')] + for name, runner in sorted(self.framework_registry.frameworks.items()): + version = (runner.get_versions(self) if runner.is_installed() + else None) + versions.append('\n'.join(version) if version else + '{}: {}'.format(name, _('not available'))) + QMessageBox.information(self, _('Dependencies'), + _('\n\n'.join(versions))) + def configure(self): """Configure tests.""" if self.config: