forked from TrueCloudLab/frostfs-testlib
199 lines
7.7 KiB
Python
199 lines
7.7 KiB
Python
import re
|
|
|
|
from docstring_parser import parse
|
|
from docstring_parser.common import DocstringStyle
|
|
from docstring_parser.google import DEFAULT_SECTIONS, Section, SectionType
|
|
|
|
DEFAULT_SECTIONS.append(Section("Steps", "steps", SectionType.MULTIPLE))
|
|
|
|
|
|
class TestCase:
|
|
"""
|
|
Test case object implementation for use in collector and exporters
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
uuid_id: str,
|
|
title: str,
|
|
description: str,
|
|
priority: int,
|
|
steps: dict,
|
|
params: str,
|
|
suite_name: str,
|
|
suite_section_name: str,
|
|
):
|
|
"""
|
|
Base constructor for TestCase object
|
|
|
|
Args:
|
|
uuid_id: uuid from id decorator
|
|
title: test case title from title decorator
|
|
priority: test case priority value (0-3)
|
|
steps: list of test case steps read from function __doc__
|
|
params: string with test case param read from pytest Function(test) object
|
|
suite_name: test case suite name from test_suite decorator
|
|
suite_section_name: test case suite section from test_suite_section decorator
|
|
"""
|
|
|
|
# It can confuse, but we rewrite id to "id [params]" string
|
|
# We do it in case that one functions can return a lot of tests if we use test params
|
|
if params:
|
|
self.id = f"{uuid_id} [{params}]"
|
|
else:
|
|
self.id: str = uuid_id
|
|
self.title: str = title
|
|
self.description: str = description
|
|
self.priority: int = priority
|
|
self.steps: dict = steps
|
|
self.params: str = params
|
|
self.suite_name: str = suite_name
|
|
self.suite_section_name: str = suite_section_name
|
|
|
|
|
|
class TestCaseCollector:
|
|
"""
|
|
Collector working like a plugin for pytest and can be used in collect-only call to get tests list from pytest
|
|
Additionally, we have several function to filter tests that can be exported.
|
|
"""
|
|
|
|
pytest_tests = []
|
|
|
|
def __format_string_with_params__(self, source_string: str, test_params: dict) -> str:
|
|
"""
|
|
Helper function for format test case string arguments using test params.
|
|
Params name can be deep like a.b.c, so we will get the value from tests params.
|
|
Additionally, we check is the next object dict or real object to use right call for get next argument.
|
|
|
|
Args:
|
|
source_string: string for format by using test params (if needed)
|
|
test_params: dictionary with test params got from pytest test object
|
|
Returns:
|
|
(str): formatted string with replaced params name by params value
|
|
"""
|
|
|
|
target_string: str = source_string
|
|
for match in re.findall(r"\{(.*?)}", source_string):
|
|
nestings_attrs = match.split(".")
|
|
param = None
|
|
for nesting_attr in nestings_attrs:
|
|
if not param:
|
|
param = test_params.get(nesting_attr)
|
|
else:
|
|
if isinstance(param, dict):
|
|
param = param.get(nesting_attr)
|
|
else:
|
|
param = getattr(param, nesting_attr)
|
|
target_string = target_string.replace(f"{{{match}}}", str(param))
|
|
return target_string
|
|
|
|
def __get_test_case_from_pytest_test__(self, test) -> TestCase:
|
|
"""
|
|
Parce test meta and return test case if there is enough information for that.
|
|
|
|
Args:
|
|
test: pytest Function object
|
|
Returns:
|
|
(TestCase): return tests cases if there is enough information for that and None if not
|
|
"""
|
|
|
|
# Default values for use behind
|
|
suite_name: str = None
|
|
suite_section_name: str = None
|
|
test_case_steps = dict()
|
|
test_case_params: str = ""
|
|
test_case_description: str = ""
|
|
|
|
# Read test_case suite and section name from test class if possible and get test function from class
|
|
if test.cls:
|
|
suite_name = test.cls.__dict__.get("__test_case_suite_name__", suite_name)
|
|
suite_section_name = test.cls.__dict__.get(
|
|
"__test_case_suite_section__", suite_section_name
|
|
)
|
|
test_function = test.cls.__dict__[test.originalname]
|
|
else:
|
|
# If no test class, read test function from module
|
|
test_function = test.module.__dict__[test.originalname]
|
|
|
|
# Read base values from test function arguments
|
|
test_case_id = test_function.__dict__.get("__test_case_id__", None)
|
|
test_case_title = test_function.__dict__.get("__test_case_title__", None)
|
|
test_case_priority = test_function.__dict__.get("__test_case_priority__", None)
|
|
suite_name = test_function.__dict__.get("__test_case_suite_name__", suite_name)
|
|
suite_section_name = test_function.__dict__.get(
|
|
"__test_case_suite_section__", suite_section_name
|
|
)
|
|
|
|
# Parce test_steps if they define in __doc__
|
|
doc_string = parse(test_function.__doc__, style=DocstringStyle.GOOGLE)
|
|
|
|
if doc_string.short_description:
|
|
test_case_description = doc_string.short_description
|
|
if doc_string.long_description:
|
|
test_case_description = (
|
|
f"{doc_string.short_description}\r\n{doc_string.long_description}"
|
|
)
|
|
|
|
if doc_string.meta:
|
|
for meta in doc_string.meta:
|
|
if meta.args[0] == "steps":
|
|
test_case_steps[meta.args[1]] = meta.description
|
|
|
|
# Read params from tests function if its exist
|
|
test_case_call_spec = getattr(test, "callspec", "")
|
|
|
|
if test_case_call_spec:
|
|
# Set test cases params string value
|
|
test_case_params = test_case_call_spec.id
|
|
# Format title with params
|
|
if test_case_title:
|
|
test_case_title = self.__format_string_with_params__(
|
|
test_case_title, test_case_call_spec.params
|
|
)
|
|
# Format steps with params
|
|
if test_case_steps:
|
|
for key, value in test_case_steps.items():
|
|
value = self.__format_string_with_params__(value, test_case_call_spec.params)
|
|
test_case_steps[key] = value
|
|
|
|
# If there is set basic test case attributes create TestCase and return
|
|
if test_case_id and test_case_title and suite_name and suite_name:
|
|
test_case = TestCase(
|
|
uuid_id=test_case_id,
|
|
title=test_case_title,
|
|
description=test_case_description,
|
|
priority=test_case_priority,
|
|
steps=test_case_steps,
|
|
params=test_case_params,
|
|
suite_name=suite_name,
|
|
suite_section_name=suite_section_name,
|
|
)
|
|
return test_case
|
|
# Return None if there is no enough information for return test case
|
|
return None
|
|
|
|
def pytest_report_collectionfinish(self, pytest_tests: list) -> None:
|
|
"""
|
|
!!! DO NOT CHANGE THE NANE IT IS NOT A MISTAKE
|
|
Implement specific function with specific name
|
|
Pytest will be call this function when he uses plugin in collect-only call
|
|
|
|
Args:
|
|
pytest_tests: list of pytest tests
|
|
"""
|
|
self.pytest_tests.extend(pytest_tests)
|
|
|
|
def collect_test_cases(self) -> list[TestCase]:
|
|
"""
|
|
We're collecting test cases from the pytest tests list and return them in test case representation.
|
|
|
|
Returns:
|
|
(list[TestCase]): list of test cases that we found in the pytest tests code
|
|
"""
|
|
test_cases = []
|
|
|
|
for test in self.pytest_tests:
|
|
test_case = self.__get_test_case_from_pytest_test__(test)
|
|
if test_case:
|
|
test_cases.append(test_case)
|
|
return test_cases
|