diff --git a/allure-pytest-bdd/src/attachment_worker.py b/allure-pytest-bdd/src/attachment_worker.py new file mode 100644 index 00000000..d2be475d --- /dev/null +++ b/allure-pytest-bdd/src/attachment_worker.py @@ -0,0 +1,61 @@ +import os.path +import os + + +class AttachmentWorker: + + def __init__(self, test_result, item): + self.test_result = test_result + self.attachments_dir = AttachmentWorker.get_path_to_attachments(item) + + def delete_duplicates(self): + if len(self.test_result.attachments) == 0: + return + + for step in self.test_result.steps: + for attach in step.attachments: + to_delete = self._find_duplicate(attach) + if to_delete is not None: + self.test_result.attachments.remove(to_delete) + os.remove(os.path.join(self.attachments_dir, to_delete.source)) + + @staticmethod + def get_path_to_attachments(item): + splitted_param = AttachmentWorker._get_allurdir_param(item).split('=') + + project_dir = str(item.config.invocation_params.dir) + if len(splitted_param) == 1: + return project_dir + + allure_dir = os.path.normpath(splitted_param[1]) + if not os.path.isabs(allure_dir): + allure_dir = os.path.join(project_dir, allure_dir.lstrip("\\")) + + return allure_dir + + def _find_duplicate(self, attachment_from_step): + for attachment in self.test_result.attachments: + if self._are_attachments_equal(attachment, attachment_from_step): + return attachment + + return None + + def _are_attachments_equal(self, first, second): + first_file = open(os.path.join(self.attachments_dir, first.source), 'br') + first_content = first_file.read() + first_file.close() + + second_file = open(os.path.join(self.attachments_dir, second.source), 'br') + second_content = second_file.read() + second_file.close() + + return \ + first.name == second.name and \ + first.type == second.type and \ + first_content == second_content + + @staticmethod + def _get_allurdir_param(item): + for param in item.config.invocation_params.args: + if param.startswith("--alluredir"): + return param diff --git a/allure-pytest-bdd/src/pytest_bdd_listener.py b/allure-pytest-bdd/src/pytest_bdd_listener.py index 0ed04f13..76e5267f 100644 --- a/allure-pytest-bdd/src/pytest_bdd_listener.py +++ b/allure-pytest-bdd/src/pytest_bdd_listener.py @@ -17,6 +17,8 @@ from allure_commons.lifecycle import AllureLifecycle from .utils import get_full_name, get_name, get_params +from .attachment_worker import AttachmentWorker + class PytestBDDListener(object): def __init__(self): @@ -113,6 +115,9 @@ def pytest_runtest_makereport(self, item, call): test_result.status = status test_result.statusDetails = status_details + if test_result and test_result.status: + AttachmentWorker(test_result, item).delete_duplicates() + if report.when == 'teardown': self.lifecycle.write_test_case(uuid=uuid) diff --git a/allure-pytest-bdd/test/acceptance.feature b/allure-pytest-bdd/test/acceptance.feature new file mode 100644 index 00000000..382af5e1 --- /dev/null +++ b/allure-pytest-bdd/test/acceptance.feature @@ -0,0 +1,16 @@ +# Created by denis at 11.01.2021 +Feature: test + # Enter feature description here + + Scenario: scenario + Given py file with name: example + And with imports: pytest, pytest_bdd, allure + And with func: + """ + @pytest_bdd.given("given_step") + def my_func(): + allure.attach("blah", ...) + raise Exception("message") + """ + And test for scenario_name from file.feature + And py file saved diff --git a/allure-pytest-bdd/test/attachments_tests/attachment_test.py b/allure-pytest-bdd/test/attachments_tests/attachment_test.py new file mode 100644 index 00000000..bd3f46a0 --- /dev/null +++ b/allure-pytest-bdd/test/attachments_tests/attachment_test.py @@ -0,0 +1,42 @@ +from pytest_bdd import scenario + + +@scenario("attachments_features\\attachment_in_step.feature", "Attachment in Given") +def test_given_with_attachments(): + pass + + +@scenario("attachments_features\\attachment_in_step.feature", "Attachment in When") +def test_when_with_attachments(): + pass + + +@scenario("attachments_features\\attachment_in_step.feature", "Attachment in Then") +def test_then_with_attachments(): + pass + + +@scenario("attachments_features\\attachment_in_exception_step.feature", "Attachment and exception in step") +def test_step_with_attachments_and_exception(): + pass + + +@scenario("attachments_features\\attachment_in_many_steps.feature", "Attachment in many steps") +def test_attachment_in_many_steps(): + pass + + +@scenario("attachments_features\\attachment_in_failed_step.feature", "Attachment in failed step") +def test_failed_step_with_attachment(): + pass + + +@scenario("attachments_features\\attachment_in_called_fixture.feature", "Attachment in called fixture") +def test_attachment_in_called_fixture(): + pass + + +@scenario("attachments_features\\attachment_in_step-fixture.feature", + "attachment in a step, that is also a fixture") +def test_attachment_in_step_fixture(): + pass diff --git a/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_called_fixture.feature b/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_called_fixture.feature new file mode 100644 index 00000000..6fe3b583 --- /dev/null +++ b/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_called_fixture.feature @@ -0,0 +1,37 @@ +Feature: Attachments + + Scenario: Attachment in called fixture + Given example.feature with content: + """ + Feature: Feature Test + Scenario: My Scenario Test + Given passed step + When when-step use fixture, that have attachment + Then passed step + """ + And py file with name: example_test + And with imports: pytest, pytest_bdd, allure + And with passed steps + And with func: + """ + @pytest.fixture() + def fixture_with_attachment(): + allure.attach('Attachment content', 'allure attachment', allure.attachment_type.TEXT) + """ + And with func: + """ + @pytest_bdd.when("when-step use fixture, that have attachment") + def step_with_attachment(fixture_with_attachment): + pass + """ + And test for My Scenario Test from example.feature + And py file saved + + When run pytest-bdd with allure + + Then attachment allure attachment must be in attachments + And this attachment with content: + """ + Attachment content + """ + And attachments must not be in When when-step use fixture, that have attachment \ No newline at end of file diff --git a/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_exception_step.feature b/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_exception_step.feature new file mode 100644 index 00000000..b0dabf85 --- /dev/null +++ b/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_exception_step.feature @@ -0,0 +1,31 @@ +Feature: Attachments + + Scenario: Attachment and exception in step + Given example.feature with content: + """ + Feature: Feature Test + Scenario: My Scenario Test + Given step with attach and exception + When passed step + Then passed step + """ + And py file with name: example_test + And with imports: pytest_bdd, allure + And with passed steps + And with func: + """ + @pytest_bdd.given("step with attach and exception") + def step_with_attachment(): + allure.attach('Attachment content', 'allure attachment', allure.attachment_type.TEXT) + raise Exception("Exception message") + """ + And test for My Scenario Test from example.feature + And py file saved + + When run pytest-bdd with allure + + Then attachment allure attachment must be in Given step with attach and exception + And this attachment with content: + """ + Attachment content + """ diff --git a/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_failed_step.feature b/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_failed_step.feature new file mode 100644 index 00000000..91c07ebc --- /dev/null +++ b/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_failed_step.feature @@ -0,0 +1,31 @@ +Feature: Attachments + + Scenario: Attachment in failed step + Given example.feature with content: + """ + Feature: Feature Test + Scenario: My Scenario Test + Given failed step with attach + When passed step + Then passed step + """ + And py file with name: example_test + And with imports: pytest_bdd, allure + And with passed steps + And with func: + """ + @pytest_bdd.given("failed step with attach") + def step_with_attachment(): + allure.attach('Attachment content', 'allure attachment', allure.attachment_type.TEXT) + assert False + """ + And test for My Scenario Test from example.feature + And py file saved + + When run pytest-bdd with allure + + Then attachment allure attachment must be in Given failed step with attach + And this attachment with content: + """ + Attachment content + """ diff --git a/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_many_steps.feature b/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_many_steps.feature new file mode 100644 index 00000000..a591261f --- /dev/null +++ b/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_many_steps.feature @@ -0,0 +1,52 @@ +Feature: Attachments + + Scenario: Attachment in many steps + Given example.feature with content: + """ + Feature: Feature Test + Scenario: My Scenario Test + Given given step with attach + When when step with attach + Then then step with attach + """ + And py file with name: example_test + And with imports: pytest_bdd, allure + And with passed steps + And with func: + """ + @pytest_bdd.given("given step with attach") + def given_step_with_attachment(): + allure.attach('Given Attachment content', 'given allure attachment', allure.attachment_type.TEXT) + """ + And with func: + """ + @pytest_bdd.when("when step with attach") + def when_step_with_attachment(): + allure.attach('When Attachment content', 'when allure attachment', allure.attachment_type.TEXT) + """ + And with func: + """ + @pytest_bdd.then("then step with attach") + def then_step_with_attachment(): + allure.attach('Then Attachment content', 'then allure attachment', allure.attachment_type.TEXT) + """ + And test for My Scenario Test from example.feature + And py file saved + + When run pytest-bdd with allure + + Then attachment given allure attachment must be in Given given step with attach + And this attachment with content: + """ + Given Attachment content + """ + And attachment when allure attachment must be in When when step with attach + And this attachment with content: + """ + When Attachment content + """ + And attachment then allure attachment must be in Then then step with attach + And this attachment with content: + """ + Then Attachment content + """ diff --git a/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_step-fixture.feature b/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_step-fixture.feature new file mode 100644 index 00000000..80370a6b --- /dev/null +++ b/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_step-fixture.feature @@ -0,0 +1,38 @@ +Feature: Attachment + + Scenario: attachment in a step, that is also a fixture + Given example.feature with content: + """ + Feature: Feature Test + Scenario: My Scenario Test + Given passed step + When when-step is fixture with attachment + Then passed step using fixture + """ + And py file with name: example_test + And with imports: pytest, pytest_bdd, allure + And with passed steps + And with func: + """ + @pytest.fixture() + @pytest_bdd.when("when-step is fixture with attachment") + def step_with_attachment(): + allure.attach('Attachment content', 'allure attachment', allure.attachment_type.TEXT) + """ + And with func: + """ + @pytest_bdd.then("passed step using fixture") + def then_step(step_with_attachment): + pass + """ + And test for My Scenario Test from example.feature + And py file saved + + When run pytest-bdd with allure + + Then attachment allure attachment must be in When when-step is fixture with attachment + And this attachment with content: + """ + Attachment content + """ + And attachments must not be in attachments \ No newline at end of file diff --git a/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_step.feature b/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_step.feature new file mode 100644 index 00000000..6aafebf6 --- /dev/null +++ b/allure-pytest-bdd/test/attachments_tests/attachments_features/attachment_in_step.feature @@ -0,0 +1,88 @@ +Feature: Attachments + + Scenario: Attachment in Given + Given example.feature with content: + """ + Feature: Feature Test + Scenario: My Scenario Test + Given step with attach + When passed step + Then passed step + """ + And py file with name: example_test + And with imports: pytest_bdd, allure + And with passed steps + And with func: + """ + @pytest_bdd.given("step with attach") + def step_with_attachment(): + allure.attach('Attachment content', 'allure attachment', allure.attachment_type.TEXT) + """ + And test for My Scenario Test from example.feature + And py file saved + + When run pytest-bdd with allure + + Then attachment allure attachment must be in Given step with attach + And this attachment with content: + """ + Attachment content + """ + + Scenario: Attachment in When + Given example.feature with content: + """ + Feature: Feature Test + Scenario: My Scenario Test + Given passed step + When step with attach + Then passed step + """ + And py file with name: example_test + And with imports: pytest_bdd, allure + And with passed steps + And with func: + """ + @pytest_bdd.when("step with attach") + def step_with_attachment(): + allure.attach('Attachment content', 'allure attachment', allure.attachment_type.TEXT) + """ + And test for My Scenario Test from example.feature + And py file saved + + When run pytest-bdd with allure + + Then attachment allure attachment must be in When step with attach + And this attachment with content: + """ + Attachment content + """ + + Scenario: Attachment in Then + Given example.feature with content: + """ + Feature: Feature Test + Scenario: My Scenario Test + Given passed step + When passed step + Then step with attach + """ + And py file with name: example_test + And with imports: pytest_bdd, allure + And with passed steps + And with func: + """ + @pytest_bdd.then("step with attach") + def step_with_attachment(): + allure.attach('Attachment content', 'allure attachment', allure.attachment_type.TEXT) + """ + And test for My Scenario Test from example.feature + And py file saved + + When run pytest-bdd with allure + + Then attachment allure attachment must be in Then step with attach + And this attachment with content: + """ + Attachment content + """ \ No newline at end of file diff --git a/allure-pytest-bdd/test/attachments_tests/conftest.py b/allure-pytest-bdd/test/attachments_tests/conftest.py new file mode 100644 index 00000000..2516dbed --- /dev/null +++ b/allure-pytest-bdd/test/attachments_tests/conftest.py @@ -0,0 +1,51 @@ +import os.path + +from pytest_bdd import then, parsers +import pytest + + +@pytest.fixture() +def test_context(): + return {"last checked attachment": None} + + +@then(parsers.re("this attachment with content:(?:\n)(?P[\\S|\\s]*)")) +def check_attachment_content(expected_content, test_context, testdir): + last_checked_attachment = test_context["last checked attachment"] + + file_path = os.path.join(testdir.tmpdir.strpath, last_checked_attachment["source"]) + with open(file_path, "r") as file: + actual_content = file.read() + + assert actual_content == expected_content + + +@then(parsers.parse("attachment {attachment_name} must be in {location_name}")) +def suitable_attachment(attachment_name, location_name, test_context, allure_report): + test_case_report = allure_report.test_cases[0] + + if location_name == "attachments": + attachments = test_case_report["attachments"] + else: + attachments = _get_step_report(test_case_report, location_name)["attachments"] + + suitable_attachments = [attachment for attachment in attachments + if attachment["name"] == attachment_name] + + assert len(suitable_attachments) == 1 + test_context["last checked attachment"] = suitable_attachments[0] + + +@then(parsers.parse("attachments must not be in {location_name}")) +def attachments_must_no_be_in(location_name, allure_report): + test_case_report = allure_report.test_cases[0] + + if location_name == "attachments": + assert "attachments" not in test_case_report.keys() + else: + assert "attachments" not in _get_step_report(test_case_report, location_name).keys() + + +def _get_step_report(test_case_report, step_name): + return next(step for step in test_case_report["steps"] + if step["name"] == step_name) diff --git a/allure-pytest-bdd/test/conftest.py b/allure-pytest-bdd/test/conftest.py index b925ac4d..0331298f 100644 --- a/allure-pytest-bdd/test/conftest.py +++ b/allure-pytest-bdd/test/conftest.py @@ -4,9 +4,13 @@ import allure_commons from allure_commons_test.report import AllureReport from allure_commons.logger import AllureFileLogger -from .steps import * # noqa F401 F403 +from .steps import * # noqa F401 F403 from pytest_bdd import given, when, parsers +from .py_file_builder import PyFileBuilder + +pytest_plugins = "pytester" + @contextmanager def fake_logger(path, logger): @@ -61,3 +65,61 @@ def feature_definition(name, extension, content, testdir): @when("run pytest-bdd with allure") def run(allured_testdir): allured_testdir.run_with_allure() + + +@pytest.fixture() +@given(parsers.parse("py file with name: {name}")) +def current_py_file_builder(name): + return PyFileBuilder(name) + + +@given(parsers.parse("with imports: {modules}")) +def add_imports_in_builder(modules, current_py_file_builder): + modules_names = [module.strip() for module in modules.split(",")] + current_py_file_builder.add_imports(*modules_names) + + +@given(parsers.re("with func:(?:\n)(?P[\\S|\\s]*)")) +def add_func_in_builder(content, current_py_file_builder): + current_py_file_builder.add_func(content) + + +@given("with passed steps") +def add_passed_steps(current_py_file_builder): + + passed_steps = '@pytest_bdd.given("passed step")\n' \ + '@pytest_bdd.when("passed step")\n' \ + '@pytest_bdd.then("passed step")\n' \ + 'def passed_step():\n' \ + ' pass' + + current_py_file_builder.add_func(passed_steps) + + +@given("with failed steps") +def add_failed_steps(current_py_file_builder): + + failed_steps = '@pytest_bdd.given("failed step")\n' \ + '@pytest_bdd.when("failed step")\n' \ + '@pytest_bdd.then("failed step")\n' \ + 'def failed_step():\n' \ + ' assert False' + + current_py_file_builder.add_func(failed_steps) + + +@given(parsers.parse("test for {scenario_name} from {feature_file}")) +def add_scenario_step(scenario_name, feature_file, current_py_file_builder): + + scenario_func = f'@pytest_bdd.scenario("{feature_file}", "{scenario_name}")\n' \ + 'def test_scenario():\n' \ + ' pass' + + current_py_file_builder.add_func(scenario_func) + + +@given(parsers.parse("py file saved")) +def save_py_file(current_py_file_builder, testdir): + testdir.makefile( + ".py", + **dict([(current_py_file_builder.name, current_py_file_builder.get_content())])) diff --git a/allure-pytest-bdd/features/background.feature b/allure-pytest-bdd/test/features/background.feature similarity index 100% rename from allure-pytest-bdd/features/background.feature rename to allure-pytest-bdd/test/features/background.feature diff --git a/allure-pytest-bdd/features/outline.feature b/allure-pytest-bdd/test/features/outline.feature similarity index 100% rename from allure-pytest-bdd/features/outline.feature rename to allure-pytest-bdd/test/features/outline.feature diff --git a/allure-pytest-bdd/features/scenario.feature b/allure-pytest-bdd/test/features/scenario.feature similarity index 100% rename from allure-pytest-bdd/features/scenario.feature rename to allure-pytest-bdd/test/features/scenario.feature diff --git a/allure-pytest-bdd/test/py_file_builder.py b/allure-pytest-bdd/test/py_file_builder.py new file mode 100644 index 00000000..570bedd8 --- /dev/null +++ b/allure-pytest-bdd/test/py_file_builder.py @@ -0,0 +1,29 @@ +class PyFileBuilder: + + def __init__(self, name): + self._import_line = None + self._file_funcs = [] + self.name = name + + def add_imports(self, *modules_names): + import_list = [] + + for module in modules_names: + import_list.append("import " + module) + + if len(import_list) != 0: + self._import_line = "\n".join(import_list) + + def add_func(self, str_func): + self._file_funcs.append(str_func) + + def get_content(self): + if len(self._file_funcs) == 0: + raise Exception("There are no functions in this file") + + content = "\n\n\n".join(self._file_funcs) + + if self._import_line is not None: + content = self._import_line + "\n\n\n" + content + + return content diff --git a/allure-pytest-bdd/test/py_file_builder_test.py b/allure-pytest-bdd/test/py_file_builder_test.py new file mode 100644 index 00000000..a2154939 --- /dev/null +++ b/allure-pytest-bdd/test/py_file_builder_test.py @@ -0,0 +1,146 @@ +from .py_file_builder import PyFileBuilder +import pytest + + +def test_common_func(): + imports = ["pytest", "pytest_bdd", "allure"] + funcs = [ + """@given("given_step") +def given_func(): + allure.attach("blah", ...) + raise Exception("message")""", + """@when("when_step") +def when_func(): + allure.attach("blah", ...) + raise Exception("message")""", + """@then("then_step") +def then_func(): + allure.attach("blah", ...) + raise Exception("message")""" + ] + + expected_answer = """import pytest +import pytest_bdd +import allure + + +@given("given_step") +def given_func(): + allure.attach("blah", ...) + raise Exception("message") + + +@when("when_step") +def when_func(): + allure.attach("blah", ...) + raise Exception("message") + + +@then("then_step") +def then_func(): + allure.attach("blah", ...) + raise Exception("message")""" + + file_builder = PyFileBuilder("test") + + file_builder.add_imports(*imports) + + for func in funcs: + file_builder.add_func(func) + + assert file_builder.get_content() == expected_answer + + +def test_without_imports_func(): + funcs = [ + """@given("given_step") +def given_func(): + allure.attach("blah", ...) + raise Exception("message")""", + """@when("when_step") +def when_func(): + allure.attach("blah", ...) + raise Exception("message")""", + """@then("then_step") +def then_func(): + allure.attach("blah", ...) + raise Exception("message")""" + ] + + expected_answer = """@given("given_step") +def given_func(): + allure.attach("blah", ...) + raise Exception("message") + + +@when("when_step") +def when_func(): + allure.attach("blah", ...) + raise Exception("message") + + +@then("then_step") +def then_func(): + allure.attach("blah", ...) + raise Exception("message")""" + + file_builder = PyFileBuilder("test") + + file_builder.add_imports() + + for func in funcs: + file_builder.add_func(func) + + assert file_builder.get_content() == expected_answer + + +def test_empty_func_str(): + funcs = [ + "", + """@when("when_step") +def when_func(): + allure.attach("blah", ...) + raise Exception("message")""", + """@then("then_step") +def then_func(): + allure.attach("blah", ...) + raise Exception("message")""" + ] + + expected_answer = """ + + +@when("when_step") +def when_func(): + allure.attach("blah", ...) + raise Exception("message") + + +@then("then_step") +def then_func(): + allure.attach("blah", ...) + raise Exception("message")""" + + file_builder = PyFileBuilder("test") + + file_builder.add_imports() + + for func in funcs: + file_builder.add_func(func) + + assert file_builder.get_content() == expected_answer + + +def test_have_no_added_funcs(): + imports = ["pytest", "pytest_bdd", "allure"] + funcs = [] + + file_builder = PyFileBuilder("test") + + file_builder.add_imports(*imports) + + for func in funcs: + file_builder.add_func(func) + + with pytest.raises(Exception): + file_builder.get_content() diff --git a/allure-pytest-bdd/test/outline_test.py b/allure-pytest-bdd/test/tests/outline_test.py similarity index 100% rename from allure-pytest-bdd/test/outline_test.py rename to allure-pytest-bdd/test/tests/outline_test.py diff --git a/allure-pytest-bdd/test/scenario_test.py b/allure-pytest-bdd/test/tests/scenario_test.py similarity index 100% rename from allure-pytest-bdd/test/scenario_test.py rename to allure-pytest-bdd/test/tests/scenario_test.py