diff --git a/README.md b/README.md index 7c865f9..4c50d5d 100644 --- a/README.md +++ b/README.md @@ -52,12 +52,12 @@ class TestIngoredClass: # noqa: allure-validator ### Options The linter supports the following flags: -- `--plugins` - regex patterns that plugins must match (paths are compiled according to the template: `plugin / src / plugin`). +- `--plugins` - regex patterns that plugins must match (paths are compiled according to the template: `plugin_name / src / plugin_name`). - `--files` - paths to files with fixtures. Configuration options can also be provided using special command line arguments, for example: ```shell -allure-validator pytest_tests/ --plugins some[-_]plugin --files etc/workspace/some_plugin/src/some_plugin/fixtures.py +allure-validator --plugins , ... --files , ... ``` ## Work example diff --git a/pyproject.toml b/pyproject.toml index 6c0cd57..ff61976 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "allure-validator" -version = "1.0.0" +version = "1.1.1" description = "Linter for allure.title validation" readme = "README.md" authors = [{ name = "YADRO", email = "info@yadro.com" }] @@ -21,7 +21,7 @@ dependencies = [ "pytest>=7.1.2", "pytest-lazy-fixture>=0.6.3" ] -requires-python = "==3.10.*" +requires-python = ">=3.10" [project.optional-dependencies] dev = ["black", "pylint", "isort", "pre-commit"] diff --git a/src/allure_validator/collect.py b/src/allure_validator/collect.py index a71d11a..73fd9be 100644 --- a/src/allure_validator/collect.py +++ b/src/allure_validator/collect.py @@ -187,16 +187,23 @@ def fixtures(ast_fixtures: list[ast.FunctionDef], ast_hooks: list[ast.FunctionDe if parse.decorator(node.value) != PYTEST_METAFUNC_PARAMETRIZE: continue - fixture_names = parse.param_names(node.value.args[0]) + if len(node.value.args) < 2: + raise SyntaxError(f"{path}:{ast_hook.lineno}:{ast_hook.col_offset}: Unexpected syntax in {PYTEST_METAFUNC_PARAMETRIZE}") + + ast_names, ast_params, *_ = node.value.args + fixture_names = parse.param_names(ast_names) + for name in fixture_names: - # Dynamic fake fixture + params = parse.metafunc_params(ast_params) + + # Dynamic fixture fixture = Fixture( name, path, node.lineno, node.col_offset, args=[], - params=["parametrized"], + params=params, ) fixtures[fixture] = fixture diff --git a/src/allure_validator/common.py b/src/allure_validator/common.py index 71512d5..4d980d2 100644 --- a/src/allure_validator/common.py +++ b/src/allure_validator/common.py @@ -3,8 +3,10 @@ REPORTER_TITLE = "reporter.title" PYTEST_FIXTURE = "pytest.fixture" PYTEST_PARAMETRIZE = "pytest.mark.parametrize" +PYTEST_PARAM = "pytest.param" PYTEST_GENERATE_TESTS = "pytest_generate_tests" PYTEST_METAFUNC_PARAMETRIZE = "metafunc.parametrize" +PYTEST_FORCED_PARAMETRIZATION = ["forced", "parametrized", "fixture"] ATTR_PATH = "__func_path" ATTR_CLASS_NAME = "__class_name" @@ -14,5 +16,6 @@ VALIDATE_ERROR = 1 DIRECTORY_ERROR = 2 PLUGIN_ERROR = 3 FILE_ERROR = 4 +SYNTAX_ERROR = 5 ALLURE_VALIDATOR_IGNORE = "noqa: allure-validator" diff --git a/src/allure_validator/main.py b/src/allure_validator/main.py index 9f364c2..7d3bb2f 100644 --- a/src/allure_validator/main.py +++ b/src/allure_validator/main.py @@ -1,8 +1,6 @@ -import sys - from allure_validator import collect, validate from allure_validator.arguments import args -from allure_validator.common import DIRECTORY_ERROR, FILE_ERROR, PLUGIN_ERROR, VALIDATE_DONE, VALIDATE_ERROR +from allure_validator.common import DIRECTORY_ERROR, FILE_ERROR, PLUGIN_ERROR, SYNTAX_ERROR, VALIDATE_DONE, VALIDATE_ERROR def main(): @@ -18,22 +16,31 @@ def main(): paths.extend(files) except NotADirectoryError as e: print(e) - sys.exit(DIRECTORY_ERROR) + exit(DIRECTORY_ERROR) except ModuleNotFoundError as e: print(e) - sys.exit(PLUGIN_ERROR) + exit(PLUGIN_ERROR) except FileNotFoundError as e: print(e) - sys.exit(FILE_ERROR) + exit(FILE_ERROR) + except SyntaxError as e: + print(e) + exit(SYNTAX_ERROR) ast_items = collect.ast_pytest_items(paths) - files = collect.fixtures(ast_items.fixtures, ast_items.hooks, plugins) - tests = collect.tests(ast_items.tests, files, plugins) + fixtures = collect.fixtures(ast_items.fixtures, ast_items.hooks, plugins) + tests = collect.tests(ast_items.tests, fixtures, plugins) validate_result = validate.titles(tests) if validate_result.errors == 0: print(f"Done🎉\nFiles analyzed: {len(paths)}\nTests analyzed: {len(tests)}") - sys.exit(VALIDATE_DONE) + + if plugins: + patterns = ", ".join(args.plugins) + print(f"Found plugins with patterns: {patterns}") + print(*(f"-> {plugin}" for plugin in plugins), sep="\n") + + exit(VALIDATE_DONE) for title, tests in validate_result.not_unique.items(): print(f"NOT UNIQUE TITLE: {title}") @@ -49,4 +56,9 @@ def main(): params_str = " OR ".join(params) if isinstance(params, list) else params print(f"{test.link}: MISSING PARAMS: Parameters are missing from title: {params_str}") - sys.exit(VALIDATE_ERROR) + if plugins: + patterns = ", ".join(args.plugins) + print(f"Found plugins with patterns: {patterns}") + print(*(f"-> {plugin}" for plugin in plugins), sep="\n") + + exit(VALIDATE_ERROR) diff --git a/src/allure_validator/parse.py b/src/allure_validator/parse.py index 6b2a6ec..ec1f7b0 100644 --- a/src/allure_validator/parse.py +++ b/src/allure_validator/parse.py @@ -2,7 +2,15 @@ import ast import re from pathlib import Path -from allure_validator.common import ALLURE_TITLE, ALLURE_VALIDATOR_IGNORE, PYTEST_FIXTURE, PYTEST_PARAMETRIZE, REPORTER_TITLE +from allure_validator.common import ( + ALLURE_TITLE, + ALLURE_VALIDATOR_IGNORE, + PYTEST_FIXTURE, + PYTEST_FORCED_PARAMETRIZATION, + PYTEST_PARAM, + PYTEST_PARAMETRIZE, + REPORTER_TITLE, +) from allure_validator.pytest_items import TestParams @@ -130,25 +138,67 @@ def fixture_params(func: ast.FunctionDef) -> list[str]: """ ast_params = [] + for deco in func.decorator_list: if not isinstance(deco, ast.Call): continue for keyword in deco.keywords: if keyword.arg == "params": - ast_params.extend(keyword.value.elts) + if isinstance(keyword.value, ast.List | ast.Tuple): + ast_params.extend(keyword.value.elts) + else: + return PYTEST_FORCED_PARAMETRIZATION params = [] + for ast_param in ast_params: - for arg in ast_param.args: - params.append(ast.unparse(arg)) + if isinstance(ast_param, ast.Call) and decorator(ast_param) == PYTEST_PARAM: + params.extend(ast.unparse(arg) for arg in ast_param.args) + continue + + if not isinstance(ast_param, ast.Starred): + params.append(ast.unparse(ast_param)) + continue + + return PYTEST_FORCED_PARAMETRIZATION return params -def param_names(ast_names: ast.Constant | ast.List) -> list[str]: +def param_names(ast_names: ast.Constant | ast.List | ast.Tuple) -> list[str]: """Generates a list of parameter names from `pytest.parametrize`.""" if isinstance(ast_names, ast.Constant): return ast_names.value.replace(",", " ").split() return [elt.value for elt in ast_names.elts] + + +def metafunc_params(ast_params: ast.List | ast.Tuple) -> list: + """ + Extracts parameters for the fixture from the `params` argument of the `metafunc.parametrize` function. + + Args: + ast_params: Node from which parameters should be extracted. + + Returns: + list: Extracted parameters. + """ + + if not isinstance(ast_params, ast.List | ast.Tuple): + return PYTEST_FORCED_PARAMETRIZATION + + if any(isinstance(elt, ast.Starred) for elt in ast_params.elts): + return PYTEST_FORCED_PARAMETRIZATION + + params = [] + + for ast_param in ast_params.elts: + if isinstance(ast_param, ast.Constant): + params.append(ast_param.value) + elif isinstance(ast_param, ast.Name | ast.Attribute): + params.append(ast.unparse(ast_param)) + else: + SyntaxError(ast.unparse(ast_param)) + + return params diff --git a/src/allure_validator/pytest_items.py b/src/allure_validator/pytest_items.py index 9a1fe66..7642469 100644 --- a/src/allure_validator/pytest_items.py +++ b/src/allure_validator/pytest_items.py @@ -39,7 +39,7 @@ class Fixture(Item): @property def parametrized(self) -> bool: - return bool(self.params) + return len(self.params) > 1 @dataclass(eq=False) diff --git a/tests/conftest.py b/tests/conftest.py index d560c27..4025796 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,10 @@ import allure import pytest +def parametrize_fixture() -> list[int]: + return [1, 2, 3] + + @pytest.fixture( scope="function", params=[ @@ -34,3 +38,12 @@ def object_fixture(file_path): ) def parametrized_fixture(request: pytest.FixtureRequest): return request.param + + +@allure.title("[Session] Optional parametrized fixture") +@pytest.fixture( + scope="session", + params=[parametrize_fixture()], +) +def optional_parametrized_fixture(request: pytest.FixtureRequest): + return request.param diff --git a/tests/test_validator.py b/tests/test_validator.py index 7d49e5f..9720779 100644 --- a/tests/test_validator.py +++ b/tests/test_validator.py @@ -1,18 +1,91 @@ import allure import pytest +from frostfs_testlib.s3.aws_cli_client import AwsCliClient +from frostfs_testlib.s3.boto3_client import Boto3ClientWrapper + + +class Clients: + AttributeClient = ... + + +def parametrize_fixture() -> list[int]: + return [1, 2, 3] + # ============== DYNAMIC FIXTURE ============== # def pytest_generate_tests(metafunc: pytest.Metafunc): - if "runtime_fixture" not in metafunc.fixturenames: - return + # Required dynamic fixture + if "required_runtime_fixture" in metafunc.fixturenames: + metafunc.parametrize("required_runtime_fixture", [AwsCliClient, Boto3ClientWrapper, Clients.AttributeClient]) - metafunc.parametrize("runtime_fixture", [1, 2, 3]) + # Optional dynamic fixture 1 + if "optional_runtime_fixture_1" in metafunc.fixturenames: + metafunc.parametrize("optional_runtime_fixture_1", [True]) + + # Optional dynamic fixture 2 + if "optional_runtime_fixture_2" in metafunc.fixturenames: + metafunc.parametrize("optional_runtime_fixture_2", [parametrize_fixture()]) + + # Required dynamic fixtures + if "required_runtime_1" in metafunc.fixturenames and "required_runtime_2" in metafunc.fixturenames: + metafunc.parametrize("required_runtime_1,required_runtime_2", [(1, 2), (3, 4)]) + + # Optional dynamic fixtures + if "optional_runtime_1" in metafunc.fixturenames and "optional_runtime_2" in metafunc.fixturenames: + metafunc.parametrize(["optional_runtime_1", "optional_runtime_2"], [("optional", [1, 2, 3])]) + + # Required dynamic fixture with implicit parameterization 1 + if "implicitly_required_runtime_fixture_1" in metafunc.fixturenames: + metafunc.parametrize("implicitly_required_runtime_fixture_1", parametrize_fixture()) + + # Required dynamic fixture with implicit parameterization 2 + if "implicitly_required_runtime_fixture_2" in metafunc.fixturenames: + metafunc.parametrize("implicitly_required_runtime_fixture_2", [*parametrize_fixture()]) + + # Required dynamic fixture with implicit parameterization 3 + if "implicitly_required_runtime_fixture_3" in metafunc.fixturenames: + metafunc.parametrize("implicitly_required_runtime_fixture_3", [0, parametrize_fixture(), *parametrize_fixture()]) -@allure.title("Test runtime fixture") -def test_runtime_fixture(runtime_fixture): +@allure.title("Test required runtime fixture") +def test_required_runtime_fixture(required_runtime_fixture): + assert True + + +@allure.title("Test optional runtime fixture 1") +def test_optional_runtime_fixture_1(optional_runtime_fixture_1): + assert True + + +@allure.title("Test optional runtime fixture 2") +def test_optional_runtime_fixture_2(optional_runtime_fixture_2): + assert True + + +@allure.title("Test required runtime fixtures") +def test_required_runtime_fixtures(required_runtime_1, required_runtime_2): + assert True + + +@allure.title("Test optional runtime fixtures") +def test_optional_runtime_fixtures(optional_runtime_1, optional_runtime_2): + assert True + + +@allure.title("Test implicitly required runtime fixture 1") +def test_implicitly_required_runtime_fixture_1(implicitly_required_runtime_fixture_1): + assert True + + +@allure.title("Test implicitly required runtime fixture 2") +def test_implicitly_required_runtime_fixture_2(implicitly_required_runtime_fixture_2): + assert True + + +@allure.title("Test implicitly required runtime fixture 3") +def test_implicitly_required_runtime_fixture_3(implicitly_required_runtime_fixture_3): assert True @@ -117,6 +190,11 @@ def test_three_multiple_indirect(object_fixture: str): assert True +@allure.title("Test optional fixture") +def test_optional_fixture(optional_parametrized_fixture): + assert True + + # ============== TESTS/FIXTURES IN CLASS ============== #