forked from TrueCloudLab/frostfs-testlib
Adding code validation targets
Signed-off-by: Andrey Berezin <a.berezin@yadro.com>
This commit is contained in:
parent
62216293f8
commit
4896abcec3
11 changed files with 80 additions and 41 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
||||||
# ignore IDE files
|
# ignore IDE files
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
|
venv.*
|
||||||
|
|
||||||
# ignore temp files under any path
|
# ignore temp files under any path
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
|
@ -63,9 +63,9 @@ $ git checkout -b feature/123-something_awesome
|
||||||
```
|
```
|
||||||
|
|
||||||
### Test your changes
|
### Test your changes
|
||||||
Before submitting any changes to the library, please, make sure that all unit tests are passing. To run the tests, please, use the following command:
|
Before submitting any changes to the library, please, make sure that linter and all unit tests are passing. To run the tests, please, use the following command:
|
||||||
```shell
|
```shell
|
||||||
$ python -m unittest discover --start-directory tests
|
$ make validation
|
||||||
```
|
```
|
||||||
|
|
||||||
To enable tests that interact with SSH server, please, setup SSH server and set the following environment variables before running the tests:
|
To enable tests that interact with SSH server, please, setup SSH server and set the following environment variables before running the tests:
|
||||||
|
|
34
Makefile
34
Makefile
|
@ -3,6 +3,8 @@ PYTHON_VERSION := 3.10
|
||||||
VENV_DIR := venv.frostfs-testlib
|
VENV_DIR := venv.frostfs-testlib
|
||||||
|
|
||||||
current_dir := $(shell pwd)
|
current_dir := $(shell pwd)
|
||||||
|
DIRECTORIES := $(sort $(dir $(wildcard ../frostfs-testlib-plugin-*/ ../*-testcases/)))
|
||||||
|
FROM_VENV := . ${VENV_DIR}/bin/activate &&
|
||||||
|
|
||||||
venv: create requirements paths precommit
|
venv: create requirements paths precommit
|
||||||
@echo Ready
|
@echo Ready
|
||||||
|
@ -14,14 +16,34 @@ precommit:
|
||||||
paths:
|
paths:
|
||||||
@echo Append paths for project
|
@echo Append paths for project
|
||||||
@echo Virtual environment: ${VENV_DIR}
|
@echo Virtual environment: ${VENV_DIR}
|
||||||
@sudo rm -rf ${VENV_DIR}/lib/python${PYTHON_VERSION}/site-packages/_paths.pth
|
@rm -rf ${VENV_DIR}/lib/python${PYTHON_VERSION}/site-packages/_paths.pth
|
||||||
@sudo touch ${VENV_DIR}/lib/python${PYTHON_VERSION}/site-packages/_paths.pth
|
@touch ${VENV_DIR}/lib/python${PYTHON_VERSION}/site-packages/_paths.pth
|
||||||
@echo ${current_dir}/src/frostfs_testlib_frostfs_testlib | sudo tee ${VENV_DIR}/lib/python${PYTHON_VERSION}/site-packages/_paths.pth
|
@echo ${current_dir}/src/frostfs_testlib_frostfs_testlib | tee ${VENV_DIR}/lib/python${PYTHON_VERSION}/site-packages/_paths.pth
|
||||||
|
|
||||||
create:
|
create: ${VENV_DIR}
|
||||||
@echo Create virtual environment for
|
|
||||||
|
${VENV_DIR}:
|
||||||
|
@echo Create virtual environment ${VENV_DIR}
|
||||||
virtualenv --python=python${PYTHON_VERSION} --prompt=frostfs-testlib ${VENV_DIR}
|
virtualenv --python=python${PYTHON_VERSION} --prompt=frostfs-testlib ${VENV_DIR}
|
||||||
|
|
||||||
requirements:
|
requirements:
|
||||||
@echo Isntalling pip requirements
|
@echo Isntalling pip requirements
|
||||||
. ${VENV_DIR}/bin/activate && pip install -Ur requirements.txt
|
. ${VENV_DIR}/bin/activate && pip install -Ur requirements.txt
|
||||||
|
|
||||||
|
|
||||||
|
#### VALIDATION SECTION ####
|
||||||
|
lint: create requirements
|
||||||
|
${FROM_VENV} pylint --disable R,C,W ./src
|
||||||
|
|
||||||
|
unit_test:
|
||||||
|
@echo Starting unit tests
|
||||||
|
${FROM_VENV} python -m pytest tests
|
||||||
|
|
||||||
|
.PHONY: lint_dependent $(DIRECTORIES)
|
||||||
|
lint_dependent: $(DIRECTORIES)
|
||||||
|
|
||||||
|
$(DIRECTORIES):
|
||||||
|
@echo checking dependent repo $@
|
||||||
|
$(MAKE) validation -C $@
|
||||||
|
|
||||||
|
validation: lint unit_test lint_dependent
|
|
@ -64,3 +64,8 @@ push = false
|
||||||
[tool.bumpver.file_patterns]
|
[tool.bumpver.file_patterns]
|
||||||
"pyproject.toml" = ['current_version = "{version}"', 'version = "{version}"']
|
"pyproject.toml" = ['current_version = "{version}"', 'version = "{version}"']
|
||||||
"src/frostfs_testlib/__init__.py" = ["{version}"]
|
"src/frostfs_testlib/__init__.py" = ["{version}"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
filterwarnings = [
|
||||||
|
"ignore:Blowfish has been deprecated:cryptography.utils.CryptographyDeprecationWarning",
|
||||||
|
]
|
|
@ -17,6 +17,7 @@ black==22.8.0
|
||||||
bumpver==2022.1118
|
bumpver==2022.1118
|
||||||
isort==5.12.0
|
isort==5.12.0
|
||||||
pre-commit==2.20.0
|
pre-commit==2.20.0
|
||||||
|
pylint==2.17.4
|
||||||
|
|
||||||
# Packaging dependencies
|
# Packaging dependencies
|
||||||
build==0.8.0
|
build==0.8.0
|
||||||
|
|
|
@ -6,6 +6,7 @@ from docstring_parser.google import DEFAULT_SECTIONS, Section, SectionType
|
||||||
|
|
||||||
DEFAULT_SECTIONS.append(Section("Steps", "steps", SectionType.MULTIPLE))
|
DEFAULT_SECTIONS.append(Section("Steps", "steps", SectionType.MULTIPLE))
|
||||||
|
|
||||||
|
|
||||||
class TestCase:
|
class TestCase:
|
||||||
"""
|
"""
|
||||||
Test case object implementation for use in collector and exporters
|
Test case object implementation for use in collector and exporters
|
||||||
|
@ -106,7 +107,9 @@ class TestCaseCollector:
|
||||||
# Read test_case suite and section name from test class if possible and get test function from class
|
# Read test_case suite and section name from test class if possible and get test function from class
|
||||||
if test.cls:
|
if test.cls:
|
||||||
suite_name = test.cls.__dict__.get("__test_case_suite_name__", suite_name)
|
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)
|
suite_section_name = test.cls.__dict__.get(
|
||||||
|
"__test_case_suite_section__", suite_section_name
|
||||||
|
)
|
||||||
test_function = test.cls.__dict__[test.originalname]
|
test_function = test.cls.__dict__[test.originalname]
|
||||||
else:
|
else:
|
||||||
# If no test class, read test function from module
|
# If no test class, read test function from module
|
||||||
|
@ -117,7 +120,9 @@ class TestCaseCollector:
|
||||||
test_case_title = test_function.__dict__.get("__test_case_title__", None)
|
test_case_title = test_function.__dict__.get("__test_case_title__", None)
|
||||||
test_case_priority = test_function.__dict__.get("__test_case_priority__", 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_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)
|
suite_section_name = test_function.__dict__.get(
|
||||||
|
"__test_case_suite_section__", suite_section_name
|
||||||
|
)
|
||||||
|
|
||||||
# Parce test_steps if they define in __doc__
|
# Parce test_steps if they define in __doc__
|
||||||
doc_string = parse(test_function.__doc__, style=DocstringStyle.GOOGLE)
|
doc_string = parse(test_function.__doc__, style=DocstringStyle.GOOGLE)
|
||||||
|
@ -125,7 +130,9 @@ class TestCaseCollector:
|
||||||
if doc_string.short_description:
|
if doc_string.short_description:
|
||||||
test_case_description = doc_string.short_description
|
test_case_description = doc_string.short_description
|
||||||
if doc_string.long_description:
|
if doc_string.long_description:
|
||||||
test_case_description = f"{doc_string.short_description}\r\n{doc_string.long_description}"
|
test_case_description = (
|
||||||
|
f"{doc_string.short_description}\r\n{doc_string.long_description}"
|
||||||
|
)
|
||||||
|
|
||||||
if doc_string.meta:
|
if doc_string.meta:
|
||||||
for meta in doc_string.meta:
|
for meta in doc_string.meta:
|
||||||
|
@ -140,25 +147,27 @@ class TestCaseCollector:
|
||||||
test_case_params = test_case_call_spec.id
|
test_case_params = test_case_call_spec.id
|
||||||
# Format title with params
|
# Format title with params
|
||||||
if test_case_title:
|
if test_case_title:
|
||||||
test_case_title = self.__format_string_with_params__(test_case_title,test_case_call_spec.params)
|
test_case_title = self.__format_string_with_params__(
|
||||||
|
test_case_title, test_case_call_spec.params
|
||||||
|
)
|
||||||
# Format steps with params
|
# Format steps with params
|
||||||
if test_case_steps:
|
if test_case_steps:
|
||||||
for key, value in test_case_steps.items():
|
for key, value in test_case_steps.items():
|
||||||
value = self.__format_string_with_params__(value,test_case_call_spec.params)
|
value = self.__format_string_with_params__(value, test_case_call_spec.params)
|
||||||
test_case_steps[key] = value
|
test_case_steps[key] = value
|
||||||
|
|
||||||
# If there is set basic test case attributes create TestCase and return
|
# 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:
|
if test_case_id and test_case_title and suite_name and suite_name:
|
||||||
test_case = TestCase(
|
test_case = TestCase(
|
||||||
id=test_case_id,
|
uuid_id=test_case_id,
|
||||||
title=test_case_title,
|
title=test_case_title,
|
||||||
description=test_case_description,
|
description=test_case_description,
|
||||||
priority=test_case_priority,
|
priority=test_case_priority,
|
||||||
steps=test_case_steps,
|
steps=test_case_steps,
|
||||||
params=test_case_params,
|
params=test_case_params,
|
||||||
suite_name=suite_name,
|
suite_name=suite_name,
|
||||||
suite_section_name=suite_section_name,
|
suite_section_name=suite_section_name,
|
||||||
)
|
)
|
||||||
return test_case
|
return test_case
|
||||||
# Return None if there is no enough information for return test case
|
# Return None if there is no enough information for return test case
|
||||||
return None
|
return None
|
||||||
|
@ -187,4 +196,4 @@ class TestCaseCollector:
|
||||||
test_case = self.__get_test_case_from_pytest_test__(test)
|
test_case = self.__get_test_case_from_pytest_test__(test)
|
||||||
if test_case:
|
if test_case:
|
||||||
test_cases.append(test_case)
|
test_cases.append(test_case)
|
||||||
return test_cases
|
return test_cases
|
||||||
|
|
|
@ -67,6 +67,6 @@ class TestExporter(ABC):
|
||||||
steps = [{"content": value, "expected": " "} for key, value in test_case.steps.items()]
|
steps = [{"content": value, "expected": " "} for key, value in test_case.steps.items()]
|
||||||
|
|
||||||
if test_case_in_tms:
|
if test_case_in_tms:
|
||||||
self.update_test_case(test_case, test_case_in_tms)
|
self.update_test_case(test_case, test_case_in_tms, test_suite, test_section)
|
||||||
else:
|
else:
|
||||||
self.create_test_case(test_case)
|
self.create_test_case(test_case, test_suite, test_section)
|
||||||
|
|
|
@ -135,13 +135,19 @@ class DockerHost(Host):
|
||||||
timeout=service_attributes.start_timeout,
|
timeout=service_attributes.start_timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
def wait_for_service_to_be_in_state(self, systemd_service_name: str, expected_state: str, timeout: int) -> None:
|
def wait_for_service_to_be_in_state(
|
||||||
|
self, systemd_service_name: str, expected_state: str, timeout: int
|
||||||
|
) -> None:
|
||||||
raise NotImplementedError("Not implemented for docker")
|
raise NotImplementedError("Not implemented for docker")
|
||||||
|
|
||||||
|
|
||||||
def get_data_directory(self, service_name: str) -> str:
|
def get_data_directory(self, service_name: str) -> str:
|
||||||
service_attributes = self._get_service_attributes(service_name)
|
service_attributes = self._get_service_attributes(service_name)
|
||||||
return service_attributes.data_directory_path
|
|
||||||
|
client = self._get_docker_client()
|
||||||
|
volume_info = client.inspect_volume(service_attributes.volume_name)
|
||||||
|
volume_path = volume_info["Mountpoint"]
|
||||||
|
|
||||||
|
return volume_path
|
||||||
|
|
||||||
def delete_metabase(self, service_name: str) -> None:
|
def delete_metabase(self, service_name: str) -> None:
|
||||||
raise NotImplementedError("Not implemented for docker")
|
raise NotImplementedError("Not implemented for docker")
|
||||||
|
@ -159,11 +165,7 @@ class DockerHost(Host):
|
||||||
raise NotImplementedError("Not implemented for docker")
|
raise NotImplementedError("Not implemented for docker")
|
||||||
|
|
||||||
def delete_storage_node_data(self, service_name: str, cache_only: bool = False) -> None:
|
def delete_storage_node_data(self, service_name: str, cache_only: bool = False) -> None:
|
||||||
service_attributes = self._get_service_attributes(service_name)
|
volume_path = self.get_data_directory(service_name)
|
||||||
|
|
||||||
client = self._get_docker_client()
|
|
||||||
volume_info = client.inspect_volume(service_attributes.volume_name)
|
|
||||||
volume_path = volume_info["Mountpoint"]
|
|
||||||
|
|
||||||
shell = self.get_shell()
|
shell = self.get_shell()
|
||||||
meta_clean_cmd = f"rm -rf {volume_path}/meta*/*"
|
meta_clean_cmd = f"rm -rf {volume_path}/meta*/*"
|
||||||
|
|
|
@ -68,9 +68,7 @@ def _cmd_run(cmd: str, timeout: int = 90) -> str:
|
||||||
end_time = datetime.now()
|
end_time = datetime.now()
|
||||||
_attach_allure_log(cmd, cmd_output, return_code, start_time, end_time)
|
_attach_allure_log(cmd, cmd_output, return_code, start_time, end_time)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Command: {cmd}\n"
|
f"Command: {cmd}\n" f"Error:\nreturn code: {return_code}\n" f"Output: {cmd_output}"
|
||||||
f"Error:\nreturn code: {return_code}\n"
|
|
||||||
f"Output: {exc.output.decode('utf-8') if type(exc.output) is bytes else exc.output}"
|
|
||||||
)
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
5
tests/conftest.py
Normal file
5
tests/conftest.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
app_dir = os.path.join(os.getcwd(), "src")
|
||||||
|
sys.path.insert(0, app_dir)
|
|
@ -14,11 +14,7 @@ def format_error_details(error: Exception) -> str:
|
||||||
Returns:
|
Returns:
|
||||||
String containing exception details.
|
String containing exception details.
|
||||||
"""
|
"""
|
||||||
detail_lines = traceback.format_exception(
|
detail_lines = traceback.format_exception(error)
|
||||||
etype=type(error),
|
|
||||||
value=error,
|
|
||||||
tb=error.__traceback__,
|
|
||||||
)
|
|
||||||
return "".join(detail_lines)
|
return "".join(detail_lines)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue