diff --git a/.flake8 b/.flake8 deleted file mode 100644 index af25e839..00000000 --- a/.flake8 +++ /dev/null @@ -1,10 +0,0 @@ -[flake8] -max-line-length = 120 -import-order-style = google -inline-quotes = " -exclude = - ./tests/allure_behave/acceptance/**/test-data/** - ./tests/allure_behave/acceptance/behave_support/background/background_steps.py -per-file-ignores = - ./allure-python-commons/src/allure_commons/types.py:A005 - ./allure-robotframework/src/listener/types.py:A005 diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 637be25f..8e4d4323 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -5,6 +5,9 @@ on: branches: - master +permissions: + contents: read + jobs: pytest-changes: name: Collect allure-pytest file changes @@ -85,7 +88,7 @@ jobs: run: pip install -r ./requirements/linting.txt - name: Linting the codebase - run: poe linter + run: poe linter --output-format=github test-pytest: name: Test allure-pytest @@ -95,7 +98,13 @@ jobs: strategy: matrix: python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] - pytest-version: ["7.*", "8.*"] + pytest-version: ["7.*", "8.*", "9.*"] + exclude: + # pytest 9 supports Python >= 3.10 + - python-version: "3.8" + pytest-version: "9.*" + - python-version: "3.9" + pytest-version: "9.*" env: TEST_TMP: /tmp ALLURE_INDENT_OUTPUT: yep diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 2d6b8ccb..f46957a0 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -5,10 +5,16 @@ on: branches: - master +permissions: + contents: read + jobs: update_draft_release: runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write steps: - uses: toolmantim/release-drafter@v5.2.0 env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 10b3295f..835966a1 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -5,6 +5,9 @@ on: release: types: [published] +permissions: + contents: read + jobs: deploy: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 25e4deb0..7b7033cb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ .tox .pytest_cache .python-version +.ruff_cache .venv *.pyc diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 7b6fd0ee..00000000 --- a/Jenkinsfile +++ /dev/null @@ -1,17 +0,0 @@ -pipeline { - agent { docker 'randomknowledge/docker-pyenv-tox' } - environment { - HOME = pwd() - } - stages { - stage("Build") { - steps { - sh 'tox -r --workdir=/tmp -c allure-python-commons-test/tox.ini' - sh 'tox -r --workdir=/tmp -c allure-python-commons/tox.ini' - sh 'tox -r --workdir=/tmp -c allure-pytest/tox.ini' - sh 'tox -r --workdir=/tmp -c allure-behave/tox.ini' - sh 'tox -r --workdir=/tmp -c allure-robotframework/tox.ini' - } - } - } -} diff --git a/allure-behave/pyproject.toml b/allure-behave/pyproject.toml index 388f9913..442ac2d8 100644 --- a/allure-behave/pyproject.toml +++ b/allure-behave/pyproject.toml @@ -1,5 +1,5 @@ [tool.poe.tasks] -linter = "flake8 --extend-ignore=A003 ./src" +linter = "ruff check" [tool.poe.tasks.tests] cmd = "pytest ../tests/allure_behave" diff --git a/allure-behave/setup.py b/allure-behave/setup.py index 9bb3dc41..2fd05e97 100644 --- a/allure-behave/setup.py +++ b/allure-behave/setup.py @@ -4,24 +4,24 @@ PACKAGE = "allure-behave" classifiers = [ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Topic :: Software Development :: Quality Assurance', - 'Topic :: Software Development :: Testing', - 'Topic :: Software Development :: Testing :: BDD', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Software Development :: Testing", + "Topic :: Software Development :: Testing :: BDD", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] setup_requires = [ - "setuptools_scm" + "setuptools_scm<10" ] install_requires = [ @@ -64,5 +64,5 @@ def main(): install_requires=install_requires ) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/allure-behave/src/formatter.py b/allure-behave/src/formatter.py index febd2d96..fd5576f1 100644 --- a/allure-behave/src/formatter.py +++ b/allure-behave/src/formatter.py @@ -24,7 +24,7 @@ def _wrap_scenario(self, scenarios): if isinstance(scenario, ScenarioOutline): self._wrap_scenario(scenario) else: - scenario.run = allure_commons.test(scenario.run, context={'scenario': scenario}) + scenario.run = allure_commons.test(scenario.run, context={"scenario": scenario}) is_planned_scenario(scenario, self.testplan) def uri(self, uri): diff --git a/allure-behave/src/hooks.py b/allure-behave/src/hooks.py index 7a153020..0873d94a 100644 --- a/allure-behave/src/hooks.py +++ b/allure-behave/src/hooks.py @@ -45,7 +45,7 @@ def __init__(self, result_dir): self.listener = AllureListener(Configuration()) self.plugins = [] - if not hasattr(_storage, 'file_logger'): + if not hasattr(_storage, "file_logger"): logger = AllureFileLogger(result_dir) _storage.file_logger = logger allure_commons.plugin_manager.register(logger) diff --git a/allure-behave/src/listener.py b/allure-behave/src/listener.py index 7d5d8753..ad543526 100644 --- a/allure-behave/src/listener.py +++ b/allure-behave/src/listener.py @@ -2,6 +2,7 @@ import allure_commons from allure_commons.reporter import AllureReporter from allure_commons.utils import uuid4 +from allure_commons.utils import md5 from allure_commons.utils import now from allure_commons.utils import platform_label from allure_commons.types import LabelType, AttachmentType, LinkType @@ -28,9 +29,9 @@ class AllureListener: def __init__(self, behave_config): self.behave_config = behave_config - self.issue_pattern = behave_config.userdata.get('AllureFormatter.issue_pattern', None) - self.link_pattern = behave_config.userdata.get('AllureFormatter.link_pattern', None) - self.hide_excluded = behave_config.userdata.get('AllureFormatter.hide_excluded', False) + self.issue_pattern = behave_config.userdata.get("AllureFormatter.issue_pattern", None) + self.link_pattern = behave_config.userdata.get("AllureFormatter.link_pattern", None) + self.hide_excluded = behave_config.userdata.get("AllureFormatter.hide_excluded", False) self.logger = AllureReporter() self.current_step_uuid = None self.current_scenario_uuid = None @@ -69,7 +70,7 @@ def stop_feature(self): @allure_commons.hookimpl def start_test(self, parent_uuid, uuid, name, parameters, context): - self.start_scenario(context['scenario']) + self.start_scenario(context["scenario"]) def start_scenario(self, scenario): self.current_scenario_uuid = uuid4() @@ -80,7 +81,8 @@ def start_scenario(self, scenario): test_case.fullName = get_fullname(scenario) test_case.titlePath = get_title_path(scenario) test_case.historyId = scenario_history_id(scenario) - test_case.description = '\n'.join(scenario.description) + test_case.testCaseId = md5(test_case.fullName) + test_case.description = "\n".join(scenario.description) test_case.parameters = scenario_parameters(scenario) test_case.links.extend(scenario_links( @@ -89,20 +91,20 @@ def start_scenario(self, scenario): link_pattern=self.link_pattern)) test_case.labels.extend(scenario_labels(scenario)) test_case.labels.append(Label(name=LabelType.FEATURE, value=scenario.feature.name)) - test_case.labels.append(Label(name=LabelType.FRAMEWORK, value='behave')) + test_case.labels.append(Label(name=LabelType.FRAMEWORK, value="behave")) test_case.labels.append(Label(name=LabelType.LANGUAGE, value=platform_label())) self.logger.schedule_test(self.current_scenario_uuid, test_case) @allure_commons.hookimpl def stop_test(self, parent_uuid, uuid, name, context, exc_type, exc_val, exc_tb): - self.stop_scenario(context['scenario']) + self.stop_scenario(context["scenario"]) def stop_scenario(self, scenario): tag_expression = self.__get_tag_expression(self.behave_config) should_run = scenario.should_run_with_tags(tag_expression) should_run = should_run and scenario.should_run_with_name_select(self.behave_config) - should_drop_skipped_by_option = scenario.status == 'skipped' and not self.behave_config.show_skipped + should_drop_skipped_by_option = scenario.status == "skipped" and not self.behave_config.show_skipped should_drop_excluded = self.hide_excluded and (scenario.skip_reason == TEST_PLAN_SKIP_REASON or not should_run) if should_drop_skipped_by_option or should_drop_excluded: @@ -133,16 +135,16 @@ def match_step(self, match): def start_behave_step(self, step): self.current_step_uuid = uuid4() - name = f'{step.keyword} {step.name}' + name = f"{step.keyword} {step.name}" allure_step = TestStepResult(name=name, start=now()) self.logger.start_step(None, self.current_step_uuid, allure_step) if step.text: - self.logger.attach_data(uuid4(), step.text, name='.text', attachment_type=AttachmentType.TEXT) + self.logger.attach_data(uuid4(), step.text, name=".text", attachment_type=AttachmentType.TEXT) if step.table: - self.logger.attach_data(uuid4(), step_table(step), name='.table', attachment_type=AttachmentType.CSV) + self.logger.attach_data(uuid4(), step_table(step), name=".table", attachment_type=AttachmentType.CSV) def stop_behave_step(self, result): status = step_status(result) @@ -176,6 +178,18 @@ def attach_data(self, body, name, attachment_type, extension): def attach_file(self, source, name, attachment_type, extension): self.logger.attach_file(uuid4(), source, name=name, attachment_type=attachment_type, extension=extension) + @allure_commons.hookimpl + def global_attach_data(self, body, name, attachment_type, extension): + self.logger.global_attach_data(uuid4(), body, name=name, attachment_type=attachment_type, extension=extension) + + @allure_commons.hookimpl + def global_attach_file(self, source, name, attachment_type, extension): + self.logger.global_attach_file(uuid4(), source, name=name, attachment_type=attachment_type, extension=extension) + + @allure_commons.hookimpl + def global_error(self, message, trace): + self.logger.global_error(message=message, trace=trace) + @allure_commons.hookimpl def add_description(self, test_description): test_result = self.logger.get_test(None) @@ -192,7 +206,7 @@ def add_description_html(self, test_description_html): def add_link(self, url, link_type, name): test_result = self.logger.get_test(None) if test_result: - pattern = '{}' + pattern = "{}" if link_type == LinkType.ISSUE and self.issue_pattern: pattern = self.issue_pattern elif link_type == LinkType.LINK and self.link_pattern: @@ -228,7 +242,7 @@ def enter(self): self._logger.start_group(group.uuid, group) self._groups.append(group) - def exit(self): # noqa: A003 + def exit(self): group = self._groups.pop() if group.befores or group.afters: self._logger.stop_group(group.uuid) diff --git a/allure-behave/src/utils.py b/allure-behave/src/utils.py index ce0f2d70..da5cf073 100644 --- a/allure-behave/src/utils.py +++ b/allure-behave/src/utils.py @@ -14,11 +14,11 @@ TEST_PLAN_SKIP_REASON = "Not in allure test plan" STATUS = { - 'passed': Status.PASSED, - 'failed': Status.FAILED, - 'skipped': Status.SKIPPED, - 'untested': Status.SKIPPED, - 'undefined': Status.BROKEN + "passed": Status.PASSED, + "failed": Status.FAILED, + "skipped": Status.SKIPPED, + "untested": Status.SKIPPED, + "undefined": Status.BROKEN } @@ -30,7 +30,7 @@ def scenario_history_id(scenario): parts = [scenario.feature.name, scenario.name] if scenario._row: row = scenario._row - parts.extend([f'{name}={value}' for name, value in zip(row.headings, row.cells)]) + parts.extend([f"{name}={value}" for name, value in zip(row.headings, row.cells)]) return md5(*parts) @@ -57,14 +57,14 @@ def scenario_labels(scenario): def scenario_status(scenario): for step in scenario.all_steps: - if step_status(step) != 'passed': + if step_status(step) != "passed": return step_status(step) return Status.PASSED def scenario_status_details(scenario): for step in scenario.all_steps: - if step_status(step) != 'passed': + if step_status(step) != "passed": return step_status_details(step) @@ -151,8 +151,8 @@ def step_status_details(result): trace=trace ) - elif result.status == 'undefined': - message = '\nYou can implement step definitions for undefined steps with these snippets:\n\n' + elif result.status == "undefined": + message = "\nYou can implement step definitions for undefined steps with these snippets:\n\n" message += make_undefined_step_snippet(result) return StatusDetails(message=message) diff --git a/allure-nose2/pyproject.toml b/allure-nose2/pyproject.toml index 822f5d98..a411cd33 100644 --- a/allure-nose2/pyproject.toml +++ b/allure-nose2/pyproject.toml @@ -1,5 +1,5 @@ [tool.poe.tasks] -linter = "flake8 ./src" +linter = "ruff check" [tool.poe.tasks.tests] cmd = "pytest ../tests/allure_nose2" diff --git a/allure-nose2/setup.py b/allure-nose2/setup.py index 6f7a1ec5..11c4babc 100644 --- a/allure-nose2/setup.py +++ b/allure-nose2/setup.py @@ -4,23 +4,23 @@ PACKAGE = "allure-nose2" classifiers = [ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Topic :: Software Development :: Quality Assurance', - 'Topic :: Software Development :: Testing', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Software Development :: Testing", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] setup_requires = [ - "setuptools_scm" + "setuptools_scm<10" ] install_requires = [ @@ -63,5 +63,5 @@ def main(): ) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/allure-nose2/src/plugin.py b/allure-nose2/src/plugin.py index 678fe8f1..edd4d65d 100644 --- a/allure-nose2/src/plugin.py +++ b/allure-nose2/src/plugin.py @@ -44,7 +44,7 @@ def unregister(cls): class Allure(Plugin): - configSection = 'allure' + configSection = "allure" commandLineSwitch = (None, "allure", "Generate an Allure report") def __init__(self, *args, **kwargs): @@ -95,7 +95,7 @@ def startTest(self, event): test_result.labels.extend(labels(event.test)) test_result.labels.append(Label(name=LabelType.HOST, value=self._host)) test_result.labels.append(Label(name=LabelType.THREAD, value=self._thread)) - test_result.labels.append(Label(name=LabelType.FRAMEWORK, value='nose2')) + test_result.labels.append(Label(name=LabelType.FRAMEWORK, value="nose2")) test_result.labels.append(Label(name=LabelType.LANGUAGE, value=platform_label())) test_result.parameters = params(event) diff --git a/allure-nose2/src/utils.py b/allure-nose2/src/utils.py index 4e2e885d..23e83d23 100644 --- a/allure-nose2/src/utils.py +++ b/allure-nose2/src/utils.py @@ -7,9 +7,9 @@ # ToDo move to commons ALLURE_LABELS = [ - 'epic', - 'feature', - 'story', + "epic", + "feature", + "story", ] @@ -21,8 +21,8 @@ def status_details(event): message, trace = None, None if event.exc_info: exc_type, value, _ = event.exc_info - message = '\n'.join(format_exception_only(exc_type, value)) if exc_type or value else None - trace = ''.join(util.exc_info_to_string(event.exc_info, event.test)) + message = "\n".join(format_exception_only(exc_type, value)) if exc_type or value else None + trace = "".join(util.exc_info_to_string(event.exc_info, event.test)) elif event.reason: message = event.reason diff --git a/allure-pytest-bdd/pyproject.toml b/allure-pytest-bdd/pyproject.toml index 83277cf5..ec703429 100644 --- a/allure-pytest-bdd/pyproject.toml +++ b/allure-pytest-bdd/pyproject.toml @@ -1,5 +1,5 @@ [tool.poe.tasks] -linter = "flake8 ./src" +linter = "ruff check" [tool.poe.tasks.tests] cmd = "pytest ../tests/allure_pytest_bdd" diff --git a/allure-pytest-bdd/setup.py b/allure-pytest-bdd/setup.py index 4e651458..6f7553d2 100644 --- a/allure-pytest-bdd/setup.py +++ b/allure-pytest-bdd/setup.py @@ -4,25 +4,25 @@ PACKAGE = "allure-pytest-bdd" classifiers = [ - 'Development Status :: 5 - Production/Stable', - 'Framework :: Pytest', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Topic :: Software Development :: Quality Assurance', - 'Topic :: Software Development :: Testing', - 'Topic :: Software Development :: Testing :: BDD', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', + "Development Status :: 5 - Production/Stable", + "Framework :: Pytest", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Software Development :: Testing", + "Topic :: Software Development :: Testing :: BDD", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] setup_requires = [ - "setuptools_scm" + "setuptools_scm<10" ] install_requires = [ @@ -67,5 +67,5 @@ def main(): ) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/allure-pytest-bdd/src/allure_api_listener.py b/allure-pytest-bdd/src/allure_api_listener.py index e132e8e2..0b980725 100644 --- a/allure-pytest-bdd/src/allure_api_listener.py +++ b/allure-pytest-bdd/src/allure_api_listener.py @@ -16,6 +16,9 @@ from .utils import apply_link_pattern from .utils import attach_data from .utils import attach_file +from .utils import global_attach_data +from .utils import global_attach_file +from .utils import global_error from .utils import get_link_patterns from .steps import start_step from .steps import stop_step @@ -117,3 +120,15 @@ def attach_data(self, body, name, attachment_type, extension): @allure_commons.hookimpl def attach_file(self, source, name, attachment_type, extension): attach_file(self.lifecycle, source, name, attachment_type, extension) + + @allure_commons.hookimpl + def global_attach_data(self, body, name, attachment_type, extension): + global_attach_data(self.lifecycle, body, name, attachment_type, extension) + + @allure_commons.hookimpl + def global_attach_file(self, source, name, attachment_type, extension): + global_attach_file(self.lifecycle, source, name, attachment_type, extension) + + @allure_commons.hookimpl + def global_error(self, message, trace): + global_error(self.lifecycle, message, trace) diff --git a/allure-pytest-bdd/src/plugin.py b/allure-pytest-bdd/src/plugin.py index 521eadc4..0579c66e 100644 --- a/allure-pytest-bdd/src/plugin.py +++ b/allure-pytest-bdd/src/plugin.py @@ -15,20 +15,20 @@ def pytest_addoption(parser): - parser.getgroup("reporting").addoption('--alluredir', + parser.getgroup("reporting").addoption("--alluredir", action="store", dest="allure_report_dir", metavar="DIR", default=None, help="Generate Allure report in the specified directory (may not exist)") - parser.getgroup("reporting").addoption('--clean-alluredir', + parser.getgroup("reporting").addoption("--clean-alluredir", action="store_true", dest="clean_alluredir", help="Clean alluredir folder if it exists") def link_pattern(string): - pattern = string.split(':', 1) + pattern = string.split(":", 1) if not pattern[0]: raise argparse.ArgumentTypeError("A link type is mandatory") diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index bcc6cba0..f91e34ae 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -141,5 +141,5 @@ def pytest_runtest_makereport(self, item, call): attach_data(self.lifecycle, report.capstderr, "stderr", AttachmentType.TEXT, None) post_process_test_result(item, test_result) - if report.when == 'teardown': + if report.when == "teardown": self.lifecycle.write_test_case(uuid=uuid) diff --git a/allure-pytest-bdd/src/utils.py b/allure-pytest-bdd/src/utils.py index f4a838b1..43bc17a1 100644 --- a/allure-pytest-bdd/src/utils.py +++ b/allure-pytest-bdd/src/utils.py @@ -27,8 +27,8 @@ ALLURE_TITLE_ATTR = "__allure_display_name__" ALLURE_DESCRIPTION_MARK = "allure_description" ALLURE_DESCRIPTION_HTML_MARK = "allure_description_html" -ALLURE_LABEL_MARK = 'allure_label' -ALLURE_LINK_MARK = 'allure_link' +ALLURE_LABEL_MARK = "allure_label" +ALLURE_LINK_MARK = "allure_link" MARK_NAMES_TO_IGNORE = { "usefixtures", @@ -236,13 +236,13 @@ def get_scenario_status_details(report, excinfo): def get_outline_params(node): - if hasattr(node, 'callspec'): - return node.callspec.params.get('_pytest_bdd_example', {}) + if hasattr(node, "callspec"): + return node.callspec.params.get("_pytest_bdd_example", {}) return {} def get_pytest_params(node): - if hasattr(node, 'callspec'): + if hasattr(node, "callspec"): pytest_params = dict(node.callspec.params) if "_pytest_bdd_example" in pytest_params: del pytest_params["_pytest_bdd_example"] @@ -323,6 +323,30 @@ def attach_file(lifecycle, source, name, attachment_type, extension=None): ) +def global_attach_data(lifecycle, body, name, attachment_type, extension=None): + lifecycle.global_attach_data( + uuid4(), + body, + name=name, + attachment_type=attachment_type, + extension=extension, + ) + + +def global_attach_file(lifecycle, source, name, attachment_type, extension=None): + lifecycle.global_attach_file( + uuid4(), + source, + name=name, + attachment_type=attachment_type, + extension=extension, + ) + + +def global_error(lifecycle, message, trace=None): + lifecycle.global_error(message=message, trace=trace) + + def format_csv(rows): with io.StringIO() as buffer: writer = csv.writer(buffer) diff --git a/allure-pytest/pyproject.toml b/allure-pytest/pyproject.toml index 22458017..98187503 100644 --- a/allure-pytest/pyproject.toml +++ b/allure-pytest/pyproject.toml @@ -1,5 +1,5 @@ [tool.poe.tasks] -linter = "flake8 ./src" +linter = "ruff check" [tool.poe.tasks.tests] cmd = "pytest ../tests/allure_pytest" diff --git a/allure-pytest/setup.py b/allure-pytest/setup.py index 6597abf5..ce0016bf 100644 --- a/allure-pytest/setup.py +++ b/allure-pytest/setup.py @@ -1,39 +1,27 @@ -import os,sys +import os from setuptools import setup -from pkg_resources import require, DistributionNotFound, VersionConflict - -try: - require('pytest-allure-adaptor') - print(""" - You have pytest-allure-adaptor installed. - You need to remove pytest-allure-adaptor from your site-packages - before installing allure-pytest, or conflicts may result. - """) - sys.exit() -except (DistributionNotFound, VersionConflict): - pass PACKAGE = "allure-pytest" classifiers = [ - 'Development Status :: 5 - Production/Stable', - 'Framework :: Pytest', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Topic :: Software Development :: Quality Assurance', - 'Topic :: Software Development :: Testing', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', + "Development Status :: 5 - Production/Stable", + "Framework :: Pytest", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Software Development :: Testing", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] setup_requires = [ - "setuptools_scm" + "setuptools_scm<10" ] @@ -78,5 +66,5 @@ def main(): install_requires=install_requires ) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/allure-pytest/src/listener.py b/allure-pytest/src/listener.py index 42b7ff49..10ec29df 100644 --- a/allure-pytest/src/listener.py +++ b/allure-pytest/src/listener.py @@ -149,14 +149,14 @@ def pytest_runtest_teardown(self, item): self.__apply_default_suites(item, test_result) test_result.labels.append(Label(name=LabelType.HOST, value=self._host)) test_result.labels.append(Label(name=LabelType.THREAD, value=self._thread)) - test_result.labels.append(Label(name=LabelType.FRAMEWORK, value='pytest')) + test_result.labels.append(Label(name=LabelType.FRAMEWORK, value="pytest")) test_result.labels.append(Label(name=LabelType.LANGUAGE, value=platform_label())) - test_result.labels.append(Label(name='package', value=allure_package(item))) + test_result.labels.append(Label(name="package", value=allure_package(item))) test_result.links.extend([Link(link_type, url, name) for link_type, url, name in allure_links(item)]) @pytest.hookimpl(hookwrapper=True) def pytest_fixture_setup(self, fixturedef, request): - fixture_name = getattr(fixturedef.func, '__allure_display_name__', fixturedef.argname) + fixture_name = getattr(fixturedef.func, "__allure_display_name__", fixturedef.argname) container_uuid = self._cache.get(fixturedef) @@ -178,16 +178,16 @@ def pytest_fixture_setup(self, fixturedef, request): status=get_outcome_status(outcome), statusDetails=get_outcome_status_details(outcome)) - finalizers = getattr(fixturedef, '_finalizers', []) + finalizers = getattr(fixturedef, "_finalizers", []) for index, finalizer in enumerate(finalizers): finalizer_name = getattr(finalizer, "__name__", index) - name = f'{fixture_name}::{finalizer_name}' + name = f"{fixture_name}::{finalizer_name}" finalizers[index] = allure_commons.fixture(finalizer, parent_uuid=container_uuid, name=name) @pytest.hookimpl(hookwrapper=True) def pytest_fixture_post_finalizer(self, fixturedef): yield - if hasattr(fixturedef, 'cached_result') and self._cache.get(fixturedef): + if hasattr(fixturedef, "cached_result") and self._cache.get(fixturedef): container_uuid = self._cache.pop(fixturedef) self.allure_logger.stop_group(container_uuid, stop=now()) @@ -203,9 +203,9 @@ def pytest_runtest_makereport(self, item, call): if call.excinfo: message = call.excinfo.exconly() - if hasattr(report, 'wasxfail'): + if hasattr(report, "wasxfail"): reason = report.wasxfail - message = (f'XFAIL {reason}' if reason else 'XFAIL') + '\n\n' + message + message = (f"XFAIL {reason}" if reason else "XFAIL") + "\n\n" + message trace = report.longreprtext status_details = StatusDetails( message=message, @@ -215,21 +215,21 @@ def pytest_runtest_makereport(self, item, call): if (status != Status.SKIPPED and _exception_brokes_test(exception)): status = Status.BROKEN - if status == Status.PASSED and hasattr(report, 'wasxfail'): + if status == Status.PASSED and hasattr(report, "wasxfail"): reason = report.wasxfail - message = f'XPASS {reason}' if reason else 'XPASS' + message = f"XPASS {reason}" if reason else "XPASS" status_details = StatusDetails(message=message) - if report.when == 'setup': + if report.when == "setup": test_result.status = status test_result.statusDetails = status_details - if report.when == 'call': + if report.when == "call": if test_result.status == Status.PASSED: test_result.status = status test_result.statusDetails = status_details - if report.when == 'teardown': + if report.when == "teardown": if status in (Status.FAILED, Status.BROKEN) and test_result.status == Status.PASSED: test_result.status = status test_result.statusDetails = status_details @@ -257,6 +257,30 @@ def attach_data(self, body, name, attachment_type, extension): def attach_file(self, source, name, attachment_type, extension): self.allure_logger.attach_file(uuid4(), source, name=name, attachment_type=attachment_type, extension=extension) + @allure_commons.hookimpl + def global_attach_data(self, body, name, attachment_type, extension): + self.allure_logger.global_attach_data( + uuid4(), + body, + name=name, + attachment_type=attachment_type, + extension=extension, + ) + + @allure_commons.hookimpl + def global_attach_file(self, source, name, attachment_type, extension): + self.allure_logger.global_attach_file( + uuid4(), + source, + name=name, + attachment_type=attachment_type, + extension=extension, + ) + + @allure_commons.hookimpl + def global_error(self, message, trace): + self.allure_logger.global_error(message=message, trace=trace) + @allure_commons.hookimpl def add_title(self, test_title): test_result = self.allure_logger.get_test(None) @@ -310,11 +334,11 @@ def add_parameter(self, name, value, excluded, mode: ParameterMode): @staticmethod def __get_pytest_params(item): - return item.callspec.params if hasattr(item, 'callspec') else {} + return item.callspec.params if hasattr(item, "callspec") else {} @staticmethod def __get_pytest_param_id(item): - return item.callspec.id if hasattr(item, 'callspec') else None + return item.callspec.id if hasattr(item, "callspec") else None def __apply_default_suites(self, item, test_result): default_suites = allure_suite_labels(item) diff --git a/allure-pytest/src/plugin.py b/allure-pytest/src/plugin.py index 2771722f..9efc358b 100644 --- a/allure-pytest/src/plugin.py +++ b/allure-pytest/src/plugin.py @@ -17,24 +17,24 @@ def pytest_addoption(parser): - parser.getgroup("reporting").addoption('--alluredir', + parser.getgroup("reporting").addoption("--alluredir", action="store", dest="allure_report_dir", metavar="DIR", default=None, help="Generate Allure report in the specified directory (may not exist)") - parser.getgroup("reporting").addoption('--clean-alluredir', + parser.getgroup("reporting").addoption("--clean-alluredir", action="store_true", dest="clean_alluredir", help="Clean alluredir folder if it exists") - parser.getgroup("reporting").addoption('--allure-no-capture', + parser.getgroup("reporting").addoption("--allure-no-capture", action="store_false", dest="attach_capture", help="Do not attach pytest captured logging/stdout/stderr to report") - parser.getgroup("reporting").addoption('--inversion', + parser.getgroup("reporting").addoption("--inversion", action="store", dest="inversion", default=False, @@ -42,21 +42,21 @@ def pytest_addoption(parser): def label_type(type_name, legal_values=set()): def a_label_type(string): - atoms = set(string.split(',')) + atoms = set(string.split(",")) if type_name is LabelType.SEVERITY: if not atoms <= legal_values: - raise argparse.ArgumentTypeError('Illegal {} values: {}, only [{}] are allowed'.format( + raise argparse.ArgumentTypeError("Illegal {} values: {}, only [{}] are allowed".format( type_name, - ', '.join(atoms - legal_values), - ', '.join(legal_values) + ", ".join(atoms - legal_values), + ", ".join(legal_values) )) return set((type_name, allure.severity_level(atom)) for atom in atoms) return set((type_name, atom) for atom in atoms) return a_label_type severities = [x.value for x in list(allure.severity_level)] - formatted_severities = ', '.join(severities) - parser.getgroup("general").addoption('--allure-severities', + formatted_severities = ", ".join(severities) + parser.getgroup("general").addoption("--allure-severities", action="store", dest="allure_severities", metavar="SEVERITIES_SET", @@ -66,7 +66,7 @@ def a_label_type(string): Tests only with these severities will be run. Possible values are: {formatted_severities}.""") - parser.getgroup("general").addoption('--allure-epics', + parser.getgroup("general").addoption("--allure-epics", action="store", dest="allure_epics", metavar="EPICS_SET", @@ -75,7 +75,7 @@ def a_label_type(string): help="""Comma-separated list of epic names. Run tests that have at least one of the specified feature labels.""") - parser.getgroup("general").addoption('--allure-features', + parser.getgroup("general").addoption("--allure-features", action="store", dest="allure_features", metavar="FEATURES_SET", @@ -84,7 +84,7 @@ def a_label_type(string): help="""Comma-separated list of feature names. Run tests that have at least one of the specified feature labels.""") - parser.getgroup("general").addoption('--allure-stories', + parser.getgroup("general").addoption("--allure-stories", action="store", dest="allure_stories", metavar="STORIES_SET", @@ -93,7 +93,7 @@ def a_label_type(string): help="""Comma-separated list of story names. Run tests that have at least one of the specified story labels.""") - parser.getgroup("general").addoption('--allure-ids', + parser.getgroup("general").addoption("--allure-ids", action="store", dest="allure_ids", metavar="IDS_SET", @@ -107,7 +107,7 @@ def cf_type(string): atoms = set(values.split(",")) return [(type_name, atom) for atom in atoms] - parser.getgroup("general").addoption('--allure-label', + parser.getgroup("general").addoption("--allure-label", action="append", dest="allure_labels", metavar="LABELS_SET", @@ -117,15 +117,15 @@ def cf_type(string): "Run tests that have at least one of the specified labels.""") def link_pattern(string): - pattern = string.split(':', 1) + pattern = string.split(":", 1) if not pattern[0]: - raise argparse.ArgumentTypeError('Link type is mandatory.') + raise argparse.ArgumentTypeError("Link type is mandatory.") if len(pattern) != 2: - raise argparse.ArgumentTypeError('Link pattern is mandatory') + raise argparse.ArgumentTypeError("Link pattern is mandatory") return pattern - parser.getgroup("general").addoption('--allure-link-pattern', + parser.getgroup("general").addoption("--allure-link-pattern", action="append", dest="allure_link_pattern", metavar="LINK_TYPE:LINK_PATTERN", @@ -160,7 +160,7 @@ def pytest_configure(config): if report_dir: report_dir = os.path.abspath(report_dir) test_listener = AllureListener(config) - config.pluginmanager.register(test_listener, 'allure_listener') + config.pluginmanager.register(test_listener, "allure_listener") allure_commons.plugin_manager.register(test_listener) config.add_cleanup(cleanup_factory(test_listener)) diff --git a/allure-pytest/src/stash.py b/allure-pytest/src/stash.py index 31d9302b..83e31b57 100644 --- a/allure-pytest/src/stash.py +++ b/allure-pytest/src/stash.py @@ -1,7 +1,7 @@ import pytest from functools import wraps -HAS_STASH = hasattr(pytest, 'StashKey') +HAS_STASH = hasattr(pytest, "StashKey") def create_stashkey_safe(): diff --git a/allure-pytest/src/utils.py b/allure-pytest/src/utils.py index 56594a09..31ffb63f 100644 --- a/allure-pytest/src/utils.py +++ b/allure-pytest/src/utils.py @@ -7,10 +7,10 @@ from allure_commons.types import LabelType from allure_pytest.stash import stashed -ALLURE_DESCRIPTION_MARK = 'allure_description' -ALLURE_DESCRIPTION_HTML_MARK = 'allure_description_html' -ALLURE_LABEL_MARK = 'allure_label' -ALLURE_LINK_MARK = 'allure_link' +ALLURE_DESCRIPTION_MARK = "allure_description" +ALLURE_DESCRIPTION_HTML_MARK = "allure_description_html" +ALLURE_LABEL_MARK = "allure_label" +ALLURE_LINK_MARK = "allure_link" ALLURE_UNIQUE_LABELS = [ LabelType.SEVERITY, LabelType.FRAMEWORK, @@ -34,11 +34,11 @@ class ParsedPytestNodeId: def __init__(self, nodeid): filepath, *class_names, function_segment = ensure_len(nodeid.split("::"), 2) self.filepath = filepath - self.path_segments = filepath.split('/') + self.path_segments = filepath.split("/") *parent_dirs, filename = ensure_len(self.path_segments, 1) - self.parent_package = '.'.join(parent_dirs) + self.parent_package = ".".join(parent_dirs) self.module = filename.rsplit(".", 1)[0] - self.package = '.'.join(filter(None, [self.parent_package, self.module])) + self.package = ".".join(filter(None, [self.parent_package, self.module])) self.class_names = class_names self.test_function = function_segment.split("[", 1)[0] @@ -65,7 +65,7 @@ def allure_description(item): description = get_marker_value(item, ALLURE_DESCRIPTION_MARK) if description: return description - elif hasattr(item, 'function'): + elif hasattr(item, "function"): return item.function.__doc__ @@ -103,7 +103,7 @@ def allure_links(item): def format_allure_link(config, url, link_type): - pattern = dict(config.option.allure_link_pattern).get(link_type, '{}') + pattern = dict(config.option.allure_link_pattern).get(link_type, "{}") return pattern.format(url) @@ -203,7 +203,7 @@ def get_status_details(exception_type, exception, exception_traceback): def get_pytest_report_status(pytest_report): - pytest_statuses = ('failed', 'passed', 'skipped') + pytest_statuses = ("failed", "passed", "skipped") statuses = (Status.FAILED, Status.PASSED, Status.SKIPPED) for pytest_status, status in zip(pytest_statuses, statuses): if getattr(pytest_report, pytest_status): diff --git a/allure-python-commons-test/pyproject.toml b/allure-python-commons-test/pyproject.toml index cc2b9ab5..a817d869 100644 --- a/allure-python-commons-test/pyproject.toml +++ b/allure-python-commons-test/pyproject.toml @@ -1,3 +1,3 @@ [tool.poe.tasks] -linter = "flake8 ./src" +linter = "ruff check" tests = "python -m doctest ./src/*.py" diff --git a/allure-python-commons-test/setup.py b/allure-python-commons-test/setup.py index bfcaddca..3d5eb5d7 100644 --- a/allure-python-commons-test/setup.py +++ b/allure-python-commons-test/setup.py @@ -4,19 +4,19 @@ PACKAGE = "allure-python-commons-test" classifiers = [ - 'Development Status :: 5 - Production/Stable', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Topic :: Software Development :: Quality Assurance', - 'Topic :: Software Development :: Testing', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Software Development :: Testing", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] install_requires = [ @@ -32,7 +32,7 @@ def main(): setup( name=PACKAGE, use_scm_version={"root": "..", "relative_to": __file__}, - setup_requires=['setuptools_scm'], + setup_requires=["setuptools_scm<10"], description=( "A collection of PyHamcrest matchers to test Allure adapters for " "Python test frameworks" @@ -53,5 +53,5 @@ def main(): install_requires=install_requires ) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/allure-python-commons-test/src/container.py b/allure-python-commons-test/src/container.py index 50aa1e6b..382c2502 100644 --- a/allure-python-commons-test/src/container.py +++ b/allure-python-commons-test/src/container.py @@ -11,16 +11,16 @@ def __init__(self, report, *matchers): def _matches(self, item): return has_property( - 'test_containers', + "test_containers", has_item( all_of( - has_entry('children', has_item(item['uuid'])), + has_entry("children", has_item(item["uuid"])), *self.matchers ) )).matches(self.report) def describe_to(self, description): - description.append_text('describe me later').append_list('[', ', ', ']', self.matchers) + description.append_text("describe me later").append_list("[", ", ", "]", self.matchers) def describe_mismatch(self, item, mismatch_description): self.matches(item, mismatch_description) @@ -84,14 +84,14 @@ def __init__(self, *args): @staticmethod def _test_case_id_by_name(report, test_case_name): for test_case in report.test_cases: - if test_case['fullName'].endswith(test_case_name): - return test_case['uuid'] + if test_case["fullName"].endswith(test_case_name): + return test_case["uuid"] def _matches(self, report): - return has_property('test_containers', + return has_property("test_containers", has_item( all_of( - has_entry('children', + has_entry("children", all_of( *[has_item(self._test_case_id_by_name(report, name)) for name in self.test_case_names] @@ -103,7 +103,7 @@ def _matches(self, report): # TODO better describe def describe_to(self, description): - description.append_text('test_case has group') + description.append_text("test_case has group") def has_same_container(*args): @@ -156,7 +156,7 @@ def has_fixture(section, name, *matchers): section, has_item( all_of( - has_entry('name', equal_to(name)), + has_entry("name", equal_to(name)), *matchers ) ) @@ -164,8 +164,8 @@ def has_fixture(section, name, *matchers): def has_before(name, *matchers): - return has_fixture('befores', name, *matchers) + return has_fixture("befores", name, *matchers) def has_after(name, *matchers): - return has_fixture('afters', name, *matchers) + return has_fixture("afters", name, *matchers) diff --git a/allure-python-commons-test/src/label.py b/allure-python-commons-test/src/label.py index 15d9e3d2..364d43a1 100644 --- a/allure-python-commons-test/src/label.py +++ b/allure-python-commons-test/src/label.py @@ -7,55 +7,55 @@ def has_label(name, value=None): if value is None: value = anything() return has_entry( - 'labels', + "labels", has_item( all_of( - has_entry('name', name), - has_entry('value', value) + has_entry("name", name), + has_entry("value", value) ) ) ) def has_severity(level): - return has_label('severity', level) + return has_label("severity", level) def has_epic(feature): - return has_label('epic', feature) + return has_label("epic", feature) def has_feature(feature): - return has_label('feature', feature) + return has_label("feature", feature) def has_story(story): - return has_label('story', story) + return has_label("story", story) def has_tag(tag): - return has_label('tag', tag) + return has_label("tag", tag) def has_package(package): - return has_label('package', package) + return has_label("package", package) def has_suite(suite): - return has_label('suite', suite) + return has_label("suite", suite) def has_parent_suite(parent_suite): - return has_label('parentSuite', parent_suite) + return has_label("parentSuite", parent_suite) def has_sub_suite(sub_suite): - return has_label('subSuite', sub_suite) + return has_label("subSuite", sub_suite) def has_allure_id(allure_id): - return has_label('as_id', allure_id) + return has_label("as_id", allure_id) def has_manual(allure_id): - return has_label('ALLURE_MANUAL', allure_id) + return has_label("ALLURE_MANUAL", allure_id) diff --git a/allure-python-commons-test/src/report.py b/allure-python-commons-test/src/report.py index 82036ea3..1db9413d 100644 --- a/allure-python-commons-test/src/report.py +++ b/allure-python-commons-test/src/report.py @@ -84,21 +84,27 @@ def __init__(self, result): self.test_cases = [ json.load(file) for _, file in self._report_items( result, - '*result.json' + "*result.json" ) ] self.test_containers = [ json.load(file) for _, file in self._report_items( result, - '*container.json' + "*container.json" ) ] self.attachments = { name: file.read() for name, file in self._report_items( result, - '*attachment.*' + "*attachment.*" ) } + self.globals = [ + json.load(file) for _, file in self._report_items( + result, + "*globals.json" + ) + ] @staticmethod def _report_items(report_dir, glob): @@ -111,12 +117,12 @@ def _report_items(report_dir, glob): def has_test_case(name, *matchers): return has_property( - 'test_cases', + "test_cases", has_item( all_of( any_of( - has_entry('fullName', ends_with(name)), - has_entry('name', starts_with(name)) + has_entry("fullName", ends_with(name)), + has_entry("name", starts_with(name)) ), *matchers ) @@ -129,7 +135,7 @@ def __init__(self, *matchers): self.matchers = matchers def _matches(self, item): - return has_property('test_cases', + return has_property("test_cases", only_contains(any_of(*self.matchers)) ).matches(item) @@ -160,19 +166,19 @@ def _matches(self, item): def describe_to(self, description): description.append_text( - f'exactly {self.num} item(s) matching ' + f"exactly {self.num} item(s) matching " ).append_text(self.matcher) def has_only_n_test_cases(name, num, *matchers): return has_property( - 'test_cases', + "test_cases", ContainsExactly( num, all_of( any_of( - has_entry('fullName', ends_with(name)), - has_entry('name', ends_with(name)) + has_entry("fullName", ends_with(name)), + has_entry("name", ends_with(name)) ), *matchers ) diff --git a/allure-python-commons-test/src/result.py b/allure-python-commons-test/src/result.py index 93a393cd..7012234f 100644 --- a/allure-python-commons-test/src/result.py +++ b/allure-python-commons-test/src/result.py @@ -71,7 +71,7 @@ def has_title(title): - return has_entry('name', title) + return has_entry("name", title) def has_title_path(*matchers): @@ -82,19 +82,19 @@ def has_title_path(*matchers): def has_description(*matchers): - return has_entry('description', all_of(*matchers)) + return has_entry("description", all_of(*matchers)) def has_description_html(*matchers): - return has_entry('descriptionHtml', all_of(*matchers)) + return has_entry("descriptionHtml", all_of(*matchers)) def has_step(name, *matchers): return has_entry( - 'steps', + "steps", has_item( all_of( - has_entry('name', equal_to(name)), + has_entry("name", equal_to(name)), *matchers ) ) @@ -110,10 +110,10 @@ def with_steps(*matchers): def get_parameter_matcher(name, *matchers): return has_entry( - 'parameters', + "parameters", has_item( all_of( - has_entry('name', equal_to(name)), + has_entry("name", equal_to(name)), *matchers ) ) @@ -123,7 +123,7 @@ def get_parameter_matcher(name, *matchers): def has_parameter(name, value, *matchers): return get_parameter_matcher( name, - has_entry('value', equal_to(value)), + has_entry("value", equal_to(value)), *matchers ) @@ -148,12 +148,12 @@ def resolve_link_attr_matcher(key, value): def has_link(url, link_type=None, name=None): return has_entry( - 'links', + "links", has_item( all_of( *[ resolve_link_attr_matcher(key, value) for key, value in zip( - ('url', 'type', 'name'), + ("url", "type", "name"), (url, link_type, name) ) if value is not None ] @@ -163,21 +163,21 @@ def has_link(url, link_type=None, name=None): def has_issue_link(url, name=None): - return has_link(url, link_type='issue', name=name) + return has_link(url, link_type="issue", name=name) def has_test_case_link(url, name=None): - return has_link(url, link_type='tms', name=name) + return has_link(url, link_type="tms", name=name) def has_attachment(attach_type=None, name=None): return has_entry( - 'attachments', + "attachments", has_item( all_of( - has_entry('source', anything()), - has_entry('type', attach_type) if attach_type else anything(), - has_entry('name', name) if name else anything() + has_entry("source", anything()), + has_entry("type", attach_type) if attach_type else anything(), + has_entry("name", name) if name else anything() ) ) ) @@ -190,47 +190,84 @@ def has_attachment_with_content( name=None ): return has_entry( - 'attachments', + "attachments", has_item( all_of( - has_entry('name', name) if name else anything(), - has_entry('type', attach_type) if attach_type else anything(), - has_entry('source', maps_to(attachments, content_matcher)) + has_entry("name", name) if name else anything(), + has_entry("type", attach_type) if attach_type else anything(), + has_entry("source", maps_to(attachments, content_matcher)) + ) + ) + ) + + +def has_global_attachment_with_content( + attachments, + content_matcher, + attach_type=None, + name=None +): + return has_entry( + "attachments", + has_item( + all_of( + has_entry("name", name) if name else anything(), + has_entry("type", attach_type) if attach_type else anything(), + has_entry("timestamp", not_none()), + has_entry("source", maps_to(attachments, content_matcher)) ) ) ) def with_id(): - return has_entry('uuid', not_none()) + return has_entry("uuid", not_none()) def with_status(status): - return has_entry('status', status) + return has_entry("status", status) def has_status_details(*matchers): - return has_entry('statusDetails', all_of(*matchers)) + return has_entry("statusDetails", all_of(*matchers)) + + +def has_global_error(*matchers): + return has_entry( + "errors", + has_item( + all_of( + has_entry("timestamp", not_none()), + *matchers + ) + ) + ) def with_message_contains(string): - return has_entry('message', contains_string(string)) + return has_entry("message", contains_string(string)) def with_trace_contains(string): - return has_entry('trace', contains_string(string)) + return has_entry("trace", contains_string(string)) +def with_no_trace(): + return not_(has_entry("trace", anything())) def with_excluded(): - return has_entry('excluded', True) + return has_entry("excluded", True) def with_mode(mode): - return has_entry('mode', mode) + return has_entry("mode", mode) def has_history_id(matcher=None): - return has_entry('historyId', matcher or anything()) + return has_entry("historyId", matcher or anything()) + + +def has_test_case_id(matcher=None): + return has_entry("testCaseId", matcher or anything()) def has_full_name(matcher): diff --git a/allure-python-commons/pyproject.toml b/allure-python-commons/pyproject.toml index 9fd4df26..3111b705 100644 --- a/allure-python-commons/pyproject.toml +++ b/allure-python-commons/pyproject.toml @@ -1,3 +1,3 @@ [tool.poe.tasks] -linter = "flake8 --extend-ignore=A001,A002,A003 ./src" +linter = "ruff check" tests = "python -m doctest ./src/allure_commons/*.py" diff --git a/allure-python-commons/setup.py b/allure-python-commons/setup.py index 75bb2332..f20db001 100644 --- a/allure-python-commons/setup.py +++ b/allure-python-commons/setup.py @@ -33,7 +33,7 @@ def main(): setup( name=PACKAGE, use_scm_version={"root": "..", "relative_to": __file__}, - setup_requires=["setuptools_scm"], + setup_requires=["setuptools_scm<10"], description=( "Contains the API for end users as well as helper functions and " "classes to build Allure adapters for Python test frameworks" diff --git a/allure-python-commons/src/allure/__init__.py b/allure-python-commons/src/allure/__init__.py index c30329a6..6fe2f270 100644 --- a/allure-python-commons/src/allure/__init__.py +++ b/allure-python-commons/src/allure/__init__.py @@ -10,6 +10,8 @@ from allure_commons._allure import Dynamic as dynamic from allure_commons._allure import step from allure_commons._allure import attach +from allure_commons._allure import global_attach +from allure_commons._allure import global_error from allure_commons._allure import manual from allure_commons.types import Severity as severity_level from allure_commons.types import AttachmentType as attachment_type @@ -17,27 +19,29 @@ __all__ = [ - 'title', - 'description', - 'description_html', - 'label', - 'severity', - 'suite', - 'parent_suite', - 'sub_suite', - 'tag', - 'id', - 'epic', - 'feature', - 'story', - 'link', - 'issue', - 'testcase', - 'manual', - 'step', - 'dynamic', - 'severity_level', - 'attach', - 'attachment_type', - 'parameter_mode' + "title", + "description", + "description_html", + "label", + "severity", + "suite", + "parent_suite", + "sub_suite", + "tag", + "id", + "epic", + "feature", + "story", + "link", + "issue", + "testcase", + "manual", + "step", + "dynamic", + "severity_level", + "attach", + "global_attach", + "global_error", + "attachment_type", + "parameter_mode" ] diff --git a/allure-python-commons/src/allure_commons/__init__.py b/allure-python-commons/src/allure_commons/__init__.py index 111c2d06..e480acea 100644 --- a/allure-python-commons/src/allure_commons/__init__.py +++ b/allure-python-commons/src/allure_commons/__init__.py @@ -1,12 +1,12 @@ -from allure_commons._hooks import hookimpl # noqa: F401 -from allure_commons._core import plugin_manager # noqa: F401 -from allure_commons._allure import fixture # noqa: F401 -from allure_commons._allure import test # noqa: F401 +from allure_commons._hooks import hookimpl +from allure_commons._core import plugin_manager +from allure_commons._allure import fixture +from allure_commons._allure import test __all__ = [ - 'hookimpl', - 'plugin_manager', - 'fixture', - 'test' + "hookimpl", + "plugin_manager", + "fixture", + "test" ] diff --git a/allure-python-commons/src/allure_commons/_allure.py b/allure-python-commons/src/allure_commons/_allure.py index b7bbe2a5..607e1cb8 100644 --- a/allure-python-commons/src/allure_commons/_allure.py +++ b/allure-python-commons/src/allure_commons/_allure.py @@ -4,6 +4,7 @@ from allure_commons._core import plugin_manager from allure_commons.types import LabelType, LinkType, ParameterMode from allure_commons.utils import uuid4 +from allure_commons.utils import format_exception, format_traceback from allure_commons.utils import func_parameters, represent _TFunc = TypeVar("_TFunc", bound=Callable[..., Any]) @@ -125,7 +126,7 @@ def tag(*tags): Dynamic.label(LabelType.TAG, *tags) @staticmethod - def id(id): # noqa: A003,A002 + def id(id): # noqa: A002 Dynamic.label(LabelType.ID, id) @staticmethod @@ -216,6 +217,48 @@ def file(self, source, name=None, attachment_type=None, extension=None): attach = Attach() +class GlobalAttach: + + def __call__(self, body, name=None, attachment_type=None, extension=None): + plugin_manager.hook.global_attach_data( + body=body, + name=name, + attachment_type=attachment_type, + extension=extension, + ) + + def file(self, source, name=None, attachment_type=None, extension=None): + plugin_manager.hook.global_attach_file( + source=source, + name=name, + attachment_type=attachment_type, + extension=extension, + ) + + +global_attach = GlobalAttach() + + +@overload +def global_error(value: BaseException) -> None: + ... + + +@overload +def global_error(value: str, trace: Union[str, None] = None) -> None: + ... + + +def global_error(value, trace=None): + message = None + if isinstance(value, BaseException): + message = format_exception(type(value), value) + trace = format_traceback(value.__traceback__) + else: + message = value + plugin_manager.hook.global_error(message=message, trace=trace) + + class fixture: def __init__(self, fixture_function, parent_uuid=None, name=None): self._fixture_function = fixture_function diff --git a/allure-python-commons/src/allure_commons/_core.py b/allure-python-commons/src/allure_commons/_core.py index 40d9deaf..8687891c 100644 --- a/allure-python-commons/src/allure_commons/_core.py +++ b/allure-python-commons/src/allure_commons/_core.py @@ -8,7 +8,7 @@ class MetaPluginManager(type): @staticmethod def get_plugin_manager(): if not MetaPluginManager._plugin_manager: - MetaPluginManager._plugin_manager = PluginManager('allure') + MetaPluginManager._plugin_manager = PluginManager("allure") MetaPluginManager._plugin_manager.add_hookspecs(_hooks.AllureUserHooks) MetaPluginManager._plugin_manager.add_hookspecs(_hooks.AllureDeveloperHooks) diff --git a/allure-python-commons/src/allure_commons/_hooks.py b/allure-python-commons/src/allure_commons/_hooks.py index 0ff19a27..84e916d9 100644 --- a/allure-python-commons/src/allure_commons/_hooks.py +++ b/allure-python-commons/src/allure_commons/_hooks.py @@ -66,6 +66,18 @@ def attach_data(self, body, name, attachment_type, extension): def attach_file(self, source, name, attachment_type, extension): """ attach file """ + @hookspec + def global_attach_data(self, body, name, attachment_type, extension): + """ attach global data """ + + @hookspec + def global_attach_file(self, source, name, attachment_type, extension): + """ attach global file """ + + @hookspec + def global_error(self, message, trace): + """ global error """ + class AllureDeveloperHooks: @@ -100,3 +112,7 @@ def report_attached_file(self, source, file_name): @hookspec def report_attached_data(self, body, file_name): """ reporting """ + + @hookspec + def report_globals(self, globals_item): + """ reporting """ diff --git a/allure-python-commons/src/allure_commons/lifecycle.py b/allure-python-commons/src/allure_commons/lifecycle.py index 2e730e2e..e2c2251e 100644 --- a/allure-python-commons/src/allure_commons/lifecycle.py +++ b/allure-python-commons/src/allure_commons/lifecycle.py @@ -4,6 +4,7 @@ from allure_commons.model2 import TestResultContainer from allure_commons.model2 import TestResult from allure_commons.model2 import Attachment, ATTACHMENT_PATTERN +from allure_commons.model2 import GlobalAttachment, GlobalError, Globals from allure_commons.model2 import TestStepResult from allure_commons.model2 import ExecutableItem from allure_commons.model2 import TestBeforeResult @@ -124,14 +125,11 @@ def stop_after_fixture(self, uuid=None): fixture.stop = now() def _attach(self, uuid, name=None, attachment_type=None, extension=None, parent_uuid=None): - mime_type = attachment_type - extension = extension if extension else 'attach' - - if type(attachment_type) is AttachmentType: - extension = attachment_type.extension - mime_type = attachment_type.mime_type - - file_name = ATTACHMENT_PATTERN.format(prefix=uuid, ext=extension) + file_name, mime_type = self.__resolve_attachment_filename_and_type( + uuid, + attachment_type=attachment_type, + extension=extension, + ) attachment = Attachment(source=file_name, name=name, type=mime_type) last_uuid = parent_uuid if parent_uuid else self._last_item_uuid(ExecutableItem) self._items[last_uuid].attachments.append(attachment) @@ -147,3 +145,41 @@ def attach_data(self, uuid, body, name=None, attachment_type=None, extension=Non file_name = self._attach(uuid, name=name, attachment_type=attachment_type, extension=extension, parent_uuid=parent_uuid) plugin_manager.hook.report_attached_data(body=body, file_name=file_name) + + def global_attach_file(self, uuid, source, name=None, attachment_type=None, extension=None): + file_name, mime_type = self.__resolve_attachment_filename_and_type( + uuid, + attachment_type=attachment_type, + extension=extension, + ) + plugin_manager.hook.report_attached_file(source=source, file_name=file_name) + plugin_manager.hook.report_globals(globals_item=Globals(attachments=[ + GlobalAttachment(source=file_name, name=name, type=mime_type, timestamp=now()) + ])) + + def global_attach_data(self, uuid, body, name=None, attachment_type=None, extension=None): + file_name, mime_type = self.__resolve_attachment_filename_and_type( + uuid, + attachment_type=attachment_type, + extension=extension, + ) + plugin_manager.hook.report_attached_data(body=body, file_name=file_name) + plugin_manager.hook.report_globals(globals_item=Globals(attachments=[ + GlobalAttachment(source=file_name, name=name, type=mime_type, timestamp=now()) + ])) + + def global_error(self, message=None, trace=None): + plugin_manager.hook.report_globals(globals_item=Globals(errors=[ + GlobalError(message=message, trace=trace, timestamp=now()) + ])) + + def __resolve_attachment_filename_and_type(self, uuid, attachment_type=None, extension=None): + mime_type = attachment_type + extension = extension if extension else "attach" + + if type(attachment_type) is AttachmentType: + extension = attachment_type.extension + mime_type = attachment_type.mime_type + + file_name = ATTACHMENT_PATTERN.format(prefix=uuid, ext=extension) + return file_name, mime_type diff --git a/allure-python-commons/src/allure_commons/logger.py b/allure-python-commons/src/allure_commons/logger.py index 55f956f2..4345bc77 100644 --- a/allure-python-commons/src/allure_commons/logger.py +++ b/allure-python-commons/src/allure_commons/logger.py @@ -22,7 +22,7 @@ def _report_item(self, item): indent = INDENT if os.environ.get("ALLURE_INDENT_OUTPUT") else None filename = item.file_pattern.format(prefix=uuid.uuid4()) data = asdict(item, filter=lambda _, v: v or v is False) - with io.open(self._report_dir / filename, 'w', encoding='utf8') as json_file: + with io.open(self._report_dir / filename, "w", encoding="utf8") as json_file: json.dump(data, json_file, indent=indent, ensure_ascii=False) @hookimpl @@ -41,12 +41,16 @@ def report_attached_file(self, source, file_name): @hookimpl def report_attached_data(self, body, file_name): destination = self._report_dir / file_name - with open(destination, 'wb') as attached_file: + with open(destination, "wb") as attached_file: if isinstance(body, str): - attached_file.write(body.encode('utf-8')) + attached_file.write(body.encode("utf-8")) else: attached_file.write(body) + @hookimpl + def report_globals(self, globals_item): + self._report_item(globals_item) + class AllureMemoryLogger: @@ -54,6 +58,7 @@ def __init__(self): self.test_cases = [] self.test_containers = [] self.attachments = {} + self.globals = [] @hookimpl def report_result(self, result): @@ -72,3 +77,8 @@ def report_attached_file(self, source, file_name): @hookimpl def report_attached_data(self, body, file_name): self.attachments[file_name] = body + + @hookimpl + def report_globals(self, globals_item): + data = asdict(globals_item, filter=lambda _, v: v or v is False) + self.globals.append(data) diff --git a/allure-python-commons/src/allure_commons/mapping.py b/allure-python-commons/src/allure_commons/mapping.py index 737d3390..0cd099f0 100644 --- a/allure-python-commons/src/allure_commons/mapping.py +++ b/allure-python-commons/src/allure_commons/mapping.py @@ -20,7 +20,7 @@ def allure_tag_sep(tag): def __is(kind, t): - return kind in [v for k, v in t.__dict__.items() if not k.startswith('__')] + return kind in [v for k, v in t.__dict__.items() if not k.startswith("__")] def parse_tag(tag, issue_pattern=None, link_pattern=None): @@ -51,7 +51,7 @@ def parse_tag(tag, issue_pattern=None, link_pattern=None): """ sep = allure_tag_sep(tag) schema, value = islice(chain(tag.split(sep, 1), [None]), 2) - prefix, kind, name = islice(chain(schema.split('.'), [None], [None]), 3) + prefix, kind, name = islice(chain(schema.split("."), [None], [None]), 3) if tag in [severity for severity in Severity]: return Label(name=LabelType.SEVERITY, value=tag) diff --git a/allure-python-commons/src/allure_commons/model2.py b/allure-python-commons/src/allure_commons/model2.py index d8591598..cd069b17 100644 --- a/allure-python-commons/src/allure_commons/model2.py +++ b/allure-python-commons/src/allure_commons/model2.py @@ -4,7 +4,8 @@ TEST_GROUP_PATTERN = "{prefix}-container.json" TEST_CASE_PATTERN = "{prefix}-result.json" -ATTACHMENT_PATTERN = '{prefix}-attachment.{ext}' +ATTACHMENT_PATTERN = "{prefix}-attachment.{ext}" +GLOBALS_PATTERN = "{prefix}-globals.json" INDENT = 4 @@ -54,7 +55,7 @@ class TestResult(ExecutableItem): @attrs class TestStepResult(ExecutableItem): - id = attrib(default=None) # noqa: A003 + id = attrib(default=None) @attrs @@ -83,7 +84,7 @@ class Label: @attrs class Link: - type = attrib(default=None) # noqa: A003 + type = attrib(default=None) url = attrib(default=None) name = attrib(default=None) @@ -100,12 +101,30 @@ class StatusDetails: class Attachment: name = attrib(default=None) source = attrib(default=None) - type = attrib(default=None) # noqa: A003 + type = attrib(default=None) + + +@attrs +class GlobalAttachment(Attachment): + timestamp = attrib(default=None) + + +@attrs +class GlobalError(StatusDetails): + timestamp = attrib(default=None) + + +@attrs +class Globals: + file_pattern = GLOBALS_PATTERN + + attachments = attrib(default=Factory(list)) + errors = attrib(default=Factory(list)) class Status: - FAILED = 'failed' - BROKEN = 'broken' - PASSED = 'passed' - SKIPPED = 'skipped' - UNKNOWN = 'unknown' + FAILED = "failed" + BROKEN = "broken" + PASSED = "passed" + SKIPPED = "skipped" + UNKNOWN = "unknown" diff --git a/allure-python-commons/src/allure_commons/reporter.py b/allure-python-commons/src/allure_commons/reporter.py index 2e1f4a89..7e7b7594 100644 --- a/allure-python-commons/src/allure_commons/reporter.py +++ b/allure-python-commons/src/allure_commons/reporter.py @@ -5,6 +5,7 @@ from allure_commons.model2 import ExecutableItem from allure_commons.model2 import TestResult from allure_commons.model2 import Attachment, ATTACHMENT_PATTERN +from allure_commons.model2 import GlobalAttachment, GlobalError, Globals from allure_commons.utils import now from allure_commons._core import plugin_manager @@ -140,14 +141,7 @@ def stop_step(self, uuid, **kwargs): self._items.pop(uuid) def _attach(self, uuid, name=None, attachment_type=None, extension=None, parent_uuid=None): - mime_type = attachment_type - extension = extension if extension else 'attach' - - if type(attachment_type) is AttachmentType: - extension = attachment_type.extension - mime_type = attachment_type.mime_type - - file_name = ATTACHMENT_PATTERN.format(prefix=uuid, ext=extension) + file_name, mime_type = self.__resolve_attachment_filename_and_type(uuid, attachment_type, extension) attachment = Attachment(source=file_name, name=name, type=mime_type) last_uuid = parent_uuid if parent_uuid else self._last_executable() self._items[last_uuid].attachments.append(attachment) @@ -163,3 +157,33 @@ def attach_data(self, uuid, body, name=None, attachment_type=None, extension=Non file_name = self._attach(uuid, name=name, attachment_type=attachment_type, extension=extension, parent_uuid=parent_uuid) plugin_manager.hook.report_attached_data(body=body, file_name=file_name) + + def global_attach_file(self, uuid, source, name=None, attachment_type=None, extension=None): + file_name, mime_type = self.__resolve_attachment_filename_and_type(uuid, attachment_type, extension) + plugin_manager.hook.report_attached_file(source=source, file_name=file_name) + plugin_manager.hook.report_globals(globals_item=Globals(attachments=[ + GlobalAttachment(source=file_name, name=name, type=mime_type, timestamp=now()) + ])) + + def global_attach_data(self, uuid, body, name=None, attachment_type=None, extension=None): + file_name, mime_type = self.__resolve_attachment_filename_and_type(uuid, attachment_type, extension) + plugin_manager.hook.report_attached_data(body=body, file_name=file_name) + plugin_manager.hook.report_globals(globals_item=Globals(attachments=[ + GlobalAttachment(source=file_name, name=name, type=mime_type, timestamp=now()) + ])) + + def global_error(self, message=None, trace=None): + plugin_manager.hook.report_globals(globals_item=Globals(errors=[ + GlobalError(message=message, trace=trace, timestamp=now()) + ])) + + def __resolve_attachment_filename_and_type(self, uuid, attachment_type=None, extension=None): + mime_type = attachment_type + extension = extension if extension else "attach" + + if type(attachment_type) is AttachmentType: + extension = attachment_type.extension + mime_type = attachment_type.mime_type + + file_name = ATTACHMENT_PATTERN.format(prefix=uuid, ext=extension) + return file_name, mime_type diff --git a/allure-python-commons/src/allure_commons/types.py b/allure-python-commons/src/allure_commons/types.py index e631e427..43b8d5ed 100644 --- a/allure-python-commons/src/allure_commons/types.py +++ b/allure-python-commons/src/allure_commons/types.py @@ -1,37 +1,37 @@ from enum import Enum -ALLURE_UNIQUE_LABELS = ['severity', 'thread', 'host'] +ALLURE_UNIQUE_LABELS = ["severity", "thread", "host"] class Severity(str, Enum): - BLOCKER = 'blocker' - CRITICAL = 'critical' - NORMAL = 'normal' - MINOR = 'minor' - TRIVIAL = 'trivial' + BLOCKER = "blocker" + CRITICAL = "critical" + NORMAL = "normal" + MINOR = "minor" + TRIVIAL = "trivial" class LinkType: - LINK = 'link' - ISSUE = 'issue' - TEST_CASE = 'tms' + LINK = "link" + ISSUE = "issue" + TEST_CASE = "tms" class LabelType(str): - EPIC = 'epic' - FEATURE = 'feature' - STORY = 'story' - PARENT_SUITE = 'parentSuite' - SUITE = 'suite' - SUB_SUITE = 'subSuite' - SEVERITY = 'severity' - THREAD = 'thread' - HOST = 'host' - TAG = 'tag' - ID = 'as_id' - FRAMEWORK = 'framework' - LANGUAGE = 'language' - MANUAL = 'ALLURE_MANUAL' + EPIC = "epic" + FEATURE = "feature" + STORY = "story" + PARENT_SUITE = "parentSuite" + SUITE = "suite" + SUB_SUITE = "subSuite" + SEVERITY = "severity" + THREAD = "thread" + HOST = "host" + TAG = "tag" + ID = "as_id" + FRAMEWORK = "framework" + LANGUAGE = "language" + MANUAL = "ALLURE_MANUAL" class AttachmentType(Enum): @@ -50,6 +50,7 @@ def __init__(self, mime_type, extension): JSON = ("application/json", "json") YAML = ("application/yaml", "yaml") PCAP = ("application/vnd.tcpdump.pcap", "pcap") + ZIP = ("application/zip", "zip") PNG = ("image/png", "png") JPG = ("image/jpg", "jpg") @@ -66,6 +67,6 @@ def __init__(self, mime_type, extension): class ParameterMode(Enum): - HIDDEN = 'hidden' - MASKED = 'masked' + HIDDEN = "hidden" + MASKED = "masked" DEFAULT = None diff --git a/allure-python-commons/src/allure_commons/utils.py b/allure-python-commons/src/allure_commons/utils.py index 5ba0d377..c50cb7ec 100644 --- a/allure-python-commons/src/allure_commons/utils.py +++ b/allure-python-commons/src/allure_commons/utils.py @@ -21,7 +21,7 @@ def md5(*args): if not isinstance(arg, bytes): if not isinstance(arg, str): arg = repr(arg) - arg = arg.encode('utf-8') + arg = arg.encode("utf-8") m.update(arg) return m.hexdigest() @@ -37,11 +37,11 @@ def now(): def platform_label(): major_version, *_ = platform.python_version_tuple() implementation = platform.python_implementation().lower() - return f'{implementation}{major_version}' + return f"{implementation}{major_version}" def thread_tag(): - return '{0}-{1}'.format(os.getpid(), threading.current_thread().name) + return "{0}-{1}".format(os.getpid(), threading.current_thread().name) def host_tag(): @@ -241,7 +241,7 @@ def func_parameters(func, *args, **kwargs): varargs = args[len(arg_spec.args):] parameters.update({arg_spec.varargs: varargs} if varargs else {}) - if arg_spec.args and arg_spec.args[0] in ['cls', 'self']: + if arg_spec.args and arg_spec.args[0] in ["cls", "self"]: args_dict.pop(arg_spec.args[0], None) if kwargs: @@ -269,7 +269,7 @@ def func_parameters(func, *args, **kwargs): def format_traceback(exc_traceback): - return ''.join(traceback.format_tb(exc_traceback)) if exc_traceback else None + return "".join(traceback.format_tb(exc_traceback)) if exc_traceback else None def format_exception(etype, value): @@ -320,7 +320,7 @@ def format_exception(etype, value): ... format_exception(etype, e) # doctest: +ELLIPSIS "AssertionError: \\nExpected:...but:..." """ - return '\n'.join(format_exception_only(etype, value)) if etype or value else None + return "\n".join(format_exception_only(etype, value)) if etype or value else None def get_testplan(): @@ -328,7 +328,7 @@ def get_testplan(): file_path = os.environ.get("ALLURE_TESTPLAN_PATH") if file_path and os.path.exists(file_path): - with open(file_path, 'r') as plan_file: + with open(file_path, "r") as plan_file: plan = json.load(plan_file) planned_tests = plan.get("tests", []) diff --git a/allure-robotframework/pyproject.toml b/allure-robotframework/pyproject.toml index 0ee96703..2d6844e1 100644 --- a/allure-robotframework/pyproject.toml +++ b/allure-robotframework/pyproject.toml @@ -1,5 +1,5 @@ [tool.poe.tasks] -linter = "flake8 ./src" +linter = "ruff check" [tool.poe.tasks.tests] shell = "python -m doctest ./src/listener/utils.py && pytest ../tests/allure_robotframework" diff --git a/allure-robotframework/setup.py b/allure-robotframework/setup.py index 8b194c29..9cb42fe7 100644 --- a/allure-robotframework/setup.py +++ b/allure-robotframework/setup.py @@ -4,25 +4,25 @@ PACKAGE = "allure-robotframework" classifiers = [ - 'Development Status :: 5 - Production/Stable', - 'Framework :: Robot Framework', - 'Framework :: Robot Framework :: Tool', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Topic :: Software Development :: Quality Assurance', - 'Topic :: Software Development :: Testing', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Programming Language :: Python :: 3.12', - 'Programming Language :: Python :: 3.13', + "Development Status :: 5 - Production/Stable", + "Framework :: Robot Framework", + "Framework :: Robot Framework :: Tool", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Topic :: Software Development :: Quality Assurance", + "Topic :: Software Development :: Testing", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] setup_requires = [ - "setuptools_scm" + "setuptools_scm<10" ] install_requires = [ @@ -41,7 +41,7 @@ def get_readme(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() -if __name__ == '__main__': +if __name__ == "__main__": setup( name=PACKAGE, use_scm_version=prepare_version, @@ -50,9 +50,9 @@ def get_readme(fname): install_requires=install_requires, setup_requires=setup_requires, keywords="allure reporting robotframework", - packages=['allure_robotframework', 'AllureLibrary'], - package_dir={"allure_robotframework": "src/listener", 'AllureLibrary': 'src/library'}, - py_modules=['allure_robotframework'], + packages=["allure_robotframework", "AllureLibrary"], + package_dir={"allure_robotframework": "src/listener", "AllureLibrary": "src/library"}, + py_modules=["allure_robotframework"], url="https://allurereport.org/", project_urls={ "Source": "https://github.com/allure-framework/allure-python", diff --git a/allure-robotframework/src/library/__init__.py b/allure-robotframework/src/library/__init__.py index 0786652e..d21bce6b 100644 --- a/allure-robotframework/src/library/__init__.py +++ b/allure-robotframework/src/library/__init__.py @@ -1,3 +1,3 @@ -from .allure_library import attach, attach_file +from .allure_library import attach, attach_file, global_attach, global_attach_file, global_error -__all__ = ['attach', 'attach_file'] +__all__ = ["attach", "attach_file", "global_attach", "global_attach_file", "global_error"] diff --git a/allure-robotframework/src/library/allure_library.py b/allure-robotframework/src/library/allure_library.py index a7dc93e5..e9d4f7b1 100644 --- a/allure-robotframework/src/library/allure_library.py +++ b/allure-robotframework/src/library/allure_library.py @@ -1,7 +1,7 @@ import allure -__all__ = ['attach', 'attach_file'] +__all__ = ["attach", "attach_file", "global_attach", "global_attach_file", "global_error"] def _attachment_type(name): @@ -17,3 +17,15 @@ def attach(data, name=None, attachment_type=None, extension=None): def attach_file(source, name=None, attachment_type=None, extension=None): allure.attach.file(source, name=name, attachment_type=_attachment_type(attachment_type), extension=extension) + + +def global_attach(data, name=None, attachment_type=None, extension=None): + allure.global_attach(data, name=name, attachment_type=_attachment_type(attachment_type), extension=extension) + + +def global_attach_file(source, name=None, attachment_type=None, extension=None): + allure.global_attach.file(source, name=name, attachment_type=_attachment_type(attachment_type), extension=extension) + + +def global_error(message, trace=None): + allure.global_error(message, trace=trace) diff --git a/allure-robotframework/src/listener/__init__.py b/allure-robotframework/src/listener/__init__.py index d2640a4e..92740385 100644 --- a/allure-robotframework/src/listener/__init__.py +++ b/allure-robotframework/src/listener/__init__.py @@ -1,4 +1,4 @@ from allure_robotframework.robot_listener import allure_robotframework from allure_robotframework.allure_testplan import allure_testplan as testplan -__all__ = ['allure_robotframework', "testplan"] +__all__ = ["allure_robotframework", "testplan"] diff --git a/allure-robotframework/src/listener/allure_listener.py b/allure-robotframework/src/listener/allure_listener.py index 236a524e..6be71a19 100644 --- a/allure-robotframework/src/listener/allure_listener.py +++ b/allure-robotframework/src/listener/allure_listener.py @@ -39,8 +39,8 @@ def get_status_details(exc_type, exception, exc_traceback): def pool_id(): - pabot_pool_id = BuiltIn().get_variable_value('${PABOTEXECUTIONPOOLID}') - pabot_caller_id = BuiltIn().get_variable_value('${CALLER_ID}') + pabot_pool_id = BuiltIn().get_variable_value("${PABOTEXECUTIONPOOLID}") + pabot_caller_id = BuiltIn().get_variable_value("${CALLER_ID}") return f"{pabot_pool_id}-{pabot_caller_id}" if all([ pabot_pool_id, pabot_caller_id @@ -52,9 +52,9 @@ def get_message_time(timestamp): return int(s_time.timestamp() * 1000) -LOG_MESSAGE_FORMAT = '
[{level}] {message}
' +LOG_MESSAGE_FORMAT = "[{level}] {message}
" FAIL_MESSAGE_FORMAT = '[{level}] {message}
' -MAX_STEP_MESSAGE_COUNT = int(os.getenv('ALLURE_MAX_STEP_MESSAGE_COUNT', 0)) +MAX_STEP_MESSAGE_COUNT = int(os.getenv("ALLURE_MAX_STEP_MESSAGE_COUNT", 0)) class AllureListener: @@ -70,8 +70,8 @@ def start_suite_container(self, name, attributes): pass def stop_suite_container(self, name, attributes): - suite_status = get_allure_status(attributes.get('status')) - suite_message = attributes.get('message') + suite_status = get_allure_status(attributes.get("status")) + suite_message = attributes.get("message") with self.lifecycle.update_container() as container: for uuid in container.children: @@ -89,8 +89,8 @@ def start_test_container(self, name, attributes): pass def stop_test_container(self, name, attributes): - suite_status = get_allure_status(attributes.get('status')) - suite_message = attributes.get('message') + suite_status = get_allure_status(attributes.get("status")) + suite_message = attributes.get("message") with self.lifecycle.schedule_test_case() as test_result: if test_result.status == Status.PASSED and suite_message: @@ -106,7 +106,7 @@ def start_before_fixture(self, name): fixture.name = name def stop_before_fixture(self, attributes, messages): - status = attributes.get('status') + status = attributes.get("status") self._report_messages(status, messages) with self.lifecycle.update_before_fixture() as fixture: fixture.status = get_allure_status(status) @@ -118,7 +118,7 @@ def start_after_fixture(self, name): fixture.name = name def stop_after_fixture(self, attributes, messages): - status = attributes.get('status') + status = attributes.get("status") self._report_messages(status, messages) with self.lifecycle.update_after_fixture() as fixture: fixture.status = get_allure_status(status) @@ -128,37 +128,38 @@ def stop_after_fixture(self, attributes, messages): def start_test(self, name, attributes): uuid = uuid4() with self.lifecycle.schedule_test_case(uuid=uuid) as test_result: - long_name = attributes.get('longname') + long_name = attributes.get("longname") test_result.name = name test_result.fullName = long_name test_result.titlePath = attributes.get("titlepath", []) - test_result.historyId = md5(long_name) + test_result.testCaseId = md5(long_name) + test_result.historyId = test_result.testCaseId test_result.start = now() for container in self.lifecycle.containers(): container.children.append(uuid) def stop_test(self, _, attributes, messages): - self._report_messages(attributes.get('status'), messages) + self._report_messages(attributes.get("status"), messages) - if 'skipped' in [tag.lower() for tag in attributes['tags']]: - attributes['status'] = RobotStatus.SKIPPED + if "skipped" in [tag.lower() for tag in attributes["tags"]]: + attributes["status"] = RobotStatus.SKIPPED with self.lifecycle.update_test_case() as test_result: test_result.stop = now() - test_result.description = attributes.get('doc') - test_result.status = get_allure_status(attributes.get('status')) - test_result.labels.extend(get_allure_suites(attributes.get('longname'))) - test_result.labels.append(Label(name=LabelType.FRAMEWORK, value='robotframework')) + test_result.description = attributes.get("doc") + test_result.status = get_allure_status(attributes.get("status")) + test_result.labels.extend(get_allure_suites(attributes.get("longname"))) + test_result.labels.append(Label(name=LabelType.FRAMEWORK, value="robotframework")) test_result.labels.append(Label(name=LabelType.LANGUAGE, value=self._platform)) test_result.labels.append(Label(name=LabelType.HOST, value=self._host)) test_result.labels.append(Label(name=LabelType.THREAD, value=pool_id())) - tags = attributes.get('tags', ()) + tags = attributes.get("tags", ()) test_result.labels.extend(allure_labels(tags)) - test_result.statusDetails = StatusDetails(message=attributes.get('message'), + test_result.statusDetails = StatusDetails(message=attributes.get("message"), trace=self._current_tb) - if attributes.get('critical') == 'yes': + if attributes.get("critical") == "yes": test_result.labels.append(Label(name=LabelType.SEVERITY, value=Severity.CRITICAL)) for link_type in (LinkType.ISSUE, LinkType.TEST_CASE, LinkType.LINK): @@ -171,11 +172,11 @@ def start_keyword(self, name): step.name = name def stop_keyword(self, attributes, messages): - status = attributes.get('status') + status = attributes.get("status") self._report_messages(status, messages) with self.lifecycle.update_step() as step: step.status = get_allure_status(status) - step.parameters = get_allure_parameters(attributes.get('args')) + step.parameters = get_allure_parameters(attributes.get("args")) step.statusDetails = StatusDetails(message=self._current_msg, trace=self._current_tb) self.lifecycle.stop_step() @@ -187,8 +188,8 @@ def _report_messages(self, status, messages): self._current_tb, self._current_msg = None, None for message, next_message in zip_longest(messages, messages[1:]): - name = message.get('message') - level = message.get('level') + name = message.get("message") + level = message.get("level") message_format = FAIL_MESSAGE_FORMAT if level in RobotLogLevel.CRITICAL_LEVELS else LOG_MESSAGE_FORMAT if level == RobotLogLevel.FAIL: @@ -196,7 +197,7 @@ def _report_messages(self, status, messages): self._current_tb = next_message.get("message") if has_trace and next_message else self._current_tb if len(messages) > MAX_STEP_MESSAGE_COUNT: - attachment += message_format.format(level=level, message=name.replace('\n', '