diff --git a/README.md b/README.md index 69fa626..3808d09 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,13 @@ All you need to do is add the `--httpdbg-allure` option to your pytest command l pytest ../httpdbg-docs/examples/ --alluredir=./allure-results --httpdbg-allure ``` -If an HTTP request is made by the test (or within a fixture, during the setup or teardown phase), the request will be saved in the Allure report under a step called `httpdbg`. +If an HTTP request is made by the test (or within a fixture, during the setup or teardown phase), a HTTP traces report will be saved in the Allure report under a step called `httpdbg`. -![](https://github.com/cle-b/pytest-httpdbg/blob/main/pytest-httpdbg-allure-0.8.0.png?raw=true) +### compact mode +![](https://github.com/cle-b/pytest-httpdbg/blob/main/pytest-httpdbg-allure-compact-0.10.0.png?raw=true) + +### full mode +![](https://github.com/cle-b/pytest-httpdbg/blob/main/pytest-httpdbg-allure-full-0.10.0.png?raw=true) ## Custom test report @@ -77,8 +81,6 @@ reporting: --httpdbg-no-clean do not clean the httpdbg directory --httpdbg-allure save HTTP(S) traces into the allure report - --httpdbg-no-headers do not save the HTTP headers - --httpdbg-no-binary do not save the HTTP payload if it's a binary content --httpdbg-only-on-failure save the HTTP requests only if the test failed --httpdbg-initiator=HTTPDBG_INITIATOR add a new initiator (package) for httpdbg diff --git a/pyproject.toml b/pyproject.toml index cda1f2e..c85f31d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ ] dynamic = ["version"] dependencies = [ - "httpdbg>=1.2.5", + "httpdbg>=2.1.1", "pytest>=7.0.0" ] diff --git a/pytest-httpdbg-allure-0.8.0.png b/pytest-httpdbg-allure-0.8.0.png deleted file mode 100644 index 8af71db..0000000 Binary files a/pytest-httpdbg-allure-0.8.0.png and /dev/null differ diff --git a/pytest-httpdbg-allure-compact-0.10.0.png b/pytest-httpdbg-allure-compact-0.10.0.png new file mode 100644 index 0000000..159722a Binary files /dev/null and b/pytest-httpdbg-allure-compact-0.10.0.png differ diff --git a/pytest-httpdbg-allure-full-0.10.0.png b/pytest-httpdbg-allure-full-0.10.0.png new file mode 100644 index 0000000..945b62d Binary files /dev/null and b/pytest-httpdbg-allure-full-0.10.0.png differ diff --git a/pytest_httpdbg/__init__.py b/pytest_httpdbg/__init__.py index 85bde42..258b15b 100644 --- a/pytest_httpdbg/__init__.py +++ b/pytest_httpdbg/__init__.py @@ -1,3 +1,3 @@ from pytest_httpdbg.plugin import httpdbg_record_filename # noqa F401 -__version__ = "0.9.1" +__version__ = "0.10.0" diff --git a/pytest_httpdbg/plugin.py b/pytest_httpdbg/plugin.py index 77da088..4dd5cec 100644 --- a/pytest_httpdbg/plugin.py +++ b/pytest_httpdbg/plugin.py @@ -1,12 +1,12 @@ import glob import os import time -import traceback from typing import Optional import pytest from httpdbg import httprecord +from httpdbg.export import generate_html httpdbg_record_filename = pytest.StashKey[str]() @@ -88,20 +88,6 @@ def pytest_addoption(parser): help="save HTTP(S) traces into the allure report", ) - reporting_group.addoption( - "--httpdbg-no-headers", - action="store_true", - default=False, - help="do not save the HTTP headers", - ) - - reporting_group.addoption( - "--httpdbg-no-binary", - action="store_true", - default=False, - help="do not save the HTTP payload if it's a binary content", - ) - reporting_group.addoption( "--httpdbg-only-on-failure", action="store_true", @@ -183,52 +169,6 @@ def pytest_sessionfinish(session, exitstatus): session.httpdbg_recorder.__exit__(None, None, None) -def get_allure_attachment_type_from_content_type(content_type: str): - try: - import allure - - content_type = content_type.split(";", 1)[0].strip() - - for attachment_type in allure.attachment_type: - if attachment_type.mime_type.lower() == content_type.lower(): - return attachment_type - except ImportError: - pass - return None - - -def req_resp_steps(label, req, save_headers, save_binary_payload): - try: - import allure - - # we generate the payload first because we do not want to add a step - # if there is no headers and no payload to save - content = req.preview - payload = None - if content.get("text"): - payload = content.get("text") - elif save_binary_payload: - payload = req.content - - if save_headers or payload: - with allure.step(label): - if save_headers: - allure.attach( - req.rawheaders.decode("utf-8"), - name="headers", - attachment_type=allure.attachment_type.TEXT, - ) - if payload: - attachment_type = get_allure_attachment_type_from_content_type( - content.get("content_type", "") - ) - allure.attach( - payload, name="payload", attachment_type=attachment_type - ) - except ImportError: - pass - - @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): @@ -246,73 +186,13 @@ def pytest_runtest_makereport(item, call): with allure.step("httpdbg"): - records = item.session.httpdbg_records - - for record in records: - - label = "" - - if record.response.status_code: - label += f"{record.response.status_code} " - - if record.request.method: - label += f"{record.request.method} " - - if record.request.uri: - url = record.request.uri - else: - url = record.url - if len(url) > 200: - url = url[:100] + "..." + url[-97:] - ex = ( - (str(type(record.exception)) + " ") - if record.exception is not None - else "" - ) - label += f"{ex}{url}" - - if record.tag: - label += f" (from {record.tag})" - - with allure.step(label): - details = record.url - details += f"\n\nstatus: {record.response.status_code} {record.response.message}" - details += f"\n\nstart: {record.tbegin.isoformat()}" - details += f"\nend: {record.last_update.isoformat()}" - - if record.initiator_id in records.initiators: - details += f"\n\n{records.initiators[record.initiator_id].short_stack}" - - if record.exception is not None: - details += ( - f"\n\nException: {type(record.exception)}\n" - ) - details += "".join( - traceback.format_exception( - type(record.exception), - record.exception, - record.exception.__traceback__, - ) - ) - - allure.attach( - details, - name="details", - attachment_type=allure.attachment_type.TEXT, - ) - - req_resp_steps( - "request", - record.request, - not item.config.option.httpdbg_no_headers, - not item.config.option.httpdbg_no_binary, - ) - req_resp_steps( - "response", - record.response, - not item.config.option.httpdbg_no_headers, - not item.config.option.httpdbg_no_binary, - ) + allure.attach( + generate_html( + item.session.httpdbg_records, for_export=True + ), + name="http traces", + attachment_type=allure.attachment_type.HTML, + ) except ImportError: pass diff --git a/tests/test_allure.py b/tests/test_allure.py index 2d9342a..4b152d1 100644 --- a/tests/test_allure.py +++ b/tests/test_allure.py @@ -1,9 +1,7 @@ import json -import allure import pytest -from pytest_httpdbg.plugin import get_allure_attachment_type_from_content_type confest_py = """ import pytest @@ -77,42 +75,18 @@ def test_post(httpbin, fixture_session, fixture_function): result = json.load(f) if result["name"] == "test_get": - - httpdbg_steps = [] - for step in result["steps"]: - if step["name"] == "httpdbg": - httpdbg_steps = step["steps"] - assert len(httpdbg_steps) == 4 - - step = httpdbg_steps[0] - assert step["name"] == "200 GET /get?setupsession (from fixture_session)" - step = httpdbg_steps[1] - assert step["name"] == "200 GET /get?setupfunction (from fixture_function)" - step = httpdbg_steps[2] - assert step["name"] == "200 GET /get" - step = httpdbg_steps[3] - assert ( - step["name"] == "200 GET /get?teardownfunction (from fixture_function)" - ) - - else: - - httpdbg_steps = [] for step in result["steps"]: if step["name"] == "httpdbg": - httpdbg_steps = step["steps"] - assert len(httpdbg_steps) == 4 - - step = httpdbg_steps[0] - assert step["name"] == "200 GET /get?setupfunction (from fixture_function)" - step = httpdbg_steps[1] - assert step["name"] == "200 POST /post" - step = httpdbg_steps[2] - assert ( - step["name"] == "200 GET /get?teardownfunction (from fixture_function)" - ) - step = httpdbg_steps[3] - assert step["name"] == "200 GET /get?teardownsession (from fixture_session)" + attachments = step["attachments"] + assert len(attachments) == 1 + assert attachments[0]["name"] == "http traces" + assert attachments[0]["type"] == "text/html" + with open(tmp_path / attachments[0]["source"]) as f: + htmlcontent = f.read() + assert "test_get" in htmlcontent + assert "/get?setupsession" in htmlcontent + assert "/get?setupfunction" in htmlcontent + assert "/get?teardownfunction" in htmlcontent def test_mode_allure_only_on_failure(pytester, tmp_path): @@ -146,16 +120,14 @@ def test_fail(httpbin): with open(result_file) as f: result = json.load(f) - httpdbg_steps = [] - for step in result.get("steps", []): - if step["name"] == "httpdbg": - httpdbg_steps = step["steps"] - if result["name"] == "test_pass": - assert len(httpdbg_steps) == 0 - else: - assert result["name"] == "test_fail" - assert len(httpdbg_steps) == 1 + assert "steps" not in result + + if result["name"] == "test_fail": + for step in result["steps"]: + if step["name"] == "httpdbg": + attachments = step.get("attachments", []) + assert len(attachments) == 1 def get_attachments_req_resp(result_file, name): @@ -181,274 +153,3 @@ def get_attachments_req_resp(result_file, name): filename_response = attachment["source"] return filename_request, filename_response - - -def test_mode_allure_with_headers(pytester, tmp_path): - - pytester.makepyfile( - """ - import requests - - def test_pass(httpbin): - requests.get(httpbin.url + "/get") - """ - ) - - result = pytester.runpytest("--httpdbg-allure", f"--alluredir={tmp_path}") - - result.assert_outcomes(passed=1) - - result_files = list(tmp_path.glob("*result.json")) - - assert len(result_files) == 1 - - headers_filename_request, headers_filename_response = get_attachments_req_resp( - result_files[0], "headers" - ) - - with open(tmp_path / headers_filename_request) as f: - assert "GET /get HTTP/1.1" in f.read() - - with open(tmp_path / headers_filename_response) as f: - assert "HTTP/1.1 200 OK" in f.read() - - -def test_mode_allure_without_headers(pytester, tmp_path): - - pytester.makepyfile( - """ - import requests - - def test_pass(httpbin): - requests.get(httpbin.url + "/get") - """ - ) - - result = pytester.runpytest( - "--httpdbg-allure", f"--alluredir={tmp_path}", "--httpdbg-no-headers" - ) - - result.assert_outcomes(passed=1) - - result_files = list(tmp_path.glob("*result.json")) - - assert len(result_files) == 1 - - headers_filename_request, headers_filename_response = get_attachments_req_resp( - result_files[0], "headers" - ) - - assert headers_filename_request is None - assert headers_filename_response is None - - -def test_mode_allure_payload(pytester, tmp_path): - - pytester.makepyfile( - """ - import requests - - def test_get(httpbin): - requests.get(httpbin.url + "/get") - - def test_post(httpbin): - requests.post(httpbin.url + "/post", data="hello") - - def test_binary(httpbin): - requests.get(httpbin.url + "/bytes/56") - - """ - ) - - result = pytester.runpytest("--httpdbg-allure", f"--alluredir={tmp_path}") - - result.assert_outcomes(passed=3) - - result_files = list(tmp_path.glob("*result.json")) - - assert len(result_files) == 3 - - for result_file in result_files: - - with open(result_file) as f: - test_name = json.load(f)["name"] - - if test_name == "test_get": - get_payload_request, get_payload_response = get_attachments_req_resp( - result_file, "payload" - ) - - if test_name == "test_post": - post_payload_request, post_payload_response = get_attachments_req_resp( - result_file, "payload" - ) - - if test_name == "test_binary": - binary_payload_request, binary_payload_response = get_attachments_req_resp( - result_file, "payload" - ) - - assert get_payload_request is None - with open(tmp_path / get_payload_response) as f: - payload = f.read() - assert "User-Agent" in payload - assert "/get" in payload - - with open(tmp_path / post_payload_request) as f: - payload = f.read() - assert "hello" in payload - with open(tmp_path / post_payload_response) as f: - payload = f.read() - assert "User-Agent" in payload - assert "/post" in payload - - assert binary_payload_request is None - with open(tmp_path / binary_payload_response, "rb") as f: - assert len(f.read()) == 56 - - -def test_mode_allure_payload_no_binary(pytester, tmp_path): - - pytester.makepyfile( - """ - import requests - - def test_get(httpbin): - requests.get(httpbin.url + "/get") - - def test_post(httpbin): - requests.post(httpbin.url + "/post", data="hello") - - def test_binary(httpbin): - requests.get(httpbin.url + "/bytes/56") - """ - ) - - result = pytester.runpytest( - "--httpdbg-allure", f"--alluredir={tmp_path}", "--httpdbg-no-binary" - ) - - result.assert_outcomes(passed=3) - - result_files = list(tmp_path.glob("*result.json")) - - assert len(result_files) == 3 - - for result_file in result_files: - - with open(result_file) as f: - test_name = json.load(f)["name"] - - if test_name == "test_get": - get_payload_request, get_payload_response = get_attachments_req_resp( - result_file, "payload" - ) - - if test_name == "test_post": - post_payload_request, post_payload_response = get_attachments_req_resp( - result_file, "payload" - ) - - if test_name == "test_binary": - binary_payload_request, binary_payload_response = get_attachments_req_resp( - result_file, "payload" - ) - - assert get_payload_request is None - assert get_payload_response is not None - - assert post_payload_request is not None - assert post_payload_response is not None - - assert binary_payload_request is None - assert binary_payload_request is None - - -def test_mode_allure_details(pytester, tmp_path): - - pytester.makepyfile( - """ - import requests - - def test_get(httpbin): - requests.get("http://127.0.0.1" + ":8345" + "/get") - """ - ) - - result = pytester.runpytest("--httpdbg-allure", f"--alluredir={tmp_path}") - - result.assert_outcomes(failed=1) - - result_files = list(tmp_path.glob("*result.json")) - - assert len(result_files) == 1 - - with open(result_files[0]) as f: - result = json.load(f) - - filename_payload = None - for step in result.get("steps", []): - if step["name"] == "httpdbg": - for attachment in step["steps"][0]["attachments"]: - if attachment["name"] == "details": - filename_payload = attachment["source"] - - with open(tmp_path / filename_payload) as f: - payload = f.read() - assert "http://127.0.0.1:8345/get" in payload - assert 'requests.get("http://127.0.0.1" + ":8345" + "/get")' in payload - - -def test_mode_allure_no_step_if_empty(pytester, tmp_path): - - pytester.makepyfile( - """ - import requests - - def test_binary(httpbin): - requests.get(httpbin.url + "/bytes/56") - """ - ) - - result = pytester.runpytest( - "--httpdbg-allure", - f"--alluredir={tmp_path}", - "--httpdbg-no-headers", - "--httpdbg-no-binary", - ) - - result.assert_outcomes(passed=1) - - result_files = list(tmp_path.glob("*result.json")) - - assert len(result_files) == 1 - - sub_steps = "should be None" - - with open(result_files[0]) as f: - result = json.load(f) - - for step in result.get("steps", []): - if step["name"] == "httpdbg": - sub_steps = step["steps"][0].get("steps") - - assert sub_steps is None - - -@pytest.mark.parametrize( - "content_type, attachment_type", - [ - ["application/json", allure.attachment_type.JSON], - ["application/JSON", allure.attachment_type.JSON], - ["application/json;charset=utf-8", allure.attachment_type.JSON], - ["application/json ; charset=utf-8", allure.attachment_type.JSON], - [ - "application/json;charset=utf-8,application/json;charset=utf-8", - allure.attachment_type.JSON, - ], - ["image/svg+xml", allure.attachment_type.SVG], - ["text/plain", allure.attachment_type.TEXT], - ], -) -def test_get_allure_attachment_type_from_content_type(content_type, attachment_type): - assert get_allure_attachment_type_from_content_type(content_type) == attachment_type