diff --git a/pylint_pytest/checkers/fixture.py b/pylint_pytest/checkers/fixture.py index 91c2af8..258e86d 100644 --- a/pylint_pytest/checkers/fixture.py +++ b/pylint_pytest/checkers/fixture.py @@ -1,5 +1,5 @@ import fnmatch -import os +import io import sys from pathlib import Path from typing import Set, Tuple @@ -68,8 +68,8 @@ class FixtureChecker(BasePytestChecker): "F6401": ( ( "pylint-pytest plugin cannot enumerate and collect pytest fixtures. " - "Please run `pytest --fixtures --collect-only path/to/current/module.py`" - " and resolve any potential syntax error or package dependency issues" + "Please run `pytest --fixtures --collect-only %s` and resolve " + "any potential syntax error or package dependency issues. stdout: %s. stderr: %s." ), "cannot-enumerate-pytest-fixtures", "Used when pylint-pytest has been unable to enumerate and collect pytest fixtures.", @@ -118,9 +118,10 @@ def visit_module(self, node): stdout, stderr = sys.stdout, sys.stderr try: - with open(os.devnull, "w") as devnull: + with io.StringIO() as captured_stdout, io.StringIO() as captured_stderr: # suppress any future output from pytest - sys.stderr = sys.stdout = devnull + sys.stderr = captured_stderr + sys.stdout = captured_stdout # run pytest session with customized plugin to collect fixtures fixture_collector = FixtureCollector() @@ -143,8 +144,32 @@ def visit_module(self, node): FixtureChecker._pytest_fixtures = fixture_collector.fixtures - if (ret != pytest.ExitCode.OK or fixture_collector.errors) and is_test_module: - self.add_message("cannot-enumerate-pytest-fixtures", node=node) + legitimate_failure_paths = set( + collection_report.nodeid + for collection_report in fixture_collector.errors + if any( + fnmatch.fnmatch( + Path(collection_report.nodeid).name, + pattern, + ) + for pattern in FILE_NAME_PATTERNS + ) + ) + if (ret != pytest.ExitCode.OK or legitimate_failure_paths) and is_test_module: + files_to_report = { + str(Path(x).absolute().relative_to(Path.cwd())) + for x in legitimate_failure_paths | {node.file} + } + + self.add_message( + "cannot-enumerate-pytest-fixtures", + args=( + " ".join(files_to_report), + captured_stdout.getvalue(), + captured_stderr.getvalue(), + ), + node=node, + ) finally: # restore output devices sys.stdout, sys.stderr = stdout, stderr diff --git a/tests/test_cannot_enumerate_fixtures.py b/tests/test_cannot_enumerate_fixtures.py index 4ad241e..362ab2a 100644 --- a/tests/test_cannot_enumerate_fixtures.py +++ b/tests/test_cannot_enumerate_fixtures.py @@ -1,3 +1,5 @@ +import re + import pytest from base_tester import BasePytestTester @@ -13,7 +15,39 @@ def test_no_such_package(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(1 if enable_plugin else 0) + if enable_plugin: + msg = self.msgs[0] + + # Asserts/Fixes duplicate filenames in output: + # https://github.com/reverbc/pylint-pytest/pull/22/files#r698204470 + filename_arg = msg.args[0] + assert len(re.findall(r"\.py", filename_arg)) == 1 + + # Asserts that path is relative (usually to the root of the repository). + assert filename_arg[0] != "/" + + # Assert `stdout` is non-empty. + assert msg.args[1] + # Assert `stderr` is empty (pytest runs stably, even though fixture collection fails). + assert not msg.args[2] + @pytest.mark.parametrize("enable_plugin", [True, False]) def test_import_corrupted_module(self, enable_plugin): self.run_linter(enable_plugin) self.verify_messages(1 if enable_plugin else 0) + + if enable_plugin: + msg = self.msgs[0] + + # ... somehow, since `import_corrupted_module.py` imports `no_such_package.py` + # both of their names are returned in the message. + filename_arg = msg.args[0] + assert len(re.findall(r"\.py", filename_arg)) == 2 + + # Asserts that paths are relative (usually to the root of the repository). + assert not [x for x in filename_arg.split(" ") if x[0] == "/"] + + # Assert `stdout` is non-empty. + assert msg.args[1] + # Assert `stderr` is empty (pytest runs stably, even though fixture collection fails). + assert not msg.args[2]