Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

options for context and fail when unused fixtures are present #4

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,41 @@ Running the tests is required to accurately record which fixtures are used, as p
$ pip install pytest-unused-fixtures
```

## Options

| Option Name | Type | Description |
|---------------------------------------|------------------|----------------------------------------------------------------|
| `--unused-fixtures` | switch | Enable plugin |
| `--unused-fixtures-ignore-path` | string* | Ignore paths for consideration of unused fixtures |
| `--unused-fixtures-context` | array\<string\> | Only consider fixtures missing if defined in these directories |
| `--unused-fixtures-fail-when-present` | switch | Fail pytest session if unused fixtures are present |


## Usage

After installing the package, the plugin is enabled by adding the switch `--unused-fixtures`.

Paths of fixtures can be ignored with one or multiple `--unused-fixtures-ignore-path` arguments. For example `--unused-fixtures-ignore-path=venv` will ignore all fixtures defined in the `venv` folder.

Alternatively, you can limit the scope in which the plugin looks for unused fixtures to a specific directory or directories. For example:

**Limit scope to one directory**
This example will only display unused fixtures that were defined in the `tests` folder
```shell
pytest tests --unused-fixtures --unused-fixtures-context tests
```

**Limit scope to multiple directories**
This example will only display unused fixtures that were defined in the `directory1` and `directory2/sub-directory` folders
```shell
pytest tests --unused-fixtures --unused-fixtures-context directory1 directory2/sub-directory
```

**Fail test session**
By default, when you use the `--unused-fixtures` switch, the plugin will exit with the same exit code pytest
would have used if running without the plugin. Add the switch `--unused-fixtures-fail-when-present` and the
pytest session will return a non-zero exit code if there are unused fixtures.

### Ignoring specific fixtures from report

Sometimes there will be fixture which are unused on purpose, for example when used in tests which are skipped by default. A decorator is provided for ignoring fixtures from the unused report. See the example for usage:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pytest-unused-fixtures"
version = "0.1.5"
version = "0.2.0"
description = "A pytest plugin to list unused fixtures after a test run."
authors = ["Mikuláš Poul <[email protected]>"]
license = "MIT"
Expand Down
18 changes: 17 additions & 1 deletion pytest_unused_fixtures/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ def pytest_addoption(parser: Parser, pluginmanager: PytestPluginManager) -> NoRe
action="append",
help="Ignore fixtures in PATHs from unused fixtures report.",
)
group.addoption(
"--unused-fixtures-fail-when-present",
action="store_true",
default=False,
help="Exit the pytest session with a failure code if unused fixtures are found",
)
group.addoption(
"--unused-fixtures-context",
metavar="PATH",
type=str,
nargs="+",
default=None,
help="Limit the scope of which to look for unused fixtures to these directories",
)


def pytest_configure(config: Config) -> NoReturn:
Expand All @@ -34,4 +48,6 @@ def pytest_configure(config: Config) -> NoReturn:

plugin = PytestUnusedFixturesPluginXdist

pluginmanager.register(plugin(config.getoption("--unused-fixtures-ignore-path")))
paths_ignore = config.getoption("--unused-fixtures-ignore-path")
unused_fixture_context = config.getoption("--unused-fixtures-context")
pluginmanager.register(plugin(paths_ignore, unused_fixture_context))
49 changes: 41 additions & 8 deletions pytest_unused_fixtures/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,13 @@ def pretty_path(self):


class PytestUnusedFixturesPlugin:
def __init__(self, ignore_paths: list[str | Path] | None = None):
def __init__(self, ignore_paths: list[str | Path] | None = None, context: list[str | Path] | None = None):
self.ignore_paths: list[Path] = [Path(x).resolve() for x in (ignore_paths or [])]
self.used_fixtures: set[FixtureInfo] = set()
self.available_fixtures: None | set[FixtureInfo] = None
self.curdir = Path().cwd()
self.context = context or []
self._shouldfail = False

def get_fixture_info(self, fixturedef: FixtureDef) -> FixtureInfo:
return FixtureInfo(
Expand All @@ -67,6 +69,8 @@ def pytest_collection_finish(self, session: Session) -> None:
for x in itertools.chain(*session._fixturemanager._arg2fixturedefs.values())
# if fixture is not in available fixtures, it won't be marked as unused
if not hasattr(x.func, "ignore_unused_fixture")
# baseid = '' when fixture comes from other plugins
and getattr(x, "baseid", None) != ""
}

def _write_fixtures(self, config: Config, terminalreporter: TerminalReporter, fixtures: set[FixtureInfo]):
Expand Down Expand Up @@ -105,20 +109,49 @@ def pytest_terminal_summary(
) -> NoReturn:
"""Add the fixture time report."""
fullwidth = config.get_terminal_writer().fullwidth
unused_fixtures = self._get_unused_fixtures()

# print fixtures
if unused_fixtures:
terminalreporter.write_sep(sep="=", title="UNUSED FIXTURES", fullwidth=fullwidth)
self._write_fixtures(config, terminalreporter, unused_fixtures)

def _get_unused_fixtures(self) -> set[FixtureInfo]:
# do a simple set operation to get the unused fixtures
unused_fixtures = self.available_fixtures - self.used_fixtures

# ignore unused fixtures from ignored paths
fixture: FixtureInfo
non_ignored_unused_fixtures = []
non_ignored_unused_fixtures = set()
for fixture in unused_fixtures:
if any(fixture.location_path.is_relative_to(x) or fixture.location_path == x for x in self.ignore_paths):
continue
non_ignored_unused_fixtures.append(fixture)
unused_fixtures = non_ignored_unused_fixtures
fixtures_in_context = [
fixture.location_path.is_relative_to(x) for x in [Path(i).resolve() for i in self.context]
]
if fixtures_in_context and not any(fixtures_in_context):
continue
non_ignored_unused_fixtures.add(fixture)

# print fixtures
if unused_fixtures:
terminalreporter.write_sep(sep="=", title="UNUSED FIXTURES", fullwidth=fullwidth)
self._write_fixtures(config, terminalreporter, unused_fixtures)
return non_ignored_unused_fixtures

@pytest.hookimpl(hookwrapper=True)
def pytest_runtestloop(self, session: Session):
yield

if not session.config.getoption("--unused-fixtures-fail-when-present", False):
return

unused_fixtures = self._get_unused_fixtures()
has_unused = unused_fixtures != set()

if has_unused:
message = f"Unused fixtures failure: total of {len(unused_fixtures)} unused fixtures"
session.config.pluginmanager.getplugin("terminalreporter").write(
f"\nERROR: {message}\n", red=True, bold=True
)
self._shouldfail = True

def pytest_sessionfinish(self, session: Session, exitstatus: int | ExitCode) -> None:
if self._shouldfail:
session.exitstatus = pytest.ExitCode.TESTS_FAILED
2 changes: 2 additions & 0 deletions pytest_unused_fixtures/xdist.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ def pytest_sessionfinish(self, session: Session, exitstatus: int | ExitCode) ->
workeroutput[_WORKEROUTPUT_KEY_AVAILABLE] = list(map(asdict, self.available_fixtures))
workeroutput[_WORKEROUTPUT_KEY_USED] = list(map(asdict, self.used_fixtures))

super().pytest_sessionfinish(session, exitstatus)

def pytest_testnodedown(self, node: WorkerController, error: Any | None) -> NoReturn:
if (workeroutput := getattr(node, "workeroutput", None)) is not None:
# add used & available fixtures from nodes
Expand Down
22 changes: 22 additions & 0 deletions tests/test_base_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,25 @@ def test_default(pytester, sample_testfile):
],
consecutive=True,
)


def test_option_context(pytester, sample_testfile):
"""
test the option `--unused-fixtures-context`.
"""
result = pytester.runpytest("--unused-fixtures", "--unused-fixtures-context", Path(__file__).parent)
result.assert_outcomes(passed=2)
result.stdout.no_fnmatch_line("*UNUSED FIXTURES*")


def test_fail_when_present(pytester, sample_testfile):
result = pytester.runpytest("--unused-fixtures", "--unused-fixtures-fail-when-present")
result.stdout.fnmatch_lines(["*ERROR: Unused fixtures failure: total of 1 unused fixtures*"])
result.stdout.fnmatch_lines(
[
"*UNUSED FIXTURES*",
"*fixtures defined from test_fail_when_present*",
"*fixture_c -- test_fail_when_present.py:12*",
],
)
assert result.ret == pytest.ExitCode.TESTS_FAILED
Loading