forked from TrueCloudLab/allure-validator
Compare commits
1 commit
Author | SHA1 | Date | |
---|---|---|---|
d050a94272 |
9 changed files with 191 additions and 28 deletions
|
@ -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 <tests folder> --plugins <plugin regex>, ... --files <path to file with fixtures>, ...
|
||||
```
|
||||
|
||||
## Work example
|
||||
|
|
|
@ -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"]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -39,7 +39,7 @@ class Fixture(Item):
|
|||
|
||||
@property
|
||||
def parametrized(self) -> bool:
|
||||
return bool(self.params)
|
||||
return len(self.params) > 1
|
||||
|
||||
|
||||
@dataclass(eq=False)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ============== #
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue