Skip to content

Commit

Permalink
Support nose2 as testing framework instead of nose
Browse files Browse the repository at this point in the history
The nose testing framework has not done any release for well over five
years and needs to be retired. We replace it with nose2, which is
fairly similar but does not support all feautures. In particular, plugins
work differently in nose2 and spyder-unittest does not report the nose2
plugins.
  • Loading branch information
jitseniesen committed Apr 14, 2023
1 parent d043cde commit b3ce196
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 140 deletions.
2 changes: 1 addition & 1 deletion .github/issue_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,6 @@
* Version of spyder-unittest plugin:
* Installation method for Spyder and the unittest plugin: Anaconda / pip / ...
* Python version:
* Testing framework used: nose / py.test / unittest
* Testing framework used: nose2 / pytest / unittest
* Testing framework version:
* Operating system:
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Spyder-unittest is a plugin that integrates popular unit test frameworks
with Spyder, allowing you to run test suites and view the results in the IDE.

The plugin supports the `unittest` module in the Python standard library
as well as the `pytest` and `nose` testing frameworks.
as well as the `pytest` and `nose2` testing frameworks.
Support for `pytest` is most complete at the moment.


Expand Down Expand Up @@ -76,10 +76,10 @@ The plugin has the following dependencies:
* [spyder](https://github.com/spyder-ide/spyder) (obviously), at least version 4.0
* [lxml](http://lxml.de/)
* the testing framework that you will be using: [pytest](https://pytest.org)
and/or [nose](https://nose.readthedocs.io)
and/or [nose2](https://docs.nose2.io)

In order to run the tests distributed with this plugin, you need
[nose](https://nose.readthedocs.io), [pytest](https://pytest.org)
[nose2](https://docs.nose2.io), [pytest](https://pytest.org)
and [pytest-qt](https://github.com/pytest-dev/pytest-qt). If you use Python 2,
you also need [mock](https://github.com/testing-cabal/mock).

Expand Down
2 changes: 1 addition & 1 deletion requirements/tests.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
codecov
flaky
nose
nose2
pytest>=5
pytest-cov
pytest-qt
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def get_package_data(name, extlist):
frameworks. It allows you to run tests and view the results.
The plugin supports the `unittest` framework in the Python
standard library and the `pytest` and `nose` testing frameworks.
standard library and the `pytest` and `nose2` testing frameworks.
"""

setup(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@
_ = gettext.gettext


class NoseRunner(RunnerBase):
class Nose2Runner(RunnerBase):
"""Class for running tests within Nose framework."""

module = 'nose'
name = 'nose'
module = 'nose2'
name = 'nose2'

def create_argument_list(self, config, cov_path):
"""Create argument list for testing process."""
return [
'-m', self.module, '--with-xunit',
'--xunit-file={}'.format(self.resultfilename),
]
'-m', self.module, '--plugin=nose2.plugins.junitxml',
'--junit-xml', '--junit-xml-path={}'.format(self.resultfilename)
]

def finished(self):
"""Called when the unit test process has finished."""
Expand Down Expand Up @@ -81,7 +81,7 @@ def load_data(self):
message = type_
if child.text:
extras.append(child.text)
elif child.tag in ('system-out', 'system-err'):
elif child.tag in ('system-out', 'system-err') and child.text:
if child.tag == 'system-out':
heading = _('Captured stdout')
else:
Expand Down
42 changes: 42 additions & 0 deletions spyder_unittest/backend/tests/test_nose2runner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
#
# Copyright © 2013 Spyder Project Contributors
# Licensed under the terms of the MIT License
# (see LICENSE.txt for details)
"""Tests for nose2runner.py"""

# Local imports
from spyder_unittest.backend.nose2runner import Nose2Runner
from spyder_unittest.backend.runnerbase import Category


def test_nose2runner_load_data(tmpdir):
result_file = tmpdir.join('results')
result_txt = """
<testsuite name="nose2-junit" errors="0" failures="1" skipped="0" tests="2" time="0.000">
<testcase time="0.04" classname="test_foo" name="test1">
<system-out />
</testcase>
<testcase time="0.01" classname="test_foo" name="test2">
<failure message="test failure">text</failure>
<system-out />
</testcase>
</testsuite>"""
result_file.write(result_txt)
runner = Nose2Runner(None, result_file.strpath)
results = runner.load_data()
assert len(results) == 2

assert results[0].category == Category.OK
assert results[0].status == 'ok'
assert results[0].name == 'test_foo.test1'
assert results[0].message == ''
assert results[0].time == 0.04
assert results[0].extra_text == []

assert results[1].category == Category.FAIL
assert results[1].status == 'failure'
assert results[1].name == 'test_foo.test2'
assert results[1].message == 'test failure'
assert results[1].time == 0.01
assert results[1].extra_text == ['text']
76 changes: 0 additions & 76 deletions spyder_unittest/backend/tests/test_noserunner.py

This file was deleted.

26 changes: 12 additions & 14 deletions spyder_unittest/backend/workers/print_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,23 +37,21 @@ def pytest_cmdline_main(self, config):
'plugins': plugins}


def get_nose_info():
"""Return information about nose."""
from pkg_resources import iter_entry_points
def get_nose2_info():
"""
Return information about nose2.
This only returns the version of nose2. The function does not gather any
information about plugins.
"""
try:
import nose
import nose2
except ImportError:
return {'available': False}

plugins = {}
for entry_point, _ in (nose.plugins.manager.EntryPointPluginManager
.entry_points):
for ep in iter_entry_points(entry_point):
plugins[ep.dist.project_name] = ep.dist.version

return {'available': True,
'version': nose.__version__,
'plugins': plugins}
'version': nose2.__version__,
'plugins': {}}


def get_unittest_info():
Expand All @@ -75,11 +73,11 @@ def get_all_info():
Information is returned as a dictionary like the following:
{'pytest': {'available': True, 'version': '7.1.1',
'plugins': {'flaky': '3.7.0', 'pytest-mock': '3.6.1'}},
'nose': {'available': False},
'nose2': {'available': False},
'unittest': {'available': True, 'version': '3.10.5', 'plugins': {}}}
"""
return {'pytest': get_pytest_info(),
'nose': get_nose_info(),
'nose2': get_nose2_info(),
'unittest': get_unittest_info()}


Expand Down
29 changes: 5 additions & 24 deletions spyder_unittest/backend/workers/tests/test_print_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"""Tests for print_versions.py"""

from spyder_unittest.backend.workers.print_versions import (
get_nose_info, get_pytest_info, get_unittest_info)
get_nose2_info, get_pytest_info, get_unittest_info)


def test_get_pytest_info_without_plugins(monkeypatch):
Expand Down Expand Up @@ -37,30 +37,11 @@ def test_get_pytest_info_with_plugins(monkeypatch):
assert get_pytest_info() == expected


def test_get_nose_info_without_plugins(monkeypatch):
import nose
import pkg_resources
monkeypatch.setattr(nose, '__version__', '1.2.3')
monkeypatch.setattr(pkg_resources, 'iter_entry_points', lambda x: ())
def test_get_nose2_info(monkeypatch):
import nose2
monkeypatch.setattr(nose2, '__version__', '1.2.3')
expected = {'available': True, 'version': '1.2.3', 'plugins': {}}
assert get_nose_info() == expected


def test_get_nose_info_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]))
expected = {'available': True, 'version': '1.2.3',
'plugins': {'myPlugin': '4.5.6'}}
assert get_nose_info() == expected
assert get_nose2_info() == expected


def test_get_unittest_imfo(monkeypatch):
Expand Down
22 changes: 11 additions & 11 deletions spyder_unittest/widgets/tests/test_unittestgui.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,15 +151,15 @@ def test_unittestwidget_process_finished_abnormally_status_label(widget):
expected_text = '<b>{}</b>'.format('Test process exited abnormally')
assert widget.status_label.text() == expected_text

@pytest.mark.parametrize('framework', ['pytest', 'nose'])
@pytest.mark.parametrize('framework', ['pytest', 'nose2'])
def test_run_tests_and_display_results(qtbot, widget, tmpdir, monkeypatch, framework):
"""Basic integration test."""
os.chdir(tmpdir.strpath)
testfilename = tmpdir.join('test_foo.py').strpath

with open(testfilename, 'w') as f:
f.write("def test_ok(): assert 1+1 == 2\n"
"def test_fail(): assert 1+1 == 3\n")
f.write("def test_fail(): assert 1+1 == 3\n"
"def test_ok(): assert 1+1 == 2\n")

MockQMessageBox = Mock()
monkeypatch.setattr('spyder_unittest.widgets.unittestgui.QMessageBox',
Expand All @@ -173,14 +173,14 @@ def test_run_tests_and_display_results(qtbot, widget, tmpdir, monkeypatch, frame
model = widget.testdatamodel
assert model.rowCount() == 2
assert model.index(0, 0).data(
Qt.DisplayRole) == 'ok' if framework == 'nose' else 'passed'
assert model.index(0, 1).data(Qt.DisplayRole) == 't.test_ok'
assert model.index(0, 1).data(Qt.ToolTipRole) == 'test_foo.test_ok'
assert model.index(0, 2).data(Qt.DisplayRole) == ''
Qt.DisplayRole) == 'failure' if framework == 'nose2' else 'failed'
assert model.index(0, 1).data(Qt.DisplayRole) == 't.test_fail'
assert model.index(0, 1).data(Qt.ToolTipRole) == 'test_foo.test_fail'
assert model.index(1, 0).data(
Qt.DisplayRole) == 'failure' if framework == 'nose' else 'failed'
assert model.index(1, 1).data(Qt.DisplayRole) == 't.test_fail'
assert model.index(1, 1).data(Qt.ToolTipRole) == 'test_foo.test_fail'
Qt.DisplayRole) == 'ok' if framework == 'nose2' else 'passed'
assert model.index(1, 1).data(Qt.DisplayRole) == 't.test_ok'
assert model.index(1, 1).data(Qt.ToolTipRole) == 'test_foo.test_ok'
assert model.index(1, 2).data(Qt.DisplayRole) == ''


def test_run_tests_using_unittest_and_display_results(
Expand Down Expand Up @@ -215,7 +215,7 @@ def test_run_tests_using_unittest_and_display_results(
assert model.index(1, 1).data(Qt.ToolTipRole) == 'test_foo.MyTest.test_ok'
assert model.index(1, 2).data(Qt.DisplayRole) == ''

@pytest.mark.parametrize('framework', ['unittest', 'pytest', 'nose'])
@pytest.mark.parametrize('framework', ['unittest', 'pytest', 'nose2'])
def test_run_with_no_tests_discovered_and_display_results(
qtbot, widget, tmpdir, monkeypatch, framework):
"""Basic integration test."""
Expand Down
4 changes: 2 additions & 2 deletions spyder_unittest/widgets/unittestgui.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

# Local imports
from spyder_unittest.backend.frameworkregistry import FrameworkRegistry
from spyder_unittest.backend.noserunner import NoseRunner
from spyder_unittest.backend.nose2runner import Nose2Runner
from spyder_unittest.backend.pytestrunner import PyTestRunner
from spyder_unittest.backend.runnerbase import Category, TestResult
from spyder_unittest.backend.unittestrunner import UnittestRunner
Expand All @@ -37,7 +37,7 @@
_ = gettext.gettext

# Supported testing frameworks
FRAMEWORKS = {NoseRunner, PyTestRunner, UnittestRunner}
FRAMEWORKS = {Nose2Runner, PyTestRunner, UnittestRunner}


class UnitTestWidgetActions:
Expand Down

0 comments on commit b3ce196

Please sign in to comment.