|
| 1 | +import os |
| 2 | +import datetime |
| 3 | +from itertools import zip_longest |
| 4 | +from robot.libraries.BuiltIn import BuiltIn |
| 5 | + |
1 | 6 | import allure_commons |
2 | 7 | from allure_commons.utils import now |
3 | 8 | from allure_commons.utils import uuid4 |
4 | | -from allure_commons.model2 import TestStepResult |
| 9 | +from allure_commons.utils import md5 |
| 10 | +from allure_commons.utils import platform_label |
| 11 | +from allure_commons.utils import host_tag |
| 12 | +from allure_commons.utils import format_exception, format_traceback |
| 13 | +from allure_commons.model2 import Label |
5 | 14 | from allure_commons.model2 import Status, StatusDetails |
6 | 15 | from allure_commons.model2 import Parameter |
7 | | -from allure_commons.utils import format_exception, format_traceback |
| 16 | +from allure_commons.types import LabelType, AttachmentType, Severity, LinkType |
| 17 | +from allure_robotframework.utils import get_allure_status |
| 18 | +from allure_robotframework.utils import get_allure_suites |
| 19 | +from allure_robotframework.utils import get_allure_parameters |
| 20 | +from allure_robotframework.utils import allure_labels, allure_links, allure_tags |
| 21 | +from allure_robotframework.types import RobotStatus, RobotLogLevel |
8 | 22 |
|
9 | 23 |
|
10 | 24 | def get_status(exception): |
@@ -21,27 +35,185 @@ def get_status_details(exc_type, exception, exc_traceback): |
21 | 35 | trace=format_traceback(exc_traceback)) |
22 | 36 |
|
23 | 37 |
|
| 38 | +def pool_id(): |
| 39 | + return BuiltIn().get_variable_value('${PABOTEXECUTIONPOOLID}') or "default" |
| 40 | + |
| 41 | + |
| 42 | +def get_message_time(timestamp): |
| 43 | + s_time = datetime.datetime.strptime(timestamp, "%Y%m%d %H:%M:%S.%f") |
| 44 | + return int(s_time.timestamp() * 1000) |
| 45 | + |
| 46 | + |
| 47 | +LOG_MESSAGE_FORMAT = '<p><b>[{level}]</b> {message}</p>' |
| 48 | +FAIL_MESSAGE_FORMAT = '<p style="color: red"><b>[{level}]</b> {message}</p>' |
| 49 | +MAX_STEP_MESSAGE_COUNT = int(os.getenv('ALLURE_MAX_STEP_MESSAGE_COUNT', 0)) |
| 50 | + |
| 51 | + |
24 | 52 | class AllureListener(object): |
25 | | - def __init__(self, logger): |
26 | | - self.logger = logger |
| 53 | + def __init__(self, lifecycle): |
| 54 | + self.lifecycle = lifecycle |
| 55 | + self._platform = platform_label() |
| 56 | + self._host = host_tag() |
| 57 | + self._current_msg = None |
| 58 | + self._current_tb = None |
| 59 | + |
| 60 | + def start_suite_container(self, name, attributes): |
| 61 | + with self.lifecycle.start_container(): |
| 62 | + pass |
| 63 | + |
| 64 | + def stop_suite_container(self, name, attributes): |
| 65 | + suite_status = get_allure_status(attributes.get('status')) |
| 66 | + suite_message = attributes.get('message') |
| 67 | + |
| 68 | + with self.lifecycle.update_container() as container: |
| 69 | + for uuid in container.children: |
| 70 | + with self.lifecycle.update_test_case(uuid) as test_result: |
| 71 | + if test_result and test_result.status == Status.PASSED and suite_message: |
| 72 | + test_result.status = suite_status |
| 73 | + test_result.statusDetails = StatusDetails(message=self._current_msg or suite_message, |
| 74 | + trace=self._current_tb) |
| 75 | + self.lifecycle.write_test_case(uuid) |
| 76 | + self._current_tb, self._current_msg = None, None |
| 77 | + self.lifecycle.write_container() |
| 78 | + |
| 79 | + def start_test_container(self, name, attributes): |
| 80 | + with self.lifecycle.start_container(): |
| 81 | + pass |
| 82 | + |
| 83 | + def stop_test_container(self, name, attributes): |
| 84 | + suite_status = get_allure_status(attributes.get('status')) |
| 85 | + suite_message = attributes.get('message') |
| 86 | + |
| 87 | + with self.lifecycle.schedule_test_case() as test_result: |
| 88 | + if test_result.status == Status.PASSED and suite_message: |
| 89 | + test_result.status = suite_status |
| 90 | + test_result.statusDetails = StatusDetails(message=self._current_msg or suite_message, |
| 91 | + trace=self._current_tb) |
| 92 | + |
| 93 | + self._current_tb, self._current_msg = None, None |
| 94 | + self.lifecycle.write_container() |
| 95 | + |
| 96 | + def start_before_fixture(self, name): |
| 97 | + with self.lifecycle.start_before_fixture() as fixture: |
| 98 | + fixture.name = name |
| 99 | + |
| 100 | + def stop_before_fixture(self, attributes, messages): |
| 101 | + self._report_messages(messages) |
| 102 | + with self.lifecycle.update_before_fixture() as fixture: |
| 103 | + fixture.status = get_allure_status(attributes.get('status')) |
| 104 | + fixture.statusDetails = StatusDetails(message=self._current_msg, trace=self._current_tb) |
| 105 | + self.lifecycle.stop_before_fixture() |
| 106 | + |
| 107 | + def start_after_fixture(self, name): |
| 108 | + with self.lifecycle.start_after_fixture() as fixture: |
| 109 | + fixture.name = name |
| 110 | + |
| 111 | + def stop_after_fixture(self, attributes, messages): |
| 112 | + self._report_messages(messages) |
| 113 | + with self.lifecycle.update_after_fixture() as fixture: |
| 114 | + fixture.status = get_allure_status(attributes.get('status')) |
| 115 | + fixture.statusDetails = StatusDetails(message=self._current_msg, trace=self._current_tb) |
| 116 | + self.lifecycle.stop_after_fixture() |
| 117 | + |
| 118 | + def start_test(self, name, attributes): |
| 119 | + uuid = uuid4() |
| 120 | + with self.lifecycle.schedule_test_case(uuid=uuid) as test_result: |
| 121 | + long_name = attributes.get('longname') |
| 122 | + test_result.name = name |
| 123 | + test_result.fullName = long_name |
| 124 | + test_result.historyId = md5(long_name) |
| 125 | + test_result.start = now() |
| 126 | + |
| 127 | + for container in self.lifecycle.containers(): |
| 128 | + container.children.append(uuid) |
| 129 | + |
| 130 | + def stop_test(self, _, attributes, messages): |
| 131 | + self._report_messages(messages) |
| 132 | + |
| 133 | + if 'skipped' in [tag.lower() for tag in attributes['tags']]: |
| 134 | + attributes['status'] = RobotStatus.SKIPPED |
| 135 | + |
| 136 | + with self.lifecycle.update_test_case() as test_result: |
| 137 | + test_result.stop = now() |
| 138 | + test_result.description = attributes.get('doc') |
| 139 | + test_result.status = get_allure_status(attributes.get('status')) |
| 140 | + test_result.labels.extend(get_allure_suites(attributes.get('longname'))) |
| 141 | + test_result.labels.append(Label(name=LabelType.FRAMEWORK, value='robotframework')) |
| 142 | + test_result.labels.append(Label(name=LabelType.LANGUAGE, value=self._platform)) |
| 143 | + test_result.labels.append(Label(name=LabelType.HOST, value=self._host)) |
| 144 | + test_result.labels.append(Label(name=LabelType.THREAD, value=pool_id())) |
| 145 | + test_result.labels.extend(allure_tags(attributes)) |
| 146 | + test_result.statusDetails = StatusDetails(message=self._current_msg or attributes.get('message'), |
| 147 | + trace=self._current_tb) |
| 148 | + |
| 149 | + if attributes.get('critical') == 'yes': |
| 150 | + test_result.labels.append(Label(name=LabelType.SEVERITY, value=Severity.CRITICAL)) |
| 151 | + |
| 152 | + for label_type in (LabelType.EPIC, LabelType.FEATURE, LabelType.STORY): |
| 153 | + test_result.labels.extend(allure_labels(attributes, label_type)) |
| 154 | + |
| 155 | + for link_type in (LinkType.ISSUE, LinkType.TEST_CASE, LinkType.LINK): |
| 156 | + test_result.links.extend(allure_links(attributes, link_type)) |
| 157 | + |
| 158 | + self._current_tb, self._current_msg = None, None |
| 159 | + |
| 160 | + def start_keyword(self, name): |
| 161 | + with self.lifecycle.start_step() as step: |
| 162 | + step.name = name |
| 163 | + |
| 164 | + def stop_keyword(self, attributes, messages): |
| 165 | + self._report_messages(messages) |
| 166 | + with self.lifecycle.update_step() as step: |
| 167 | + step.status = get_allure_status(attributes.get('status')) |
| 168 | + step.parameters = get_allure_parameters(attributes.get('args')) |
| 169 | + step.statusDetails = StatusDetails(message=self._current_msg, trace=self._current_tb) |
| 170 | + self.lifecycle.stop_step() |
| 171 | + |
| 172 | + def _report_messages(self, messages): |
| 173 | + has_trace = BuiltIn().get_variable_value("${LOG LEVEL}") in (RobotLogLevel.DEBUG, RobotLogLevel.TRACE) |
| 174 | + attachment = "" |
| 175 | + |
| 176 | + for message, next_message in zip_longest(messages, messages[1:]): |
| 177 | + name = message.get('message') |
| 178 | + level = message.get('level') |
| 179 | + message_format = FAIL_MESSAGE_FORMAT if level in RobotLogLevel.CRITICAL_LEVELS else LOG_MESSAGE_FORMAT |
| 180 | + |
| 181 | + if level == RobotLogLevel.FAIL: |
| 182 | + self._current_msg = name or self._current_msg |
| 183 | + self._current_tb = next_message.get("message") if has_trace and next_message else self._current_tb |
| 184 | + |
| 185 | + if len(messages) > MAX_STEP_MESSAGE_COUNT: |
| 186 | + attachment += message_format.format(level=level, message=name.replace('\n', '<br>')) |
| 187 | + else: |
| 188 | + with self.lifecycle.start_step() as step: |
| 189 | + step.name = name |
| 190 | + step.start = step.stop = get_message_time(message.get("timestamp")) |
| 191 | + step.status = Status.FAILED if level in RobotLogLevel.CRITICAL_LEVELS else Status.PASSED |
| 192 | + self.lifecycle.stop_step() |
| 193 | + |
| 194 | + if attachment: |
| 195 | + self.lifecycle.attach_data(uuid=uuid4(), body=attachment, name='Keyword Log', |
| 196 | + attachment_type=AttachmentType.HTML) |
27 | 197 |
|
28 | 198 | @allure_commons.hookimpl |
29 | 199 | def attach_data(self, body, name, attachment_type, extension): |
30 | | - self.logger.attach_data(uuid4(), body, name=name, attachment_type=attachment_type, extension=extension) |
| 200 | + self.lifecycle.attach_data(uuid4(), body, name=name, attachment_type=attachment_type, extension=extension) |
31 | 201 |
|
32 | 202 | @allure_commons.hookimpl |
33 | 203 | def attach_file(self, source, name, attachment_type, extension): |
34 | | - self.logger.attach_file(uuid4(), source, name=name, attachment_type=attachment_type, extension=extension) |
| 204 | + self.lifecycle.attach_file(uuid4(), source, name=name, attachment_type=attachment_type, extension=extension) |
35 | 205 |
|
36 | 206 | @allure_commons.hookimpl |
37 | 207 | def start_step(self, uuid, title, params): |
38 | | - parameters = [Parameter(name=name, value=value) for name, value in params.items()] |
39 | | - step = TestStepResult(name=title, start=now(), parameters=parameters) |
40 | | - self.logger.start_step(None, uuid, step) |
| 208 | + with self.lifecycle.start_step() as step: |
| 209 | + step.name = title |
| 210 | + step.start = now() |
| 211 | + step.parameters = [Parameter(name=name, value=value) for name, value in params.items()] |
41 | 212 |
|
42 | 213 | @allure_commons.hookimpl |
43 | 214 | def stop_step(self, uuid, exc_type, exc_val, exc_tb): |
44 | | - self.logger.stop_step(uuid, |
45 | | - stop=now(), |
46 | | - status=get_status(exc_val), |
47 | | - statusDetails=get_status_details(exc_type, exc_val, exc_tb)) |
| 215 | + with self.lifecycle.update_step() as step: |
| 216 | + step.stop = now() |
| 217 | + step.status = get_status(exc_val) |
| 218 | + step.statusDetails = get_status_details(exc_type, exc_val, exc_tb) |
| 219 | + self.lifecycle.stop_step() |