class PytestBDDListener(object): def __init__(self): self.lifecycle = AllureLifecycle() self.host = host_tag() self.thread = thread_tag() def _scenario_finalizer(self, scenario): for step in scenario.steps: step_uuid = get_uuid(str(id(step))) with self.lifecycle.update_step(uuid=step_uuid) as step_result: if step_result: step_result.status = Status.SKIPPED self.lifecycle.stop_step(uuid=step_uuid) @pytest.hookimpl def pytest_bdd_before_scenario(self, request, feature, scenario): uuid = get_uuid(request.node.nodeid) full_name = get_full_name(feature, scenario) name = get_name(request.node, scenario) with self.lifecycle.schedule_test_case(uuid=uuid) as test_result: test_result.fullName = full_name test_result.name = name test_result.start = now() 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-bdd")) test_result.labels.append(Label(name=LabelType.LANGUAGE, value=platform_label())) test_result.labels.append(Label(name=LabelType.FEATURE, value=feature.name)) test_result.parameters = get_params(request.node) finalizer = partial(self._scenario_finalizer, scenario) request.node.addfinalizer(finalizer) @pytest.hookimpl def pytest_bdd_after_scenario(self, request, feature, scenario): uuid = get_uuid(request.node.nodeid) with self.lifecycle.update_test_case(uuid=uuid) as test_result: test_result.stop = now() @pytest.hookimpl def pytest_bdd_before_step_call(self, request, feature, scenario, step, step_func, step_func_args): parent_uuid = get_uuid(request.node.nodeid) uuid = get_uuid(str(id(step))) with self.lifecycle.start_step(parent_uuid=parent_uuid, uuid=uuid) as step_result: step_result.name = get_step_name(request.node, step) @pytest.hookimpl def pytest_bdd_after_step(self, request, feature, scenario, step, step_func, step_func_args): uuid = get_uuid(str(id(step))) with self.lifecycle.update_step(uuid=uuid) as step_result: step_result.status = Status.PASSED self.lifecycle.stop_step(uuid=uuid) @pytest.hookimpl def pytest_bdd_step_error(self, request, feature, scenario, step, step_func, step_func_args, exception): uuid = get_uuid(str(id(step))) with self.lifecycle.update_step(uuid=uuid) as step_result: step_result.status = Status.FAILED step_result.statusDetails = get_status_details(exception) self.lifecycle.stop_step(uuid=uuid) @pytest.hookimpl def pytest_bdd_step_func_lookup_error(self, request, feature, scenario, step, exception): uuid = get_uuid(str(id(step))) with self.lifecycle.update_step(uuid=uuid) as step_result: step_result.status = Status.BROKEN self.lifecycle.stop_step(uuid=uuid) @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(self, item, call): report = (yield).get_result() status = get_pytest_report_status(report) status_details = StatusDetails( message=call.excinfo.exconly(), trace=report.longreprtext) if call.excinfo else None uuid = get_uuid(report.nodeid) with self.lifecycle.update_test_case(uuid=uuid) as test_result: if test_result and report.when == "setup": test_result.status = status test_result.statusDetails = status_details if report.when == "call" and test_result: if test_result.status not in [Status.PASSED, Status.FAILED]: test_result.status = status test_result.statusDetails = status_details if report.when == "teardown" and test_result: if test_result.status == Status.PASSED and status != Status.PASSED: test_result.status = status test_result.statusDetails = status_details if report.when == 'teardown': self.lifecycle.write_test_case(uuid=uuid) @allure_commons.hookimpl def attach_data(self, body, name, attachment_type, extension): self.lifecycle.attach_data(uuid4(), body, name=name, attachment_type=attachment_type, extension=extension) @allure_commons.hookimpl def attach_file(self, source, name, attachment_type, extension): self.lifecycle.attach_file(uuid4(), source, name=name, attachment_type=attachment_type, extension=extension)
class AllureWriter: def __init__(self, results_path): safe_makedirs(results_path) self.lifecycle = AllureLifecycle() self.logger = AllureFileLogger(results_path) self.listener = AllureListener(self.lifecycle) def process(self, structured_data, file_modication_datetime=None): plugin_manager.register(self.listener) plugin_manager.register(self.logger) for test_case in structured_data["tests"]: self.process_test_case( test_case, file_modication_datetime=file_modication_datetime) plugin_manager.unregister(plugin=self.listener) plugin_manager.unregister(plugin=self.logger) def process_test_case(self, test_case, file_modication_datetime=None): with self.lifecycle.schedule_test_case() as test_result: test_index = test_case["id"] test_data = test_case.get("data") or {} job = test_data.get("job") or {} test_result.name = test_index self._record_start_stop(test_result, file_modication_datetime, job) test_result.fullName = test_index test_result.testCaseId = md5(test_index) test_result.historyId = md5(test_index) tool_id = self._record_suite_labels(test_result, test_data, job) self._attach_data("test_data", json.dumps(test_data, indent=JSON_INDENT), attachment_type=AttachmentType.JSON) for key in [ "stderr", "stdout", "command_line", "external_id", "job_messages" ]: val = job.get(key) if not val: continue if isinstance(val, list): attachment_type = AttachmentType.JSON # job messages val = json.dumps(val, indent=JSON_INDENT) else: if not val.strip(): continue attachment_type = AttachmentType.TEXT self._attach_data(key, val, attachment_type=attachment_type) problem_message = None for key in ["execution_problem", "output_problems"]: val = test_data.get(key) if not val: continue if isinstance(val, list) and val: # remove duplicated messages... val = list(set(val)) attachment_type = AttachmentType.HTML as_html_list = "<ul>" as_html_list += "\n".join( [f"<li><pre>{v}</pre></li>" for v in val]) as_html_list += "</ul>" problem_message = val[0] val = as_html_list else: if not val.strip(): continue attachment_type = AttachmentType.TEXT problem_message = val self._attach_data(key, val, attachment_type=attachment_type) if problem_message is None and "job_messages" in job: job_messages = job.get("job_messages") if job_messages: problem_message = str(job_messages) test_result.labels.append( Label(name=LabelType.FRAMEWORK, value='planemo')) test_result.labels.append( Label(name=LabelType.LANGUAGE, value=platform_label())) self._record_tool_link(test_result, tool_id) self._record_status(test_result, test_data) if test_result.status in [Status.BROKEN, Status.FAILED]: test_result.statusDetails = StatusDetails( message=escape_non_unicode_symbols(problem_message or "Unknown problem"), trace=None) self.lifecycle.write_test_case() def _record_start_stop(self, test_result, file_modication_datetime, job): start_datetime = file_modication_datetime end_datetime = file_modication_datetime if "create_time" in job: start_datetime = parser.parse(job["create_time"]) if "update_time" in job: end_datetime = parser.parse(job["update_time"]) if start_datetime is not None: test_result.start = int(round(start_datetime.timestamp() * 1000)) if end_datetime is not None: test_result.stop = int(round(end_datetime.timestamp() * 1000)) def _record_suite_labels(self, test_result, test_data, job): tool_id = None if "tool_id" in test_data: tool_id = test_data["tool_id"] test_result.labels.append( Label(name=LabelType.PARENT_SUITE, value=tool_id)) elif "tool_id" in job: tool_id = job["tool_id"] test_result.labels.append( Label(name=LabelType.PARENT_SUITE, value=tool_id)) if "tool_version" in test_data: test_result.labels.append( Label(name=LabelType.SUITE, value=test_data["tool_version"])) elif "tool_version" in job: test_result.labels.append( Label(name=LabelType.SUITE, value=job["tool_version"])) if "test_index" in test_data: test_result.labels.append( Label(name=LabelType.SUB_SUITE, value=str(test_data["test_index"]))) return tool_id def _record_tool_link(self, test_result, tool_id): if tool_id and 'repos' in tool_id: tool_parts = tool_id.split("/") if len(tool_parts) >= 4: link = Link(LinkType.LINK, "https://%s" % "/".join(tool_parts[0:4]), "Tool Repository") test_result.links.append(link) def _record_status(self, test_result, test_data): status = test_data.get("status", "error") if status == "success": test_result.status = Status.PASSED elif status == "failure": test_result.status = Status.FAILED elif status == "skip": test_result.status = Status.SKIPPED else: test_result.status = Status.BROKEN def _attach_data(self, key, val, attachment_type=AttachmentType.TEXT): self.lifecycle.attach_data(uuid4(), val, name=key, attachment_type=attachment_type, extension=None)