From b65c42fd4e8ade68a5db00d8f089cf3285bd127e Mon Sep 17 00:00:00 2001 From: John Szakmeister Date: Mon, 17 Feb 2014 10:42:47 -0500 Subject: [PATCH] Fix #771: attr plugin is broken when parent overrides child method The issue is when the parent and child have different attributes, and the parent method was being bypassed because of the attribute selection. In this case, Nose would incorrectly use the version from the base class, even though it was supposed to skip the method entirely. To fix this, we need simply need to stop digging through base classes. The dir() method returns a flattened set of methods, so there's no need to iterate through the base classes trying to dig up all the methods. Moreover, it leads to false positives since we were not keeping track of methods seen on the parent classes. As a result, we'd incorrectly select a test for inclusion (using attributes), or we'd pick up a method that we should've ignored (like runTest in a Twisted test case). Thanks to Thomas Grainger for providing a test case! --- CHANGELOG | 3 +++ functional_tests/support/issue771/test.py | 16 ++++++++++++++++ functional_tests/test_attribute_plugin.py | 14 ++++++++++++++ functional_tests/test_program.py | 12 ++---------- nose/loader.py | 8 +++----- 5 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 functional_tests/support/issue771/test.py diff --git a/CHANGELOG b/CHANGELOG index 6a774713..33acaa7a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -64,6 +64,9 @@ In Development - Display the report summary and stack traces even if Ctrl-C was pressed during the test run. Patch by Kevin Qiu. +- Fix #771: attr plugin is broken when parent and child share same method + name with different attributes + Patch by John Szakmeister. Test case provided by Thomas Grainger. 1.3.0 diff --git a/functional_tests/support/issue771/test.py b/functional_tests/support/issue771/test.py new file mode 100644 index 00000000..a81eaa16 --- /dev/null +++ b/functional_tests/support/issue771/test.py @@ -0,0 +1,16 @@ +from nose.plugins.attrib import attr + +from unittest import TestCase + +@attr("b") +def test_b(): + assert 1 == 1 + +class TestBase(TestCase): + def test_a(self): + assert 1 == 1 + +class TestDerived(TestBase): + @attr("a") + def test_a(self): + assert 1 == 1 diff --git a/functional_tests/test_attribute_plugin.py b/functional_tests/test_attribute_plugin.py index a093cd59..249bec5f 100644 --- a/functional_tests/test_attribute_plugin.py +++ b/functional_tests/test_attribute_plugin.py @@ -156,6 +156,20 @@ def verify(self): assert 'test_case_three' not in self.output +# Issue #771 +class TestTopLevelNotSelected(AttributePluginTester): + suitepath = os.path.join(support, 'issue771') + args = ["-a", "!a"] + + def verify(self): + # Note: a failure here may mean that the test case selection is broken + # rather than the attribute plugin, but the issue more easily manifests + # itself when using attributes. + assert 'test.test_b ... ok' in self.output + assert 'test_a (test.TestBase) ... ok' in self.output + assert 'TestDerived' not in self.output + + if compat_24: class TestAttributeEval(AttributePluginTester): args = ["-A", "c>20"] diff --git a/functional_tests/test_program.py b/functional_tests/test_program.py index b42b3717..6ff600d0 100644 --- a/functional_tests/test_program.py +++ b/functional_tests/test_program.py @@ -113,16 +113,8 @@ def test_run_support_twist(self): print "-----" print repr(res) - # some versions of twisted.trial.unittest.TestCase have - # runTest in the base class -- this is wrong! But we have - # to deal with it - if hasattr(TestCase, 'runTest'): - expect = 5 - else: - expect = 4 - self.assertEqual(res.testsRun, expect, - "Expected to run %s tests, ran %s" % - (expect, res.testsRun)) + self.assertEqual(res.testsRun, 4, + "Expected to run 4 tests, ran %s" % (res.testsRun,)) assert not res.wasSuccessful() assert len(res.errors) == 1 diff --git a/nose/loader.py b/nose/loader.py index 480c8981..4152a122 100644 --- a/nose/loader.py +++ b/nose/loader.py @@ -104,7 +104,7 @@ def getTestCaseNames(self, testCaseClass): """ if self.config.getTestCaseNamesCompat: return unittest.TestLoader.getTestCaseNames(self, testCaseClass) - + def wanted(attr, cls=testCaseClass, sel=self.selector): item = getattr(cls, attr, None) if isfunction(item): @@ -112,11 +112,9 @@ def wanted(attr, cls=testCaseClass, sel=self.selector): elif not ismethod(item): return False return sel.wantMethod(item) + cases = filter(wanted, dir(testCaseClass)) - for base in testCaseClass.__bases__: - for case in self.getTestCaseNames(base): - if case not in cases: - cases.append(case) + # add runTest if nothing else picked if not cases and hasattr(testCaseClass, 'runTest'): cases = ['runTest']