|
2 | 2 | import glob |
3 | 3 | import os |
4 | 4 | import time |
| 5 | +import traceback |
5 | 6 | from typing import Optional |
6 | 7 |
|
7 | 8 | import pytest |
@@ -62,27 +63,66 @@ def record_to_md(record, initiators): |
62 | 63 |
|
63 | 64 |
|
64 | 65 | def pytest_addoption(parser): |
65 | | - parser.addoption("--httpdbg", action="store_true", help="record HTTP(S) requests") |
66 | | - parser.addoption( |
| 66 | + |
| 67 | + reporting_group = parser.getgroup("reporting") |
| 68 | + |
| 69 | + # mode custom |
| 70 | + reporting_group.addoption( |
| 71 | + "--httpdbg", action="store_true", help="record HTTP(S) requests" |
| 72 | + ) |
| 73 | + |
| 74 | + reporting_group.addoption( |
67 | 75 | "--httpdbg-dir", type=str, default="", help="save httpdbg traces in a directory" |
68 | 76 | ) |
69 | | - parser.addoption( |
| 77 | + |
| 78 | + reporting_group.addoption( |
70 | 79 | "--httpdbg-no-clean", |
71 | 80 | action="store_true", |
72 | 81 | default=False, |
73 | 82 | help="do not clean the httpdbg directory", |
74 | 83 | ) |
75 | | - parser.addoption( |
| 84 | + |
| 85 | + # mode allure |
| 86 | + reporting_group.addoption( |
| 87 | + "--httpdbg-allure", |
| 88 | + action="store_true", |
| 89 | + help="save HTTP(S) traces into the allure report", |
| 90 | + ) |
| 91 | + |
| 92 | + reporting_group.addoption( |
| 93 | + "--httpdbg-no-headers", |
| 94 | + action="store_true", |
| 95 | + default=False, |
| 96 | + help="save the HTTP headers", |
| 97 | + ) |
| 98 | + |
| 99 | + reporting_group.addoption( |
| 100 | + "--httpdbg-no-binary", |
| 101 | + action="store_true", |
| 102 | + default=False, |
| 103 | + help="do not save the HTTP payload if it's a binary content", |
| 104 | + ) |
| 105 | + |
| 106 | + reporting_group.addoption( |
| 107 | + "--httpdbg-only-on-failure", |
| 108 | + action="store_true", |
| 109 | + default=False, |
| 110 | + help="save the HTTP requests only if the test failed", |
| 111 | + ) |
| 112 | + |
| 113 | + reporting_group.addoption( |
76 | 114 | "--httpdbg-initiator", |
77 | 115 | action="append", |
78 | 116 | help="add a new initiator (package) for httpdbg", |
79 | 117 | ) |
80 | 118 |
|
81 | 119 |
|
82 | 120 | def pytest_configure(config): |
83 | | - # add a flag to indicates to HTTPDBG to not set specific initiator |
84 | | - if config.option.httpdbg: |
85 | | - os.environ["HTTPDBG_PYTEST_PLUGIN"] = "1" |
| 121 | + |
| 122 | + if config.option.httpdbg is True and config.option.httpdbg_allure is True: |
| 123 | + pytest.exit( |
| 124 | + "Error: --httpdbg and --httpdbg-allure are mutually exclusive. Please specify only one." |
| 125 | + ) |
86 | 126 |
|
87 | 127 | # clean logs directory |
88 | 128 | httpdbg_dir = config.option.httpdbg_dir |
@@ -124,3 +164,152 @@ def pytest_runtest_protocol(item: pytest.Item, nextitem: Optional[pytest.Item]): |
124 | 164 | f.write(f"{record_to_md(record, records.initiators)}\n") |
125 | 165 | else: |
126 | 166 | yield |
| 167 | + |
| 168 | + |
| 169 | +# Allure mode: HTTP requests are recorded throughout the entire session and |
| 170 | +# saved in the Allure report at the test level. |
| 171 | +def pytest_sessionstart(session): |
| 172 | + if session.config.option.httpdbg_allure: |
| 173 | + session.httpdbg_recorder = httprecord( |
| 174 | + initiators=session.config.option.httpdbg_initiator |
| 175 | + ) |
| 176 | + session.httpdbg_records = session.httpdbg_recorder.__enter__() |
| 177 | + |
| 178 | + |
| 179 | +def pytest_sessionfinish(session, exitstatus): |
| 180 | + if session.config.option.httpdbg_allure: |
| 181 | + session.httpdbg_recorder.__exit__(None, None, None) |
| 182 | + |
| 183 | + |
| 184 | +def get_allure_attachment_type_from_content_type(content_type: str): |
| 185 | + try: |
| 186 | + import allure |
| 187 | + |
| 188 | + for attachment_type in allure.attachment_type: |
| 189 | + if attachment_type.mime_type == content_type: |
| 190 | + return attachment_type |
| 191 | + except ImportError: |
| 192 | + pass |
| 193 | + return None |
| 194 | + |
| 195 | + |
| 196 | +def req_resp_steps(label, req, save_headers, save_binary_payload): |
| 197 | + try: |
| 198 | + import allure |
| 199 | + |
| 200 | + # we generate the payload first because we do not want to add a step |
| 201 | + # if there is no headers and no payload to save |
| 202 | + content = req.preview |
| 203 | + payload = None |
| 204 | + if content.get("text"): |
| 205 | + payload = content.get("text") |
| 206 | + elif save_binary_payload: |
| 207 | + payload = req.content |
| 208 | + |
| 209 | + if save_headers or payload: |
| 210 | + with allure.step(label): |
| 211 | + if save_headers: |
| 212 | + allure.attach( |
| 213 | + req.rawheaders.decode("utf-8"), |
| 214 | + name="headers", |
| 215 | + attachment_type=allure.attachment_type.TEXT, |
| 216 | + ) |
| 217 | + if payload: |
| 218 | + attachment_type = get_allure_attachment_type_from_content_type( |
| 219 | + content.get("content_type") |
| 220 | + ) |
| 221 | + allure.attach( |
| 222 | + payload, name="payload", attachment_type=attachment_type |
| 223 | + ) |
| 224 | + except ImportError: |
| 225 | + pass |
| 226 | + |
| 227 | + |
| 228 | +@pytest.hookimpl(hookwrapper=True) |
| 229 | +def pytest_runtest_makereport(item, call): |
| 230 | + |
| 231 | + outcome = yield |
| 232 | + report = outcome.get_result() |
| 233 | + |
| 234 | + if item.config.option.httpdbg_allure: |
| 235 | + # we keep the information about the status of the test for all phases |
| 236 | + item.passed = getattr(item, "passed", True) and report.passed |
| 237 | + |
| 238 | + if report.when == "teardown": |
| 239 | + if (not item.config.option.httpdbg_only_on_failure) or (not item.passed): |
| 240 | + try: |
| 241 | + import allure |
| 242 | + |
| 243 | + with allure.step("httpdbg"): |
| 244 | + |
| 245 | + records = item.session.httpdbg_records |
| 246 | + |
| 247 | + for record in records: |
| 248 | + |
| 249 | + label = "" |
| 250 | + |
| 251 | + if record.response.status_code: |
| 252 | + label += f"{record.response.status_code} " |
| 253 | + |
| 254 | + if record.request.method: |
| 255 | + label += f"{record.request.method} " |
| 256 | + |
| 257 | + if record.request.uri: |
| 258 | + url = record.request.uri |
| 259 | + else: |
| 260 | + url = record.url |
| 261 | + if len(url) > 200: |
| 262 | + url = url[:100] + "..." + url[-97:] |
| 263 | + ex = ( |
| 264 | + (str(type(record.exception)) + " ") |
| 265 | + if record.exception is not None |
| 266 | + else "" |
| 267 | + ) |
| 268 | + label += f"{ex}{url}" |
| 269 | + |
| 270 | + if record.tag: |
| 271 | + label += f" (from {record.tag})" |
| 272 | + |
| 273 | + with allure.step(label): |
| 274 | + details = record.url |
| 275 | + details += f"\n\nstatus: {record.response.status_code} {record.response.message}" |
| 276 | + details += f"\n\nstart: {record.tbegin.isoformat()}" |
| 277 | + details += f"\nend: {record.last_update.isoformat()}" |
| 278 | + |
| 279 | + if record.initiator_id in records.initiators: |
| 280 | + details += f"\n\n{records.initiators[record.initiator_id].short_stack}" |
| 281 | + |
| 282 | + if record.exception is not None: |
| 283 | + details += ( |
| 284 | + f"\n\nException: {type(record.exception)}\n" |
| 285 | + ) |
| 286 | + details += "".join( |
| 287 | + traceback.format_exception( |
| 288 | + type(record.exception), |
| 289 | + record.exception, |
| 290 | + record.exception.__traceback__, |
| 291 | + ) |
| 292 | + ) |
| 293 | + |
| 294 | + allure.attach( |
| 295 | + details, |
| 296 | + name="details", |
| 297 | + attachment_type=allure.attachment_type.TEXT, |
| 298 | + ) |
| 299 | + |
| 300 | + req_resp_steps( |
| 301 | + "request", |
| 302 | + record.request, |
| 303 | + not item.config.option.httpdbg_no_headers, |
| 304 | + not item.config.option.httpdbg_no_binary, |
| 305 | + ) |
| 306 | + req_resp_steps( |
| 307 | + "response", |
| 308 | + record.response, |
| 309 | + not item.config.option.httpdbg_no_headers, |
| 310 | + not item.config.option.httpdbg_no_binary, |
| 311 | + ) |
| 312 | + except ImportError: |
| 313 | + pass |
| 314 | + |
| 315 | + item.session.httpdbg_records.reset() |
0 commit comments