Skip to content

Commit f4ba853

Browse files
ref: replace monkeypatch with unittest.mock (#96714)
the monkeypatch fixture has not-well-defined scope compared to context managers. it also can produce unexpected results when repeatedly called on the same target potentially leading to test pollution getsentry portion: getsentry/getsentry#18043 <!-- Describe your PR here. -->
1 parent 3963c86 commit f4ba853

23 files changed

+917
-883
lines changed

src/sentry/testutils/pytest/metrics.py

Lines changed: 96 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import contextlib
12
import dataclasses
23
import functools
4+
from unittest import mock
35

46
import pytest
57
from snuba_sdk import Entity, Join
@@ -21,104 +23,119 @@
2123

2224

2325
@pytest.fixture(autouse=True)
24-
def control_metrics_access(monkeypatch, request, set_sentry_option):
26+
def control_metrics_access(request, set_sentry_option):
2527
from snuba_sdk import MetricsQuery
2628

2729
from sentry.sentry_metrics import indexer
2830
from sentry.sentry_metrics.indexer.mock import MockIndexer
2931
from sentry.snuba import tasks
3032
from sentry.utils import snuba
3133

32-
if "sentry_metrics" in {mark.name for mark in request.node.iter_markers()}:
33-
mock_indexer = MockIndexer()
34-
monkeypatch.setattr("sentry.sentry_metrics.indexer.backend", mock_indexer)
35-
monkeypatch.setattr("sentry.sentry_metrics.indexer.bulk_record", mock_indexer.bulk_record)
36-
monkeypatch.setattr("sentry.sentry_metrics.indexer.record", mock_indexer.record)
37-
monkeypatch.setattr("sentry.sentry_metrics.indexer.resolve", mock_indexer.resolve)
38-
monkeypatch.setattr(
39-
"sentry.sentry_metrics.indexer.reverse_resolve", mock_indexer.reverse_resolve
40-
)
41-
monkeypatch.setattr(
42-
"sentry.sentry_metrics.indexer.bulk_reverse_resolve", mock_indexer.bulk_reverse_resolve
43-
)
44-
45-
old_resolve = indexer.resolve
46-
47-
def new_resolve(use_case_id, org_id, string):
48-
if (
49-
use_case_id == UseCaseID.TRANSACTIONS
50-
and string in STRINGS_THAT_LOOK_LIKE_TAG_VALUES
51-
):
52-
pytest.fail(
53-
f"stop right there, thief! you're about to resolve the string {string!r}. that looks like a tag value, but in this test mode, tag values are stored in clickhouse. the indexer might not have the value!"
34+
with contextlib.ExitStack() as ctx:
35+
if "sentry_metrics" in {mark.name for mark in request.node.iter_markers()}:
36+
mock_indexer = MockIndexer()
37+
38+
ctx.enter_context(
39+
mock.patch.multiple(
40+
indexer,
41+
backend=mock_indexer,
42+
bulk_record=mock_indexer.bulk_record,
43+
record=mock_indexer.record,
44+
resolve=mock_indexer.resolve,
45+
reverse_resolve=mock_indexer.reverse_resolve,
46+
bulk_reverse_resolve=mock_indexer.bulk_reverse_resolve,
5447
)
55-
return old_resolve(use_case_id, org_id, string)
56-
57-
monkeypatch.setattr(indexer, "resolve", new_resolve)
48+
)
5849

59-
old_build_results = snuba._apply_cache_and_build_results
50+
old_resolve = indexer.resolve
51+
52+
def new_resolve(use_case_id, org_id, string):
53+
if (
54+
use_case_id == UseCaseID.TRANSACTIONS
55+
and string in STRINGS_THAT_LOOK_LIKE_TAG_VALUES
56+
):
57+
pytest.fail(
58+
f"stop right there, thief! you're about to resolve the string {string!r}. that looks like a tag value, but in this test mode, tag values are stored in clickhouse. the indexer might not have the value!"
59+
)
60+
return old_resolve(use_case_id, org_id, string)
61+
62+
ctx.enter_context(mock.patch.object(indexer, "resolve", new_resolve))
63+
64+
old_build_results = snuba._apply_cache_and_build_results
65+
66+
def new_build_results(*args, **kwargs):
67+
if isinstance(args[0][0].request, dict):
68+
# We only support snql queries, and metrics only go through snql
69+
return old_build_results(*args, **kwargs)
70+
query = args[0][0].request.query
71+
is_performance_metrics = False
72+
is_metrics = False
73+
if not isinstance(query, MetricsQuery) and not isinstance(query.match, Join):
74+
is_performance_metrics = query.match.name.startswith("generic")
75+
is_metrics = "metrics" in query.match.name
76+
77+
if is_performance_metrics:
78+
_validate_query(query, True)
79+
elif is_metrics:
80+
_validate_query(query, False)
6081

61-
def new_build_results(*args, **kwargs):
62-
if isinstance(args[0][0].request, dict):
63-
# We only support snql queries, and metrics only go through snql
6482
return old_build_results(*args, **kwargs)
65-
query = args[0][0].request.query
66-
is_performance_metrics = False
67-
is_metrics = False
68-
if not isinstance(query, MetricsQuery) and not isinstance(query.match, Join):
69-
is_performance_metrics = query.match.name.startswith("generic")
70-
is_metrics = "metrics" in query.match.name
71-
72-
if is_performance_metrics:
73-
_validate_query(query, True)
74-
elif is_metrics:
75-
_validate_query(query, False)
76-
77-
return old_build_results(*args, **kwargs)
78-
79-
monkeypatch.setattr(snuba, "_apply_cache_and_build_results", new_build_results)
80-
81-
old_create_snql_in_snuba = tasks._create_snql_in_snuba
82-
83-
def new_create_snql_in_snuba(subscription, snuba_query, snql_query, entity_subscription):
84-
query = snql_query.query
85-
is_performance_metrics = False
86-
is_metrics = False
87-
if isinstance(query.match, Entity):
88-
is_performance_metrics = query.match.name.startswith("generic")
89-
is_metrics = "metrics" in query.match.name
90-
91-
if is_performance_metrics:
92-
_validate_query(query, True)
93-
elif is_metrics:
94-
_validate_query(query, False)
95-
96-
return old_create_snql_in_snuba(
97-
subscription, snuba_query, snql_query, entity_subscription
83+
84+
ctx.enter_context(
85+
mock.patch.object(snuba, "_apply_cache_and_build_results", new_build_results)
9886
)
9987

100-
monkeypatch.setattr(tasks, "_create_snql_in_snuba", new_create_snql_in_snuba)
101-
yield
102-
else:
103-
should_fail = False
88+
old_create_snql_in_snuba = tasks._create_snql_in_snuba
10489

105-
def fail(old_fn, *args, **kwargs):
106-
nonlocal should_fail
107-
should_fail = True
108-
return old_fn(*args, **kwargs)
90+
def new_create_snql_in_snuba(
91+
subscription, snuba_query, snql_query, entity_subscription
92+
):
93+
query = snql_query.query
94+
is_performance_metrics = False
95+
is_metrics = False
96+
if isinstance(query.match, Entity):
97+
is_performance_metrics = query.match.name.startswith("generic")
98+
is_metrics = "metrics" in query.match.name
99+
100+
if is_performance_metrics:
101+
_validate_query(query, True)
102+
elif is_metrics:
103+
_validate_query(query, False)
104+
105+
return old_create_snql_in_snuba(
106+
subscription, snuba_query, snql_query, entity_subscription
107+
)
109108

110-
monkeypatch.setattr(indexer, "resolve", functools.partial(fail, indexer.resolve))
111-
monkeypatch.setattr(indexer, "bulk_record", functools.partial(fail, indexer.bulk_record))
109+
ctx.enter_context(
110+
mock.patch.object(tasks, "_create_snql_in_snuba", new_create_snql_in_snuba)
111+
)
112+
yield
113+
else:
114+
should_fail = False
112115

113-
yield
116+
def fail(old_fn, *args, **kwargs):
117+
nonlocal should_fail
118+
should_fail = True
119+
return old_fn(*args, **kwargs)
114120

115-
if should_fail:
116-
pytest.fail(
117-
"Your test accesses sentry metrics without declaring it in "
118-
"metadata. Add this to your testfile:\n\n"
119-
"pytestmark = pytest.mark.sentry_metrics"
121+
ctx.enter_context(
122+
mock.patch.object(indexer, "resolve", functools.partial(fail, indexer.resolve))
123+
)
124+
ctx.enter_context(
125+
mock.patch.object(
126+
indexer, "bulk_record", functools.partial(fail, indexer.bulk_record)
127+
)
120128
)
121129

130+
yield
131+
132+
if should_fail:
133+
pytest.fail(
134+
"Your test accesses sentry metrics without declaring it in "
135+
"metadata. Add this to your testfile:\n\n"
136+
"pytestmark = pytest.mark.sentry_metrics"
137+
)
138+
122139

123140
def _validate_query(query, tag_values_are_strings):
124141
def _walk(node):

src/sentry/testutils/pytest/stale_database_reads.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import dataclasses
22
from threading import local
33
from typing import Any
4+
from unittest import mock
45

56
import pytest
67
from celery.app.task import Task
@@ -88,7 +89,7 @@ def test_very_special(stale_database_reads):
8889

8990

9091
@pytest.fixture(autouse=True)
91-
def stale_database_reads(monkeypatch):
92+
def stale_database_reads():
9293
_state = local()
9394

9495
old_send = ModelSignal.send
@@ -100,8 +101,6 @@ def send(self, sender, **named):
100101
finally:
101102
_state.in_signal_sender = False
102103

103-
monkeypatch.setattr(ModelSignal, "send", send)
104-
105104
old_on_commit = transaction.on_commit
106105

107106
# Needs to be hooked for autocommit
@@ -112,8 +111,6 @@ def on_commit(*args, **kwargs):
112111
finally:
113112
_state.in_on_commit = False
114113

115-
monkeypatch.setattr(transaction, "on_commit", on_commit)
116-
117114
old_atomic = transaction.atomic
118115

119116
def atomic(*args, **kwargs):
@@ -123,8 +120,6 @@ def atomic(*args, **kwargs):
123120
finally:
124121
_state.in_atomic = False
125122

126-
monkeypatch.setattr(transaction, "atomic", atomic)
127-
128123
old_run_and_clear_commit_hooks = BaseDatabaseWrapper.run_and_clear_commit_hooks
129124

130125
# Needs to be hooked for running commit hooks inside of TransactionTestCase
@@ -135,10 +130,6 @@ def run_and_clear_commit_hooks(*args, **kwargs):
135130
finally:
136131
_state.in_run_and_clear_commit_hooks = False
137132

138-
monkeypatch.setattr(
139-
BaseDatabaseWrapper, "run_and_clear_commit_hooks", run_and_clear_commit_hooks
140-
)
141-
142133
reports = StaleDatabaseReads(model_signal_handlers=[], transaction_blocks=[])
143134

144135
old_apply_async = Task.apply_async
@@ -156,8 +147,15 @@ def apply_async(self, args=(), kwargs=(), **options):
156147

157148
return old_apply_async(self, args, kwargs, **options)
158149

159-
monkeypatch.setattr(Task, "apply_async", apply_async)
160-
161-
yield reports
150+
with (
151+
mock.patch.object(ModelSignal, "send", send),
152+
mock.patch.object(transaction, "on_commit", on_commit),
153+
mock.patch.object(transaction, "atomic", atomic),
154+
mock.patch.object(
155+
BaseDatabaseWrapper, "run_and_clear_commit_hooks", run_and_clear_commit_hooks
156+
),
157+
mock.patch.object(Task, "apply_async", apply_async),
158+
):
159+
yield reports
162160

163161
_raise_reports(reports)

tests/sentry/api/endpoints/test_relay_projectconfigs.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,12 @@ def add_org_key(default_organization, relay):
100100

101101

102102
@pytest.fixture
103-
def no_internal_networks(monkeypatch):
103+
def no_internal_networks():
104104
"""
105105
Disable is_internal_ip functionality (make all requests appear to be from external networks)
106106
"""
107-
monkeypatch.setattr("sentry.auth.system.INTERNAL_NETWORKS", ())
107+
with patch("sentry.auth.system.INTERNAL_NETWORKS", ()):
108+
yield
108109

109110

110111
@django_db_all
@@ -205,10 +206,10 @@ def test_untrusted_external_relays_should_not_receive_configs(call_endpoint, no_
205206

206207

207208
@pytest.fixture
208-
def projectconfig_cache_set(monkeypatch):
209+
def projectconfig_cache_set():
209210
calls: list[dict[str, Any]] = []
210-
monkeypatch.setattr("sentry.relay.projectconfig_cache.backend.set_many", calls.append)
211-
return calls
211+
with patch("sentry.relay.projectconfig_cache.backend.set_many", calls.append):
212+
yield calls
212213

213214

214215
@django_db_all

tests/sentry/api/endpoints/test_relay_projectconfigs_v2.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,12 @@ def add_org_key(default_organization, relay):
5555

5656

5757
@pytest.fixture
58-
def no_internal_networks(monkeypatch):
58+
def no_internal_networks():
5959
"""
6060
Disable is_internal_ip functionality (make all requests appear to be from external networks)
6161
"""
62-
monkeypatch.setattr("sentry.auth.system.INTERNAL_NETWORKS", ())
62+
with patch("sentry.auth.system.INTERNAL_NETWORKS", ()):
63+
yield
6364

6465

6566
@django_db_all
@@ -166,10 +167,10 @@ def test_untrusted_external_relays_should_not_receive_configs(call_endpoint, no_
166167

167168

168169
@pytest.fixture
169-
def projectconfig_cache_set(monkeypatch):
170+
def projectconfig_cache_set():
170171
calls: list[dict[str, Any]] = []
171-
monkeypatch.setattr("sentry.relay.projectconfig_cache.backend.set_many", calls.append)
172-
return calls
172+
with patch("sentry.relay.projectconfig_cache.backend.set_many", calls.append):
173+
yield calls
173174

174175

175176
@django_db_all

tests/sentry/api/endpoints/test_relay_projectconfigs_v3.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,37 +45,38 @@ def inner(public_keys=None, global_=False):
4545

4646

4747
@pytest.fixture
48-
def projectconfig_cache_get_mock_config(monkeypatch):
49-
monkeypatch.setattr(
48+
def projectconfig_cache_get_mock_config():
49+
with patch(
5050
"sentry.relay.projectconfig_cache.backend.get",
51-
lambda *args, **kwargs: {"is_mock_config": True},
52-
)
51+
return_value={"is_mock_config": True},
52+
):
53+
yield
5354

5455

5556
@pytest.fixture
56-
def single_mock_proj_cached(monkeypatch):
57+
def single_mock_proj_cached():
5758
def cache_get(*args, **kwargs):
5859
if args[0] == "must_exist":
5960
return {"is_mock_config": True}
6061
return None
6162

62-
monkeypatch.setattr("sentry.relay.projectconfig_cache.backend.get", cache_get)
63+
with patch("sentry.relay.projectconfig_cache.backend.get", cache_get):
64+
yield
6365

6466

6567
@pytest.fixture
66-
def projectconfig_debounced_cache(monkeypatch):
67-
monkeypatch.setattr(
68-
"sentry.relay.projectconfig_debounce_cache.backend.is_debounced",
69-
lambda *args, **kargs: True,
70-
)
68+
def projectconfig_debounced_cache():
69+
with patch("sentry.relay.projectconfig_debounce_cache.backend.is_debounced", return_value=True):
70+
yield
7171

7272

7373
@pytest.fixture
74-
def project_config_get_mock(monkeypatch):
75-
monkeypatch.setattr(
74+
def project_config_get_mock():
75+
with patch(
7676
"sentry.relay.config.get_project_config",
77-
lambda *args, **kwargs: ProjectConfig(sentinel.mock_project, is_mock_config=True),
78-
)
77+
return_value=ProjectConfig(sentinel.mock_project, is_mock_config=True),
78+
):
79+
yield
7980

8081

8182
@django_db_all

0 commit comments

Comments
 (0)