diff --git a/CHANGELOG b/CHANGELOG index 90ebf6e49..52feb271f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,11 @@ Changes between 2.3.5 and 2.4.DEV when importing markers between modules. Specifying conditions as strings will remain fully supported. +- improved doctest counting for doctests in python modules -- + files without any doctest items will not show up anymore + and doctest examples are counted as separate test items. + thanks Danilo Bellini. + - fix issue245 by depending on the released py-1.4.14 which fixes py.io.dupfile to work with files with no mode. Thanks Jason R. Coombs. diff --git a/_pytest/doctest.py b/_pytest/doctest.py index 702315883..29ddf33ad 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -34,6 +34,14 @@ class ReprFailDoctest(TerminalRepr): self.reprlocation.toterminal(tw) class DoctestItem(pytest.Item): + def __init__(self, name, parent, runner=None, dtest=None): + super(DoctestItem, self).__init__(name, parent) + self.runner = runner + self.dtest = dtest + + def runtest(self): + self.runner.run(self.dtest) + def repr_failure(self, excinfo): doctest = py.std.doctest if excinfo.errisinstance((doctest.DocTestFailure, @@ -76,7 +84,7 @@ class DoctestItem(pytest.Item): return super(DoctestItem, self).repr_failure(excinfo) def reportinfo(self): - return self.fspath, None, "[doctest]" + return self.fspath, None, "[doctest] %s" % self.name class DoctestTextfile(DoctestItem, pytest.File): def runtest(self): @@ -91,8 +99,8 @@ class DoctestTextfile(DoctestItem, pytest.File): extraglobs=dict(getfixture=fixture_request.getfuncargvalue), raise_on_error=True, verbose=0) -class DoctestModule(DoctestItem, pytest.File): - def runtest(self): +class DoctestModule(pytest.File): + def collect(self): doctest = py.std.doctest if self.fspath.basename == "conftest.py": module = self.config._conftest.importconftest(self.fspath) @@ -102,7 +110,11 @@ class DoctestModule(DoctestItem, pytest.File): self.funcargs = {} self._fixtureinfo = FuncFixtureInfo((), [], {}) fixture_request = FixtureRequest(self) - failed, tot = doctest.testmod( - module, raise_on_error=True, verbose=0, - extraglobs=dict(getfixture=fixture_request.getfuncargvalue), - optionflags=doctest.ELLIPSIS) + doctest_globals = dict(getfixture=fixture_request.getfuncargvalue) + # uses internal doctest module parsing mechanism + finder = doctest.DocTestFinder() + runner = doctest.DebugRunner(verbose=0, optionflags=doctest.ELLIPSIS) + for test in finder.find(module, module.__name__, + extraglobs=doctest_globals): + if test.examples: # skip empty doctests + yield DoctestItem(test.name, self, runner, test) diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 564c675e6..ca5e1388d 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,4 +1,4 @@ -from _pytest.doctest import DoctestModule, DoctestTextfile +from _pytest.doctest import DoctestItem, DoctestModule, DoctestTextfile import py, pytest class TestDoctests: @@ -19,13 +19,61 @@ class TestDoctests: items, reprec = testdir.inline_genitems(w) assert len(items) == 1 - def test_collect_module(self, testdir): + def test_collect_module_empty(self, testdir): path = testdir.makepyfile(whatever="#") + for p in (path, testdir.tmpdir): + items, reprec = testdir.inline_genitems(p, + '--doctest-modules') + assert len(items) == 0 + + def test_collect_module_single_modulelevel_doctest(self, testdir): + path = testdir.makepyfile(whatever='""">>> pass"""') for p in (path, testdir.tmpdir): items, reprec = testdir.inline_genitems(p, '--doctest-modules') assert len(items) == 1 - assert isinstance(items[0], DoctestModule) + assert isinstance(items[0], DoctestItem) + assert isinstance(items[0].parent, DoctestModule) + + def test_collect_module_two_doctest_one_modulelevel(self, testdir): + path = testdir.makepyfile(whatever=""" + '>>> x = None' + def my_func(): + ">>> magic = 42 " + """) + for p in (path, testdir.tmpdir): + items, reprec = testdir.inline_genitems(p, + '--doctest-modules') + assert len(items) == 2 + assert isinstance(items[0], DoctestItem) + assert isinstance(items[1], DoctestItem) + assert isinstance(items[0].parent, DoctestModule) + assert items[0].parent is items[1].parent + + def test_collect_module_two_doctest_no_modulelevel(self, testdir): + path = testdir.makepyfile(whatever=""" + '# Empty' + def my_func(): + ">>> magic = 42 " + def unuseful(): + ''' + # This is a function + # >>> # it doesn't have any doctest + ''' + def another(): + ''' + # This is another function + >>> import os # this one does have a doctest + ''' + """) + for p in (path, testdir.tmpdir): + items, reprec = testdir.inline_genitems(p, + '--doctest-modules') + assert len(items) == 2 + assert isinstance(items[0], DoctestItem) + assert isinstance(items[1], DoctestItem) + assert isinstance(items[0].parent, DoctestModule) + assert items[0].parent is items[1].parent def test_simple_doctestfile(self, testdir): p = testdir.maketxtfile(test_doc=""" @@ -164,3 +212,47 @@ class TestDoctests: """) reprec = testdir.inline_run(p, "--doctest-modules") reprec.assertoutcome(passed=1) + + def test_doctestmodule_three_tests(self, testdir): + p = testdir.makepyfile(""" + ''' + >>> dir = getfixture('tmpdir') + >>> type(dir).__name__ + 'LocalPath' + ''' + def my_func(): + ''' + >>> magic = 42 + >>> magic - 42 + 0 + ''' + def unuseful(): + pass + def another(): + ''' + >>> import os + >>> os is os + True + ''' + """) + reprec = testdir.inline_run(p, "--doctest-modules") + reprec.assertoutcome(passed=3) + + def test_doctestmodule_two_tests_one_fail(self, testdir): + p = testdir.makepyfile(""" + class MyClass: + def bad_meth(self): + ''' + >>> magic = 42 + >>> magic + 0 + ''' + def nice_meth(self): + ''' + >>> magic = 42 + >>> magic - 42 + 0 + ''' + """) + reprec = testdir.inline_run(p, "--doctest-modules") + reprec.assertoutcome(failed=1, passed=1)