Adding code validation targets

Signed-off-by: Andrey Berezin <a.berezin@yadro.com>
This commit is contained in:
Andrey Berezin 2023-07-18 20:38:37 +03:00
parent 62216293f8
commit 4896abcec3
11 changed files with 80 additions and 41 deletions

1
.gitignore vendored
View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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",
]

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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*/*"

View file

@ -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
View file

@ -0,0 +1,5 @@
import os
import sys
app_dir = os.path.join(os.getcwd(), "src")
sys.path.insert(0, app_dir)

View file

@ -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)