Compare commits

..

No commits in common. "master" and "master" have entirely different histories.

10 changed files with 29 additions and 194 deletions

View file

@ -1,3 +1 @@
.* @TrueCloudLab/qa-committers @Kiriruso * @JuliaKovshova @abereziny @anikeev-yadro @Kiriruso
.forgejo/.* @potyarkin
Makefile @potyarkin

View file

@ -52,12 +52,12 @@ class TestIngoredClass: # noqa: allure-validator
### Options ### Options
The linter supports the following flags: The linter supports the following flags:
- `--plugins` - regex patterns that plugins must match (paths are compiled according to the template: `plugin_name / src / plugin_name`). - `--plugins` - regex patterns that plugins must match (paths are compiled according to the template: `plugin / src / plugin`).
- `--files` - paths to files with fixtures. - `--files` - paths to files with fixtures.
Configuration options can also be provided using special command line arguments, for example: Configuration options can also be provided using special command line arguments, for example:
```shell ```shell
allure-validator <tests folder> --plugins <plugin regex>, ... --files <path to file with fixtures>, ... allure-validator pytest_tests/ --plugins some[-_]plugin --files etc/workspace/some_plugin/src/some_plugin/fixtures.py
``` ```
## Work example ## Work example

View file

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "allure-validator" name = "allure-validator"
version = "1.1.1" version = "1.0.0"
description = "Linter for allure.title validation" description = "Linter for allure.title validation"
readme = "README.md" readme = "README.md"
authors = [{ name = "YADRO", email = "info@yadro.com" }] authors = [{ name = "YADRO", email = "info@yadro.com" }]
@ -21,7 +21,7 @@ dependencies = [
"pytest>=7.1.2", "pytest>=7.1.2",
"pytest-lazy-fixture>=0.6.3" "pytest-lazy-fixture>=0.6.3"
] ]
requires-python = ">=3.10" requires-python = "==3.10.*"
[project.optional-dependencies] [project.optional-dependencies]
dev = ["black", "pylint", "isort", "pre-commit"] dev = ["black", "pylint", "isort", "pre-commit"]

View file

@ -187,23 +187,16 @@ def fixtures(ast_fixtures: list[ast.FunctionDef], ast_hooks: list[ast.FunctionDe
if parse.decorator(node.value) != PYTEST_METAFUNC_PARAMETRIZE: if parse.decorator(node.value) != PYTEST_METAFUNC_PARAMETRIZE:
continue continue
if len(node.value.args) < 2: fixture_names = parse.param_names(node.value.args[0])
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: for name in fixture_names:
params = parse.metafunc_params(ast_params) # Dynamic fake fixture
# Dynamic fixture
fixture = Fixture( fixture = Fixture(
name, name,
path, path,
node.lineno, node.lineno,
node.col_offset, node.col_offset,
args=[], args=[],
params=params, params=["parametrized"],
) )
fixtures[fixture] = fixture fixtures[fixture] = fixture

View file

@ -3,10 +3,8 @@ REPORTER_TITLE = "reporter.title"
PYTEST_FIXTURE = "pytest.fixture" PYTEST_FIXTURE = "pytest.fixture"
PYTEST_PARAMETRIZE = "pytest.mark.parametrize" PYTEST_PARAMETRIZE = "pytest.mark.parametrize"
PYTEST_PARAM = "pytest.param"
PYTEST_GENERATE_TESTS = "pytest_generate_tests" PYTEST_GENERATE_TESTS = "pytest_generate_tests"
PYTEST_METAFUNC_PARAMETRIZE = "metafunc.parametrize" PYTEST_METAFUNC_PARAMETRIZE = "metafunc.parametrize"
PYTEST_FORCED_PARAMETRIZATION = ["forced", "parametrized", "fixture"]
ATTR_PATH = "__func_path" ATTR_PATH = "__func_path"
ATTR_CLASS_NAME = "__class_name" ATTR_CLASS_NAME = "__class_name"
@ -16,6 +14,5 @@ VALIDATE_ERROR = 1
DIRECTORY_ERROR = 2 DIRECTORY_ERROR = 2
PLUGIN_ERROR = 3 PLUGIN_ERROR = 3
FILE_ERROR = 4 FILE_ERROR = 4
SYNTAX_ERROR = 5
ALLURE_VALIDATOR_IGNORE = "noqa: allure-validator" ALLURE_VALIDATOR_IGNORE = "noqa: allure-validator"

View file

@ -1,6 +1,8 @@
import sys
from allure_validator import collect, validate from allure_validator import collect, validate
from allure_validator.arguments import args from allure_validator.arguments import args
from allure_validator.common import DIRECTORY_ERROR, FILE_ERROR, PLUGIN_ERROR, SYNTAX_ERROR, VALIDATE_DONE, VALIDATE_ERROR from allure_validator.common import DIRECTORY_ERROR, FILE_ERROR, PLUGIN_ERROR, VALIDATE_DONE, VALIDATE_ERROR
def main(): def main():
@ -16,31 +18,22 @@ def main():
paths.extend(files) paths.extend(files)
except NotADirectoryError as e: except NotADirectoryError as e:
print(e) print(e)
exit(DIRECTORY_ERROR) sys.exit(DIRECTORY_ERROR)
except ModuleNotFoundError as e: except ModuleNotFoundError as e:
print(e) print(e)
exit(PLUGIN_ERROR) sys.exit(PLUGIN_ERROR)
except FileNotFoundError as e: except FileNotFoundError as e:
print(e) print(e)
exit(FILE_ERROR) sys.exit(FILE_ERROR)
except SyntaxError as e:
print(e)
exit(SYNTAX_ERROR)
ast_items = collect.ast_pytest_items(paths) ast_items = collect.ast_pytest_items(paths)
fixtures = collect.fixtures(ast_items.fixtures, ast_items.hooks, plugins) files = collect.fixtures(ast_items.fixtures, ast_items.hooks, plugins)
tests = collect.tests(ast_items.tests, fixtures, plugins) tests = collect.tests(ast_items.tests, files, plugins)
validate_result = validate.titles(tests) validate_result = validate.titles(tests)
if validate_result.errors == 0: if validate_result.errors == 0:
print(f"Done🎉\nFiles analyzed: {len(paths)}\nTests analyzed: {len(tests)}") 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(): for title, tests in validate_result.not_unique.items():
print(f"NOT UNIQUE TITLE: {title}") print(f"NOT UNIQUE TITLE: {title}")
@ -56,9 +49,4 @@ def main():
params_str = " OR ".join(params) if isinstance(params, list) else params params_str = " OR ".join(params) if isinstance(params, list) else params
print(f"{test.link}: MISSING PARAMS: Parameters are missing from title: {params_str}") print(f"{test.link}: MISSING PARAMS: Parameters are missing from title: {params_str}")
if plugins: sys.exit(VALIDATE_ERROR)
patterns = ", ".join(args.plugins)
print(f"Found plugins with patterns: {patterns}")
print(*(f"-> {plugin}" for plugin in plugins), sep="\n")
exit(VALIDATE_ERROR)

View file

@ -2,15 +2,7 @@ import ast
import re import re
from pathlib import Path from pathlib import Path
from allure_validator.common import ( from allure_validator.common import ALLURE_TITLE, ALLURE_VALIDATOR_IGNORE, PYTEST_FIXTURE, PYTEST_PARAMETRIZE, REPORTER_TITLE
ALLURE_TITLE,
ALLURE_VALIDATOR_IGNORE,
PYTEST_FIXTURE,
PYTEST_FORCED_PARAMETRIZATION,
PYTEST_PARAM,
PYTEST_PARAMETRIZE,
REPORTER_TITLE,
)
from allure_validator.pytest_items import TestParams from allure_validator.pytest_items import TestParams
@ -138,67 +130,25 @@ def fixture_params(func: ast.FunctionDef) -> list[str]:
""" """
ast_params = [] ast_params = []
for deco in func.decorator_list: for deco in func.decorator_list:
if not isinstance(deco, ast.Call): if not isinstance(deco, ast.Call):
continue continue
for keyword in deco.keywords: for keyword in deco.keywords:
if keyword.arg == "params": if keyword.arg == "params":
if isinstance(keyword.value, ast.List | ast.Tuple): ast_params.extend(keyword.value.elts)
ast_params.extend(keyword.value.elts)
else:
return PYTEST_FORCED_PARAMETRIZATION
params = [] params = []
for ast_param in ast_params: for ast_param in ast_params:
if isinstance(ast_param, ast.Call) and decorator(ast_param) == PYTEST_PARAM: for arg in ast_param.args:
params.extend(ast.unparse(arg) for arg in ast_param.args) params.append(ast.unparse(arg))
continue
if not isinstance(ast_param, ast.Starred):
params.append(ast.unparse(ast_param))
continue
return PYTEST_FORCED_PARAMETRIZATION
return params return params
def param_names(ast_names: ast.Constant | ast.List | ast.Tuple) -> list[str]: def param_names(ast_names: ast.Constant | ast.List) -> list[str]:
"""Generates a list of parameter names from `pytest.parametrize`.""" """Generates a list of parameter names from `pytest.parametrize`."""
if isinstance(ast_names, ast.Constant): if isinstance(ast_names, ast.Constant):
return ast_names.value.replace(",", " ").split() return ast_names.value.replace(",", " ").split()
return [elt.value for elt in ast_names.elts] 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

View file

@ -39,7 +39,7 @@ class Fixture(Item):
@property @property
def parametrized(self) -> bool: def parametrized(self) -> bool:
return len(self.params) > 1 return bool(self.params)
@dataclass(eq=False) @dataclass(eq=False)

View file

@ -2,10 +2,6 @@ import allure
import pytest import pytest
def parametrize_fixture() -> list[int]:
return [1, 2, 3]
@pytest.fixture( @pytest.fixture(
scope="function", scope="function",
params=[ params=[
@ -38,12 +34,3 @@ def object_fixture(file_path):
) )
def parametrized_fixture(request: pytest.FixtureRequest): def parametrized_fixture(request: pytest.FixtureRequest):
return request.param 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

View file

@ -1,91 +1,18 @@
import allure import allure
import pytest 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 ============== # # ============== DYNAMIC FIXTURE ============== #
def pytest_generate_tests(metafunc: pytest.Metafunc): def pytest_generate_tests(metafunc: pytest.Metafunc):
# Required dynamic fixture if "runtime_fixture" not in metafunc.fixturenames:
if "required_runtime_fixture" in metafunc.fixturenames: return
metafunc.parametrize("required_runtime_fixture", [AwsCliClient, Boto3ClientWrapper, Clients.AttributeClient])
# Optional dynamic fixture 1 metafunc.parametrize("runtime_fixture", [1, 2, 3])
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 required runtime fixture") @allure.title("Test runtime fixture")
def test_required_runtime_fixture(required_runtime_fixture): def test_runtime_fixture(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 assert True
@ -190,11 +117,6 @@ def test_three_multiple_indirect(object_fixture: str):
assert True assert True
@allure.title("Test optional fixture")
def test_optional_fixture(optional_parametrized_fixture):
assert True
# ============== TESTS/FIXTURES IN CLASS ============== # # ============== TESTS/FIXTURES IN CLASS ============== #