diff --git a/_pytest/main.py b/_pytest/main.py index 480810cc8..ec4ec2cc7 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -763,7 +763,11 @@ class Session(FSCollector): if not has_matched and len(rep.result) == 1 and x.name == "()": nextnames.insert(0, name) resultnodes.extend(self.matchnodes([x], nextnames)) - node.ihook.pytest_collectreport(report=rep) + else: + # report collection failures here to avoid failing to run some test + # specified in the command line because the module could not be + # imported (#134) + node.ihook.pytest_collectreport(report=rep) return resultnodes def genitems(self, node): diff --git a/_pytest/terminal.py b/_pytest/terminal.py index e226d607b..af89d0fc2 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -282,7 +282,7 @@ class TerminalReporter: line = "collected " else: line = "collecting " - line += str(self._numcollected) + " items" + line += str(self._numcollected) + " item" + ('' if self._numcollected == 1 else 's') if errors: line += " / %d errors" % errors if skipped: diff --git a/changelog/2464.bugfix b/changelog/2464.bugfix new file mode 100644 index 000000000..12062fd9e --- /dev/null +++ b/changelog/2464.bugfix @@ -0,0 +1 @@ +Fix incorrect "collected items" report when specifying tests on the command-line. diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 00abfc38d..9cb2650de 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -317,8 +317,8 @@ class TestGeneralUsage(object): ]) assert 'sessionstarttime' not in result.stderr.str() - @pytest.mark.parametrize('lookfor', ['test_fun.py', 'test_fun.py::test_a']) - def test_issue134_report_syntaxerror_when_collecting_member(self, testdir, lookfor): + @pytest.mark.parametrize('lookfor', ['test_fun.py::test_a']) + def test_issue134_report_error_when_collecting_member(self, testdir, lookfor): testdir.makepyfile(test_fun=""" def test_a(): pass diff --git a/testing/test_collection.py b/testing/test_collection.py index c19fc0e72..a90269789 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -369,6 +369,11 @@ class TestSession(object): assert len(colitems) == 1 assert colitems[0].fspath == p + def get_reported_items(self, hookrec): + """Return pytest.Item instances reported by the pytest_collectreport hook""" + calls = hookrec.getcalls('pytest_collectreport') + return [x for call in calls for x in call.report.result + if isinstance(x, pytest.Item)] def test_collect_protocol_single_function(self, testdir): p = testdir.makepyfile("def test_func(): pass") @@ -386,9 +391,10 @@ class TestSession(object): ("pytest_collectstart", "collector.fspath == p"), ("pytest_make_collect_report", "collector.fspath == p"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.nodeid.startswith(p.basename)"), - ("pytest_collectreport", "report.nodeid == ''") + ("pytest_collectreport", "report.result[0].name == 'test_func'"), ]) + # ensure we are reporting the collection of the single test item (#2464) + assert [x.name for x in self.get_reported_items(hookrec)] == ['test_func'] def test_collect_protocol_method(self, testdir): p = testdir.makepyfile(""" @@ -407,6 +413,8 @@ class TestSession(object): assert items[0].name == "test_method" newid = items[0].nodeid assert newid == normid + # ensure we are reporting the collection of the single test item (#2464) + assert [x.name for x in self.get_reported_items(hookrec)] == ['test_method'] def test_collect_custom_nodes_multi_id(self, testdir): p = testdir.makepyfile("def test_func(): pass") @@ -436,9 +444,8 @@ class TestSession(object): "collector.__class__.__name__ == 'Module'"), ("pytest_pycollect_makeitem", "name == 'test_func'"), ("pytest_collectreport", "report.nodeid.startswith(p.basename)"), - #("pytest_collectreport", - # "report.fspath == %r" % str(rcol.fspath)), ]) + assert len(self.get_reported_items(hookrec)) == 2 def test_collect_subdir_event_ordering(self, testdir): p = testdir.makepyfile("def test_func(): pass") @@ -495,11 +502,13 @@ class TestSession(object): def test_method(self): pass """) - arg = p.basename + ("::TestClass::test_method") + arg = p.basename + "::TestClass::test_method" items, hookrec = testdir.inline_genitems(arg) assert len(items) == 1 item, = items assert item.nodeid.endswith("TestClass::()::test_method") + # ensure we are reporting the collection of the single test item (#2464) + assert [x.name for x in self.get_reported_items(hookrec)] == ['test_method'] class Test_getinitialnodes(object): def test_global_file(self, testdir, tmpdir): diff --git a/testing/test_runner.py b/testing/test_runner.py index 51d430fc8..def80ea5f 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -513,12 +513,12 @@ def test_pytest_no_tests_collected_exit_status(testdir): assert 1 """) result = testdir.runpytest() - result.stdout.fnmatch_lines('*collected 1 items*') + result.stdout.fnmatch_lines('*collected 1 item*') result.stdout.fnmatch_lines('*1 passed*') assert result.ret == main.EXIT_OK result = testdir.runpytest('-k nonmatch') - result.stdout.fnmatch_lines('*collected 1 items*') + result.stdout.fnmatch_lines('*collected 1 item*') result.stdout.fnmatch_lines('*1 deselected*') assert result.ret == main.EXIT_NOTESTSCOLLECTED diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 5a90b3dd4..45c354206 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -204,6 +204,15 @@ class TestTerminal(object): assert result.ret == 2 result.stdout.fnmatch_lines(['*KeyboardInterrupt*']) + def test_collect_single_item(self, testdir): + """Use singular 'item' when reporting a single test item""" + testdir.makepyfile(""" + def test_foobar(): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(['collected 1 item']) + class TestCollectonly(object): def test_collectonly_basic(self, testdir):