From d47b9d04d4cf824150caef46c9c888779c1b3f58 Mon Sep 17 00:00:00 2001 From: Michael Goerz Date: Sun, 18 Aug 2019 13:32:46 -0400 Subject: [PATCH] Gracefully handle HTTP errors from pastebin We find that the --pastebin option to pytest sometimes fails with "HTTP Error 400: Bad Request". We're still investigating the exact cause of these errors, but in the meantime, a failure to upload to the pastebin service should probably not crash pytest and cause a test failure in the continuous-integration. This patch catches exceptions like HTTPError that may be thrown while trying to communicate with the pastebin service, and reports them as a "bad response", without crashing with a backtrace or failing the entire test suite. --- AUTHORS | 1 + changelog/5764.feature.rst | 1 + src/_pytest/pastebin.py | 13 ++++++--- testing/test_pastebin.py | 56 +++++++++++++++++++++++++++++++++++++- 4 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 changelog/5764.feature.rst diff --git a/AUTHORS b/AUTHORS index 88bbfe3527b..6d1a2a81606 100644 --- a/AUTHORS +++ b/AUTHORS @@ -173,6 +173,7 @@ mbyt Michael Aquilina Michael Birtwell Michael Droettboom +Michael Goerz Michael Seifert Michal Wajszczuk Mihai Capotă diff --git a/changelog/5764.feature.rst b/changelog/5764.feature.rst new file mode 100644 index 00000000000..3ac77b8fe7d --- /dev/null +++ b/changelog/5764.feature.rst @@ -0,0 +1 @@ +New behavior of the ``--pastebin`` option: failures to connect to the pastebin server are reported, without failing the pytest run diff --git a/src/_pytest/pastebin.py b/src/_pytest/pastebin.py index 91aa5f1fdcb..38ff97f2ddd 100644 --- a/src/_pytest/pastebin.py +++ b/src/_pytest/pastebin.py @@ -59,7 +59,7 @@ def create_new_paste(contents): Creates a new paste using bpaste.net service. :contents: paste contents as utf-8 encoded bytes - :returns: url to the pasted contents + :returns: url to the pasted contents or error message """ import re from urllib.request import urlopen @@ -67,12 +67,17 @@ def create_new_paste(contents): params = {"code": contents, "lexer": "python3", "expiry": "1week"} url = "https://bpaste.net" - response = urlopen(url, data=urlencode(params).encode("ascii")).read() - m = re.search(r'href="/raw/(\w+)"', response.decode("utf-8")) + try: + response = ( + urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8") + ) + except OSError as exc_info: # urllib errors + return "bad response: %s" % exc_info + m = re.search(r'href="/raw/(\w+)"', response) if m: return "{}/show/{}".format(url, m.group(1)) else: - return "bad response: " + response.decode("utf-8") + return "bad response: invalid format ('" + response + "')" def pytest_terminal_summary(terminalreporter): diff --git a/testing/test_pastebin.py b/testing/test_pastebin.py index 4e8bac56cb2..a1bc0622eb3 100644 --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -82,6 +82,47 @@ class TestPaste: def pastebin(self, request): return request.config.pluginmanager.getplugin("pastebin") + @pytest.fixture + def mocked_urlopen_fail(self, monkeypatch): + """ + monkeypatch the actual urlopen call to emulate a HTTP Error 400 + """ + calls = [] + + import urllib.error + import urllib.request + + def mocked(url, data): + calls.append((url, data)) + raise urllib.error.HTTPError(url, 400, "Bad request", None, None) + + monkeypatch.setattr(urllib.request, "urlopen", mocked) + return calls + + @pytest.fixture + def mocked_urlopen_invalid(self, monkeypatch): + """ + monkeypatch the actual urlopen calls done by the internal plugin + function that connects to bpaste service, but return a url in an + unexpected format + """ + calls = [] + + def mocked(url, data): + calls.append((url, data)) + + class DummyFile: + def read(self): + # part of html of a normal response + return b'View raw.' + + return DummyFile() + + import urllib.request + + monkeypatch.setattr(urllib.request, "urlopen", mocked) + return calls + @pytest.fixture def mocked_urlopen(self, monkeypatch): """ @@ -105,6 +146,19 @@ def read(self): monkeypatch.setattr(urllib.request, "urlopen", mocked) return calls + def test_pastebin_invalid_url(self, pastebin, mocked_urlopen_invalid): + result = pastebin.create_new_paste(b"full-paste-contents") + assert ( + result + == "bad response: invalid format ('View raw.')" + ) + assert len(mocked_urlopen_invalid) == 1 + + def test_pastebin_http_error(self, pastebin, mocked_urlopen_fail): + result = pastebin.create_new_paste(b"full-paste-contents") + assert result == "bad response: HTTP Error 400: Bad request" + assert len(mocked_urlopen_fail) == 1 + def test_create_new_paste(self, pastebin, mocked_urlopen): result = pastebin.create_new_paste(b"full-paste-contents") assert result == "https://bpaste.net/show/3c0c6750bd" @@ -127,4 +181,4 @@ def response(url, data): monkeypatch.setattr(urllib.request, "urlopen", response) result = pastebin.create_new_paste(b"full-paste-contents") - assert result == "bad response: something bad occurred" + assert result == "bad response: invalid format ('something bad occurred')"