conftest.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. import os
  2. from collections.abc import MutableMapping
  3. import psutil
  4. import pytest
  5. import responses
  6. from django.db import connections
  7. from sentry.silo import SiloMode
  8. from sentry.testutils.pytest.sentry import get_default_silo_mode_for_test_cases
  9. pytest_plugins = ["sentry.testutils.pytest"]
  10. # XXX: The below code is vendored code from https://github.com/utgwkk/pytest-github-actions-annotate-failures
  11. # so that we can add support for pytest_rerunfailures
  12. # retried tests will no longer be annotated in GHA
  13. #
  14. # Reference:
  15. # https://docs.pytest.org/en/latest/writing_plugins.html#hookwrapper-executing-around-other-hooks
  16. # https://docs.pytest.org/en/latest/writing_plugins.html#hook-function-ordering-call-example
  17. # https://docs.pytest.org/en/stable/reference.html#pytest.hookspec.pytest_runtest_makereport
  18. #
  19. # Inspired by:
  20. # https://github.com/pytest-dev/pytest/blob/master/src/_pytest/terminal.py
  21. @pytest.fixture(autouse=True)
  22. def unclosed_files():
  23. fds = frozenset(psutil.Process().open_files())
  24. yield
  25. assert frozenset(psutil.Process().open_files()) == fds
  26. @pytest.hookimpl(tryfirst=True, hookwrapper=True)
  27. def pytest_runtest_makereport(item, call):
  28. # execute all other hooks to obtain the report object
  29. outcome = yield
  30. report = outcome.get_result()
  31. # enable only in a workflow of GitHub Actions
  32. # ref: https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables#default-environment-variables
  33. if os.environ.get("GITHUB_ACTIONS") != "true":
  34. return
  35. # If we have the pytest_rerunfailures plugin,
  36. # and there are still retries to be run,
  37. # then do not return the error
  38. if hasattr(item, "execution_count"):
  39. import pytest_rerunfailures
  40. if item.execution_count <= pytest_rerunfailures.get_reruns_count(item):
  41. return
  42. if report.when == "call" and report.failed:
  43. # collect information to be annotated
  44. filesystempath, lineno, _ = report.location
  45. # try to convert to absolute path in GitHub Actions
  46. workspace = os.environ.get("GITHUB_WORKSPACE")
  47. if workspace:
  48. full_path = os.path.abspath(filesystempath)
  49. try:
  50. rel_path = os.path.relpath(full_path, workspace)
  51. except ValueError:
  52. # os.path.relpath() will raise ValueError on Windows
  53. # when full_path and workspace have different mount points.
  54. # https://github.com/utgwkk/pytest-github-actions-annotate-failures/issues/20
  55. rel_path = filesystempath
  56. if not rel_path.startswith(".."):
  57. filesystempath = rel_path
  58. if lineno is not None:
  59. # 0-index to 1-index
  60. lineno += 1
  61. # get the name of the current failed test, with parametrize info
  62. longrepr = report.head_line or item.name
  63. # get the error message and line number from the actual error
  64. try:
  65. longrepr += "\n\n" + report.longrepr.reprcrash.message
  66. lineno = report.longrepr.reprcrash.lineno
  67. except AttributeError:
  68. pass
  69. print(_error_workflow_command(filesystempath, lineno, longrepr)) # noqa: S002
  70. def _error_workflow_command(filesystempath, lineno, longrepr):
  71. # Build collection of arguments. Ordering is strict for easy testing
  72. details_dict = {"file": filesystempath}
  73. if lineno is not None:
  74. details_dict["line"] = lineno
  75. details = ",".join(f"{k}={v}" for k, v in details_dict.items())
  76. if longrepr is None:
  77. return f"\n::error {details}"
  78. else:
  79. longrepr = _escape(longrepr)
  80. return f"\n::error {details}::{longrepr}"
  81. def _escape(s):
  82. return s.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
  83. @pytest.fixture(autouse=True)
  84. def validate_silo_mode():
  85. # NOTE! Hybrid cloud uses many mechanisms to simulate multiple different configurations of the application
  86. # during tests. It depends upon `override_settings` using the correct contextmanager behaviors and correct
  87. # thread handling in acceptance tests. If you hit one of these, it's possible either that cleanup logic has
  88. # a bug, or you may be using a contextmanager incorrectly. Let us know and we can help!
  89. expected = get_default_silo_mode_for_test_cases()
  90. message = (
  91. f"Possible test leak bug! SiloMode was not reset to {expected} between tests. "
  92. "Please read the comment for validate_silo_mode() in tests/conftest.py."
  93. )
  94. if SiloMode.get_current_mode() != expected:
  95. raise Exception(message)
  96. yield
  97. if SiloMode.get_current_mode() != expected:
  98. raise Exception(message)
  99. @pytest.fixture(autouse=True)
  100. def setup_simulate_on_commit(request):
  101. from sentry.testutils.hybrid_cloud import simulate_on_commit
  102. with simulate_on_commit(request):
  103. yield
  104. @pytest.fixture(autouse=True)
  105. def setup_enforce_monotonic_transactions(request):
  106. from sentry.testutils.hybrid_cloud import enforce_no_cross_transaction_interactions
  107. with enforce_no_cross_transaction_interactions():
  108. yield
  109. @pytest.fixture(autouse=True)
  110. def audit_hybrid_cloud_writes_and_deletes(request):
  111. """
  112. Ensure that write operations on hybrid cloud foreign keys are recorded
  113. alongside outboxes or use a context manager to indicate that the
  114. caller has considered outbox and didn't accidentally forget.
  115. Generally you can avoid assertion errors from these checks by:
  116. 1. Running deletion/write logic within an `outbox_context`.
  117. 2. Using Model.delete()/save methods that create outbox messages in the
  118. same transaction as a delete operation.
  119. Scenarios that are generally always unsafe are using
  120. `QuerySet.delete()`, `QuerySet.update()` or raw SQL to perform
  121. writes.
  122. The User.delete() method is a good example of how to safely
  123. delete records and generate outbox messages.
  124. """
  125. from sentry.testutils.silo import validate_protected_queries
  126. debug_cursor_state: MutableMapping[str, bool] = {}
  127. for conn in connections.all():
  128. debug_cursor_state[conn.alias] = conn.force_debug_cursor
  129. conn.queries_log.clear()
  130. conn.force_debug_cursor = True
  131. try:
  132. yield
  133. finally:
  134. for conn in connections.all():
  135. conn.force_debug_cursor = debug_cursor_state[conn.alias]
  136. validate_protected_queries(conn.queries)
  137. @pytest.fixture(autouse=True)
  138. def check_leaked_responses_mocks():
  139. yield
  140. leaked = responses.registered()
  141. if leaked:
  142. responses.reset()
  143. leaked_s = "".join(f"- {item}\n" for item in leaked)
  144. raise AssertionError(
  145. f"`responses` were leaked outside of the test context:\n{leaked_s}"
  146. f"(make sure to use `@responses.activate` or `with responses.mock:`)"
  147. )