Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gracefully handle HTTP errors from pastebin #5764

Merged
merged 1 commit into from
Aug 30, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ mbyt
Michael Aquilina
Michael Birtwell
Michael Droettboom
Michael Goerz
Michael Seifert
Michal Wajszczuk
Mihai Capotă
Expand Down
1 change: 1 addition & 0 deletions changelog/5764.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
New behavior of the ``--pastebin`` option: failures to connect to the pastebin server are reported, without failing the pytest run
13 changes: 9 additions & 4 deletions src/_pytest/pastebin.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,20 +59,25 @@ 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
from urllib.parse import urlencode

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should silently gobble the errors here, if someone requests --pastebin and pytest fails to pastebin then the command should probably exit nonzero

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our use-case is a home-spun continuous integration script for https://www.qdyn-library.net. This project is hosted on a gitolite installation on a university workstation running commercial Fortran compilers, which also acts as the CI server via a simple post-commit hook kicking off a SLURM job to run the test matrix. Everything is totally headless and not web-based, so the CI-server sends developers an email with the results of the CI. These only include a summary, the detailed test output tends to be a bit too large to send around by email, so the CI-server uses --pastebin to upload the output, and puts a link in the email.

Recently, this started failing for some test runs with an HTTP 400 error. We're not quite sure what exactly the cause is. In any case, having the full test output available is very much secondary to the actual tests passing or failing. We don't want to get a "test failure" just because the upload to pastebin failed for some reason. After all, it might just be a temporary network failure. It would be enough to have the appropriate error message in the email (as this PR implements). We can then still investigate what the problem is, but a successful upload to pastebin is no longer a necessary condition for accepting patches. So to "silently gobble the errors" is explicitly the point here. The error will still be logged to the output, so it's not totally silent. Of course, this behavior is debatable. It's just that with the current behavior of pytest failing if pastebin fails, we'd probably have to stop using that feature and re-implement it as a pytest-plugin that matches our use case; that would obviously be more work than this small patch.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A possible compromise might be to exit with a specific status code that indicates "Everything OK except for the --pastebin". The CI-script might be able to catch that as a special case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm OK with catching and displaying the error instead of returning non-zero. I think most (all?) use cases for --pastebin follow the pattern of "posting to pastebin for a nice-to-have report" but that is not really required.

Either way, this should target features as it changes the behavior. 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So should I rename changelog/5764.improvement.rst to changelog/5764.feature.rst and reword the changelog text?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the base branch from master to features just now.

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):
Expand Down
56 changes: 55 additions & 1 deletion testing/test_pastebin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="/invalid/3c0c6750bd">raw</a>.'

return DummyFile()

import urllib.request

monkeypatch.setattr(urllib.request, "urlopen", mocked)
return calls

@pytest.fixture
def mocked_urlopen(self, monkeypatch):
"""
Expand All @@ -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 <a href=\"/invalid/3c0c6750bd\">raw</a>.')"
)
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"
Expand All @@ -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')"