From 5fc1720d89b44dcce2027555fc388b42b423a565 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Wed, 11 Feb 2026 22:04:18 +0000 Subject: [PATCH 1/3] Display message if unexpected test result is received This may happen when pytest-xdist is used, because that pytest plugin does not report which tests are collected using the hook pytest_item_collected(). Before Spyder raised a KeyError in this situation. This commit shows a more user-friendly message instead. Also add a test for this. --- .../widgets/tests/test_unittestgui.py | 17 ++++++++++ spyder_unittest/widgets/unittestgui.py | 33 +++++++++++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/spyder_unittest/widgets/tests/test_unittestgui.py b/spyder_unittest/widgets/tests/test_unittestgui.py index 941b51f..459c292 100644 --- a/spyder_unittest/widgets/tests/test_unittestgui.py +++ b/spyder_unittest/widgets/tests/test_unittestgui.py @@ -106,6 +106,23 @@ def test_unittestwidget_tests_yield_results(widget): widget.tests_yield_result(results) widget.testdatamodel.update_testresults.assert_called_once_with(results) +def test_unittestwidget_tests_yield_results_with_error(widget): + """ + Test that if test_yield_result() raises a KeyError, a message is displayed, + but that no message is displayed on the second time. + + Regression test for spyder-ide/spyder-unittest#233. + """ + use_mock_model(widget) + widget.testdatamodel.update_testresults = Mock(side_effect=KeyError) + results = [TestResult(Category.OK, 'ok', 'hammodule.spam')] + with patch('spyder_unittest.widgets.unittestgui.QMessageBox') as mockQMessageBox: + widget.tests_yield_result(results) + mockQMessageBox.critical.assert_called() + with patch('spyder_unittest.widgets.unittestgui.QMessageBox') as mockQMessageBox: + widget.tests_yield_result(results) + mockQMessageBox.critical.assert_not_called() + def test_unittestwidget_set_message(widget): widget.status_label = Mock() widget.set_status_label('xxx') diff --git a/spyder_unittest/widgets/unittestgui.py b/spyder_unittest/widgets/unittestgui.py index 478a530..0c0b212 100644 --- a/spyder_unittest/widgets/unittestgui.py +++ b/spyder_unittest/widgets/unittestgui.py @@ -75,6 +75,9 @@ class UnitTestWidget(PluginMainWidget): Python interpreter for which `self.dependencies` is valid. framework_registry : FrameworkRegistry Registry of supported testing frameworks. + got_unexpected_testresult : bool + Whether we received the result of a test that was not collected in the + current test run. pre_test_hook : function returning bool or None If set, contains function to run before running tests; abort the test run if hook returns False. @@ -112,6 +115,7 @@ def __init__(self, name, plugin, parent): self.pre_test_hook = None self.pythonpath = None self.testrunner = None + self.got_unexpected_testresult = False self.testdataview = TestDataView(self) self.testdatamodel = TestDataModel(self) @@ -365,6 +369,7 @@ def run_tests(self, config=None, single_test=None): if self.pre_test_hook() is False: return + self.got_unexpected_testresult = False if config is None: config = self.config pythonpath = self.pythonpath @@ -473,7 +478,7 @@ def tests_started(self, testnames): testresults = [TestResult(Category.PENDING, _('pending'), name, message=_('running')) for name in testnames] - self.testdatamodel.update_testresults(testresults) + self.update_testresults_safe(testresults) def tests_collect_error(self, testnames_plus_msg): """Called when errors are encountered during collection.""" @@ -485,7 +490,31 @@ def tests_collect_error(self, testnames_plus_msg): def tests_yield_result(self, testresults): """Called when test results are received.""" - self.testdatamodel.update_testresults(testresults) + self.update_testresults_safe(testresults) + + def update_testresults_safe(self, testresults: list[TestResult]): + """ + Update test results in data model and handle errors. + + If a KeyError is raised (because one or more of the test results are + from test that were not collected earlier) and this is the first time + in the current test run, then display an dialog box explaining the + situation. + """ + try: + self.testdatamodel.update_testresults(testresults) + except KeyError: + if self.got_unexpected_testresult: + return + self.got_unexpected_testresult = True + msg = _( + "Spyder can not display the test results because it received " + "an unexpected test result." + "

" + "This may be caused by unsupported pytest plugins, " + "e.g., pytest-xdist." + ) + QMessageBox.critical(self, _("Error"), msg) def tests_stopped(self): """Called when tests are stopped""" From f6fb9e03152dac9be744278a2675507fb07a1991 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Wed, 11 Feb 2026 22:39:17 +0000 Subject: [PATCH 2/3] Tests: Replace pkg_resources with importlib GitHub no longer includes pkg_resources and it's been deprecated for a while. --- .../backend/workers/tests/test_print_versions.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/spyder_unittest/backend/workers/tests/test_print_versions.py b/spyder_unittest/backend/workers/tests/test_print_versions.py index 3fd4ddb..3236241 100644 --- a/spyder_unittest/backend/workers/tests/test_print_versions.py +++ b/spyder_unittest/backend/workers/tests/test_print_versions.py @@ -5,6 +5,9 @@ # (see LICENSE.txt for details) """Tests for print_versions.py""" +from importlib.metadata import Distribution +from unittest.mock import MagicMock + from spyder_unittest.backend.workers.print_versions import ( get_nose2_info, get_pytest_info, get_unittest_info) @@ -22,12 +25,13 @@ def test_get_pytest_info_without_plugins(monkeypatch): def test_get_pytest_info_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') + dist1 = MagicMock( + autospec=Distribution, project_name='myPlugin1', version='4.5.6' + ) + dist2 = MagicMock( + autospec=Distribution, project_name='myPlugin2', version='7.8.9' + ) from _pytest.config import PytestPluginManager monkeypatch.setattr( PytestPluginManager, From 5505797ab53d770e6793fd72e5f9fb20b1992f18 Mon Sep 17 00:00:00 2001 From: Jitse Niesen Date: Wed, 11 Feb 2026 23:01:43 +0000 Subject: [PATCH 3/3] Rename argument in pytest_report_header From pytest 7.0.0 onwards, the pytest_report_header hook uses the start_path parameter instead of the startdir parameter, which is deprecated (and raises an error when testing against pytest 9). --- requirements/tests.txt | 2 +- spyder_unittest/backend/workers/pytestworker.py | 2 +- spyder_unittest/backend/workers/tests/test_pytestworker.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/requirements/tests.txt b/requirements/tests.txt index fc2889e..9c77493 100644 --- a/requirements/tests.txt +++ b/requirements/tests.txt @@ -1,4 +1,4 @@ flaky nose2 -pytest>=5 +pytest>=7 pytest-qt diff --git a/spyder_unittest/backend/workers/pytestworker.py b/spyder_unittest/backend/workers/pytestworker.py index ee0dad9..5b81ba8 100644 --- a/spyder_unittest/backend/workers/pytestworker.py +++ b/spyder_unittest/backend/workers/pytestworker.py @@ -40,7 +40,7 @@ def initialize_logreport(self): self.was_skipped = False self.was_xfail = False - def pytest_report_header(self, config, startdir): + def pytest_report_header(self, config, start_path): """Called by pytest before any reporting.""" self.writer.write({ 'event': 'config', diff --git a/spyder_unittest/backend/workers/tests/test_pytestworker.py b/spyder_unittest/backend/workers/tests/test_pytestworker.py index 143747e..07ab278 100644 --- a/spyder_unittest/backend/workers/tests/test_pytestworker.py +++ b/spyder_unittest/backend/workers/tests/test_pytestworker.py @@ -14,7 +14,6 @@ # Third party imports import pytest -# Local imports # Local imports # Modules in spyder_unittest.backend.workers assume that their directory # is in `sys.path`, so add that directory to the path.