Skip to content

Commit

Permalink
Fix #771: attr plugin is broken when parent overrides child method
Browse files Browse the repository at this point in the history
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!
  • Loading branch information
jszakmeister committed Feb 20, 2014
1 parent 129bd91 commit b65c42f
Show file tree
Hide file tree
Showing 5 changed files with 38 additions and 15 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 16 additions & 0 deletions functional_tests/support/issue771/test.py
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions functional_tests/test_attribute_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
12 changes: 2 additions & 10 deletions functional_tests/test_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 3 additions & 5 deletions nose/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,19 +104,17 @@ 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):
item = unbound_method(cls, item)
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']
Expand Down

0 comments on commit b65c42f

Please sign in to comment.