pastebin.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. # mypy: allow-untyped-defs
  2. """Submit failure or test session information to a pastebin service."""
  3. from __future__ import annotations
  4. from io import StringIO
  5. import tempfile
  6. from typing import IO
  7. from _pytest.config import Config
  8. from _pytest.config import create_terminal_writer
  9. from _pytest.config.argparsing import Parser
  10. from _pytest.stash import StashKey
  11. from _pytest.terminal import TerminalReporter
  12. import pytest
  13. pastebinfile_key = StashKey[IO[bytes]]()
  14. def pytest_addoption(parser: Parser) -> None:
  15. group = parser.getgroup("terminal reporting")
  16. group.addoption(
  17. "--pastebin",
  18. metavar="mode",
  19. action="store",
  20. dest="pastebin",
  21. default=None,
  22. choices=["failed", "all"],
  23. help="Send failed|all info to bpaste.net pastebin service",
  24. )
  25. @pytest.hookimpl(trylast=True)
  26. def pytest_configure(config: Config) -> None:
  27. if config.option.pastebin == "all":
  28. tr = config.pluginmanager.getplugin("terminalreporter")
  29. # If no terminal reporter plugin is present, nothing we can do here;
  30. # this can happen when this function executes in a worker node
  31. # when using pytest-xdist, for example.
  32. if tr is not None:
  33. # pastebin file will be UTF-8 encoded binary file.
  34. config.stash[pastebinfile_key] = tempfile.TemporaryFile("w+b")
  35. oldwrite = tr._tw.write
  36. def tee_write(s, **kwargs):
  37. oldwrite(s, **kwargs)
  38. if isinstance(s, str):
  39. s = s.encode("utf-8")
  40. config.stash[pastebinfile_key].write(s)
  41. tr._tw.write = tee_write
  42. def pytest_unconfigure(config: Config) -> None:
  43. if pastebinfile_key in config.stash:
  44. pastebinfile = config.stash[pastebinfile_key]
  45. # Get terminal contents and delete file.
  46. pastebinfile.seek(0)
  47. sessionlog = pastebinfile.read()
  48. pastebinfile.close()
  49. del config.stash[pastebinfile_key]
  50. # Undo our patching in the terminal reporter.
  51. tr = config.pluginmanager.getplugin("terminalreporter")
  52. del tr._tw.__dict__["write"]
  53. # Write summary.
  54. tr.write_sep("=", "Sending information to Paste Service")
  55. pastebinurl = create_new_paste(sessionlog)
  56. tr.write_line(f"pastebin session-log: {pastebinurl}\n")
  57. def create_new_paste(contents: str | bytes) -> str:
  58. """Create a new paste using the bpaste.net service.
  59. :contents: Paste contents string.
  60. :returns: URL to the pasted contents, or an error message.
  61. """
  62. import re
  63. from urllib.error import HTTPError
  64. from urllib.parse import urlencode
  65. from urllib.request import urlopen
  66. params = {"code": contents, "lexer": "text", "expiry": "1week"}
  67. url = "https://bpa.st"
  68. try:
  69. response: str = (
  70. urlopen(url, data=urlencode(params).encode("ascii")).read().decode("utf-8")
  71. )
  72. except HTTPError as e:
  73. with e: # HTTPErrors are also http responses that must be closed!
  74. return f"bad response: {e}"
  75. except OSError as e: # eg urllib.error.URLError
  76. return f"bad response: {e}"
  77. m = re.search(r'href="/raw/(\w+)"', response)
  78. if m:
  79. return f"{url}/show/{m.group(1)}"
  80. else:
  81. return "bad response: invalid format ('" + response + "')"
  82. def pytest_terminal_summary(terminalreporter: TerminalReporter) -> None:
  83. if terminalreporter.config.option.pastebin != "failed":
  84. return
  85. if "failed" in terminalreporter.stats:
  86. terminalreporter.write_sep("=", "Sending information to Paste Service")
  87. for rep in terminalreporter.stats["failed"]:
  88. try:
  89. msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc
  90. except AttributeError:
  91. msg = terminalreporter._getfailureheadline(rep)
  92. file = StringIO()
  93. tw = create_terminal_writer(terminalreporter.config, file)
  94. rep.toterminal(tw)
  95. s = file.getvalue()
  96. assert len(s)
  97. pastebinurl = create_new_paste(s)
  98. terminalreporter.write_line(f"{msg} --> {pastebinurl}")