1"""Submit failure or test session information to a pastebin service."""

2 import tempfile

3 from io import StringIO

4 from typing import IO

5 from typing import Union

6

7 import pytest

8 from _pytest.config import Config

9 from _pytest.config import create_terminal_writer

10 from _pytest.config.argparsing import Parser

11 from _pytest.stash import StashKey

12 from _pytest.terminal import TerminalReporter

13

14

15 pastebinfile_key = StashKey[IO[bytes]]()

16

17

18 def pytest_addoption(parser: Parser) -> None:

19 group = parser.getgroup("terminal reporting")

20 group._addoption(

21 "--pastebin",

22 metavar="mode",

23 action="store",

24 dest="pastebin",

25 default=None,

26 choices=["failed", "all"],

27 help="Send failed|all info to bpaste.net pastebin service",

28 )

29

30

31@pytest.hookimpl(trylast=True)

32 def pytest_configure(config: Config) -> None:

33 if config.option.pastebin == "all":

34 tr = config.pluginmanager.getplugin("terminalreporter")

35 # If no terminal reporter plugin is present, nothing we can do here;

36 # this can happen when this function executes in a worker node

37 # when using pytest-xdist, for example.

38 if tr is not None:

39 # pastebin file will be UTF-8 encoded binary file.

40 config.stash[pastebinfile_key] = tempfile.TemporaryFile("w+b")

41 oldwrite = tr._tw.write

42

43 def tee_write(s, **kwargs):

44 oldwrite(s, **kwargs)

45 if isinstance(s, str):

46 s = s.encode("utf-8")

47 config.stash[pastebinfile_key].write(s)

48

49 tr._tw.write = tee_write

50

51

52 def pytest_unconfigure(config: Config) -> None:

53 if pastebinfile_key in config.stash:

54 pastebinfile = config.stash[pastebinfile_key]

55 # Get terminal contents and delete file.

56 pastebinfile.seek(0)

57 sessionlog = pastebinfile.read()

58 pastebinfile.close()

59 del config.stash[pastebinfile_key]

60 # Undo our patching in the terminal reporter.

61 tr = config.pluginmanager.getplugin("terminalreporter")

62 del tr._tw.__dict__["write"]

63 # Write summary.

64 tr.write_sep("=", "Sending information to Paste Service")

65 pastebinurl = create_new_paste(sessionlog)

66 tr.write_line("pastebin session-log: %s\n" % pastebinurl)

67

68

69 def create_new_paste(contents: Union[str, bytes]) -> str:

70 """Create a new paste using the bpaste.net service.

71

72 :contents: Paste contents string.

73 :returns: URL to the pasted contents, or an error message.

74 """

75 import re

76 from urllib.request import urlopen

77 from urllib.parse import urlencode

78

79 params = {"code": contents, "lexer": "text", "expiry": "1week"}

80 url = "https://bpa.st"

81 try:

82 response: str = (

83 urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8")

84 )

85 except OSError as exc_info: # urllib errors

86 return "bad response: %s" % exc_info

87 m = re.search(r'href="/raw/(\w+)"', response)

88 if m:

89 return f"{url}/show/{m.group(1)}"

90 else:

91 return "bad response: invalid format ('" + response + "')"

92

93

94 def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None:

95 if terminalreporter.config.option.pastebin != "failed":

96 return

97 if "failed" in terminalreporter.stats:

98 terminalreporter.write_sep("=", "Sending information to Paste Service")

99 for rep in terminalreporter.stats["failed"]:

100 try:

101 msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc

102 except AttributeError:

103 msg = terminalreporter._getfailureheadline(rep)

104 file = StringIO()

105 tw = create_terminal_writer(terminalreporter.config, file)

106 rep.toterminal(tw)

107 s = file.getvalue()

108 assert len(s)

109 pastebinurl = create_new_paste(s)

110 terminalreporter.write_line(f"{msg} --> {pastebinurl}")