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

add fixture usage validator #8

Merged
merged 2 commits into from
Nov 24, 2020
Merged
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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,24 @@ tests/test_unit/test_utils.py:128:1: FP008 stale xfail mark

in case you have too old `xfail` mark

6) validates that test function uses `pytest.mark.usefixtures`
for those fixtures, which are not directly referenced in test body

For example, checking this function

```python3
# file: test_something.py
def test_something(fixture_one, fixture_two):
assert fixture_two.some_attribute is not None
```

would raise:

```shell
tests/test_unit/test_something.py:2:0: FP009 test_something should use fixtures
as follows: @pytest.mark.usefixtures('fixture_one')
```

## Installation

```terminal
Expand Down
2 changes: 2 additions & 0 deletions flake8_fine_pytest/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
)
from flake8_fine_pytest.watchers.signature_complexity import SignatureComplexityWatcher
from flake8_fine_pytest.watchers.assert_count import AssertCountWatcher
from flake8_fine_pytest.watchers.usefixtures import UsefixturesWatcher
from flake8_fine_pytest.common_types import CheckResult


Expand All @@ -24,6 +25,7 @@ class FinePytestChecker:
SignatureComplexityWatcher,
AssertCountWatcher,
XfailUntilArgumentWatcher,
UsefixturesWatcher,
)

def __init__(self, tree: ast.AST, filename: str):
Expand Down
11 changes: 4 additions & 7 deletions flake8_fine_pytest/watchers/assert_count.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,20 @@ class AssertCountWatcher(BaseWatcher):
def run(self) -> None:
self.allowed_assert_count = self.options.allowed_assert_count

if self._should_check():
if self._should_run():
self._validate_assert_count(self.tree)

def _should_check(self) -> bool:
return self._is_test_file(self.filename) and self.allowed_assert_count is not None
def _should_run(self) -> bool:
return super()._should_run() and self.allowed_assert_count is not None

def _validate_assert_count(self, tree: ast.AST) -> None:
for node in ast.walk(tree):
if self._is_properly_node(node) is False:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the better solution is to move this method to BaseWatcher and override it if we need. I already suggested it in another PR

if self._should_check_node(node) is False:
continue

if self._is_valid(node) is False: # type: ignore
self._note_an_error(node)

def _is_properly_node(self, node: ast.AST) -> bool:
return isinstance(node, ast.FunctionDef) and node.name.startswith('test_')

def _is_valid(self, node: ast.FunctionDef) -> bool:
asserts_count = self._get_actual_asserts_count(node)

Expand Down
9 changes: 9 additions & 0 deletions flake8_fine_pytest/watchers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,12 @@ def run(self) -> None:
def _is_test_file(self, filename: str) -> bool:
stem = get_stem(self.filename)
return stem.startswith('test_')

def _is_test_function(self, node: ast.AST) -> bool:
return isinstance(node, ast.FunctionDef) and node.name.startswith('test_')

def _should_run(self) -> bool:
return self._is_test_file(self.filename)

def _should_check_node(self, node: ast.AST) -> bool:
return self._is_test_function(node)
30 changes: 17 additions & 13 deletions flake8_fine_pytest/watchers/modules_structure.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pathlib
from typing import List, Optional
from typing import List

from flake8_fine_pytest.watchers.base import BaseWatcher
from flake8_fine_pytest.utils import get_stem
Expand Down Expand Up @@ -33,15 +33,19 @@ class ModulesStructureWatcher(BaseWatcher):
def run(self) -> None:
allowed_test_directories = self.options.allowed_test_directories

if self._should_check(allowed_test_directories) is False:
return None

file_directory = get_file_directory(self.filename)

if file_directory not in allowed_test_directories:
error_message = get_error_message(self.error_template, allowed_test_directories, self.filename)

self.add_error((0, 0, error_message))

def _should_check(self, allowed_test_directories: Optional[List[str]]) -> bool:
return self._is_test_file(self.filename) and allowed_test_directories is not None
if self._should_run():
file_directory = get_file_directory(self.filename)

if file_directory not in allowed_test_directories:
error_message = get_error_message(
self.error_template,
allowed_test_directories,
self.filename,
)
self.add_error((0, 0, error_message))

def _should_run(self) -> bool:
return (
super()._should_run()
and self.options.allowed_test_directories is not None
)
11 changes: 4 additions & 7 deletions flake8_fine_pytest/watchers/signature_complexity.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,20 @@ class SignatureComplexityWatcher(BaseWatcher):
def run(self) -> None:
self.allowed_test_arguments_count = self.options.allowed_test_arguments_count

if self._should_check():
if self._should_run():
self._validate_signature_arguments_count(self.tree)

def _should_check(self) -> bool:
return self._is_test_file(self.filename) and self.allowed_test_arguments_count is not None
def _should_run(self) -> bool:
return super()._should_run() and self.allowed_test_arguments_count is not None

def _validate_signature_arguments_count(self, tree: ast.AST) -> None:
for node in ast.walk(tree):
if self._is_properly_node(node) is False:
if self._should_check_node(node) is False:
continue

if self._is_invalid_signature(node) is True: # type: ignore
self._note_an_error(node)

def _is_properly_node(self, node: ast.AST) -> bool:
return isinstance(node, ast.FunctionDef) and node.name.startswith('test_')

def _is_invalid_signature(self, ast_node: ast.FunctionDef) -> bool:
signature_arguments = ast_node.args.args

Expand Down
48 changes: 48 additions & 0 deletions flake8_fine_pytest/watchers/usefixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import ast
import typing

from flake8_fine_pytest.watchers.base import BaseWatcher


class UsefixturesWatcher(BaseWatcher):
error_template = (
'FP009 {test_name} should use fixtures as follows: '
'@pytest.mark.usefixtures({fixtures_list_as_str})'
)

def run(self) -> None:
if self._should_run():
self._validate_usefixtures_used_where_possible(self.tree)

def _validate_usefixtures_used_where_possible(self, tree: ast.AST) -> None:
for node in ast.walk(tree):
if not self._should_check_node(node):
continue

fixture_names = self._get_unreferenced_fixture_names(node) # type: ignore
if fixture_names:
self._add_usefixtures_error(node, fixture_names) # type: ignore

def _get_unreferenced_fixture_names(
self,
function_node: ast.FunctionDef,
) -> typing.List[str]:
referenced_variable_names = {
node.id
for node in ast.walk(function_node)
if isinstance(node, ast.Name)
}
test_fixture_names = {arg.arg for arg in function_node.args.args}

return sorted(test_fixture_names - referenced_variable_names)

def _add_usefixtures_error(
self,
function_node: ast.FunctionDef,
fixture_names: typing.List[str],
) -> None:
error_message = self.error_template.format(
test_name=function_node.name,
fixtures_list_as_str=', '.join(repr(name) for name in fixture_names),
)
self.add_error((function_node.lineno, 0, error_message))
13 changes: 4 additions & 9 deletions flake8_fine_pytest/watchers/xfail_until_argument_watcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,10 @@ class XfailUntilArgumentWatcher(BaseWatcher):
]

def run(self) -> None:
if self._should_check():
if self._should_run():
self._find_decorators_node_to_validate()

def _should_check(self) -> bool:
return self._is_test_file(self.filename)

def _is_properly_node(self, decorator: ast.Call) -> bool:
def _should_check_node(self, decorator: ast.Call) -> bool: # type: ignore
return (
hasattr(decorator, 'func')
and hasattr(decorator.func, 'attr')
Expand All @@ -85,10 +82,8 @@ def _find_decorators_node_to_validate(self) -> None:

def _validate_decorators(self, decorators: List[ast.Call]) -> None:
for decorator in decorators:
if self._is_properly_node(decorator) is False:
continue

self._validate_xfail_decorator_args(decorator)
if self._should_check_node(decorator):
self._validate_xfail_decorator_args(decorator)

def _validate_xfail_decorator_args(self, decorator: ast.Call) -> None:
xfail_args = [
Expand Down
1 change: 0 additions & 1 deletion tests/test_files/test_complex_signature_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ def three():
pass



def test_with_too_complex_signature(one, two, three):
assert (2 + 2) == 4

Expand Down
10 changes: 10 additions & 0 deletions tests/test_files/test_with_fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import pytest


def test_with_no_usefixtures_where_needed(caplog, capsys, tmp_path):
assert capsys


@pytest.mark.usefixtures('caplog', 'tmp_path')
def test_with_usefixtures_where_needed(capsys):
assert capsys
8 changes: 6 additions & 2 deletions tests/test_integration/test_signature_complexity.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
def test_signature_complexity(run_validator_for_test_files):
errors = run_validator_for_test_files('test_complex_signature_tests.py', allowed_test_arguments_count=2)
errors = run_validator_for_test_files(
'test_complex_signature_tests.py',
allowed_test_arguments_count=2,
)
FP004s = [error for error in errors if 'FP004' in error[2]]

assert len(errors) == 1
assert len(FP004s) == 1
10 changes: 10 additions & 0 deletions tests/test_integration/test_usefixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
def test_fixtures_should_be_in_usefixtures(run_validator_for_test_files):
expected_error_message = (
'FP009 test_with_no_usefixtures_where_needed should use '
"fixtures as follows: @pytest.mark.usefixtures('caplog', 'tmp_path')"
)

errors = run_validator_for_test_files('test_with_fixtures.py')

assert len(errors) == 1
assert errors[0][2] == expected_error_message