Skip to content

Commit d4a0ce2

Browse files
authored
test(backup): Ensure expected models are in output (#52690)
We add a new decorator, `@targets_models`, for the `.../backup/test_models.py` test suite. The goal is two-fold: for each individual test, the decorator provides a concise way to express which models must be included in the output, lest we end up with a test that passes the equality check, but only because it excluded our target model(s) altogether. The second goal is to make the set of models being exercised in the `ModelBackupTests` class easily visible to static analysis tools like flake8, so that we may later create a check ensuring that all `__include_in_export__ = True` marked models in this repository are included in this test suite. Issue: getsentry/team-ospo#156 Issue: getsentry/team-ospo#160
1 parent cb87505 commit d4a0ce2

File tree

1 file changed

+58
-11
lines changed

1 file changed

+58
-11
lines changed

tests/sentry/backup/test_models.py

+58-11
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,53 @@
22

33
import tempfile
44
from pathlib import Path
5+
from typing import Type
56

67
from click.testing import CliRunner
78
from django.core.management import call_command
89

10+
from sentry.incidents.models import (
11+
AlertRule,
12+
AlertRuleActivity,
13+
AlertRuleExcludedProjects,
14+
AlertRuleTrigger,
15+
AlertRuleTriggerAction,
16+
AlertRuleTriggerExclusion,
17+
)
918
from sentry.models.environment import Environment
19+
from sentry.models.organization import Organization
1020
from sentry.monitors.models import Monitor, MonitorEnvironment, MonitorType, ScheduleType
1121
from sentry.runner.commands.backup import import_, validate
1222
from sentry.silo import unguarded_write
23+
from sentry.snuba.models import QuerySubscription, SnubaQuery, SnubaQueryEventType
1324
from sentry.testutils import TransactionTestCase
25+
from sentry.utils.json import JSONData
1426
from tests.sentry.backup import ValidationError, tmp_export_to_file
1527

1628

29+
def targets_models(*expected_models: Type):
30+
"""A helper decorator that checks that every model that a test "targeted" was actually seen in
31+
the output, ensuring that we're actually testing the thing we think we are. Additionally, this
32+
decorator is easily legible to static analysis, which allows for static checks to ensure that
33+
all `__include_in_export__ = True` models are being tested."""
34+
35+
def decorator(func):
36+
def wrapped(*args, **kwargs):
37+
ret = func(*args, **kwargs)
38+
if ret is None:
39+
return AssertionError(f"The test {func.__name__} did not return its actual JSON")
40+
actual_model_names = {entry["model"] for entry in ret}
41+
expected_model_names = {"sentry." + model.__name__.lower() for model in expected_models}
42+
notfound = sorted(expected_model_names - actual_model_names)
43+
if len(notfound) > 0:
44+
raise AssertionError(f"Some `@targets_models` entries were not used: {notfound}")
45+
return ret
46+
47+
return wrapped
48+
49+
return decorator
50+
51+
1752
class ModelBackupTests(TransactionTestCase):
1853
"""Test the JSON-ification of models marked `__include_in_export__ = True`. Each test here
1954
creates a fresh database, performs some writes to it, then exports that data into a temporary
@@ -26,16 +61,19 @@ def setUp(self):
2661
# Reset the Django database.
2762
call_command("flush", verbosity=0, interactive=False)
2863

29-
def import_export_then_validate(self):
64+
def import_export_then_validate(self) -> JSONData:
3065
"""Test helper that validates that data imported from a temporary `.json` file correctly
31-
matches the actual outputted export data."""
66+
matches the actual outputted export data.
67+
68+
Return the actual JSON, so that we may use the `@targets_models` decorator to ensure that
69+
we have at least one instance of all the "tested for" models in the actual output."""
3270

3371
with tempfile.TemporaryDirectory() as tmpdir:
3472
tmp_expect = Path(tmpdir).joinpath(f"{self._testMethodName}.expect.json")
3573
tmp_actual = Path(tmpdir).joinpath(f"{self._testMethodName}.actual.json")
3674

37-
# Export the current state of the database into the "expected" temporary file, then parse it
38-
# into a JSON object for comparison.
75+
# Export the current state of the database into the "expected" temporary file, then
76+
# parse it into a JSON object for comparison.
3977
expect = tmp_export_to_file(tmp_expect)
4078

4179
# Write the contents of the "expected" JSON file into the now clean database.
@@ -52,6 +90,8 @@ def import_export_then_validate(self):
5290
if res.findings:
5391
raise ValidationError(res)
5492

93+
return actual
94+
5595
def create_monitor(self):
5696
"""Re-usable monitor object for test cases."""
5797

@@ -65,42 +105,49 @@ def create_monitor(self):
65105
config={"schedule": "* * * * *", "schedule_type": ScheduleType.CRONTAB},
66106
)
67107

108+
@targets_models(AlertRule, QuerySubscription, SnubaQuery, SnubaQueryEventType)
68109
def test_alert_rule(self):
69110
self.create_alert_rule()
70-
self.import_export_then_validate()
111+
return self.import_export_then_validate()
71112

113+
@targets_models(AlertRuleActivity, AlertRuleExcludedProjects)
72114
def test_alert_rule_excluded_projects(self):
73115
user = self.create_user()
74116
org = self.create_organization(owner=user)
75117
excluded = self.create_project(organization=org)
76118
self.create_alert_rule(include_all_projects=True, excluded_projects=[excluded])
77-
self.import_export_then_validate()
119+
return self.import_export_then_validate()
78120

121+
@targets_models(AlertRuleTrigger, AlertRuleTriggerAction, AlertRuleTriggerExclusion)
79122
def test_alert_rule_trigger(self):
80123
excluded = self.create_project()
81124
rule = self.create_alert_rule(include_all_projects=True)
82125
trigger = self.create_alert_rule_trigger(alert_rule=rule, excluded_projects=[excluded])
83126
self.create_alert_rule_trigger_action(alert_rule_trigger=trigger)
84-
self.import_export_then_validate()
127+
return self.import_export_then_validate()
85128

129+
@targets_models(Environment)
86130
def test_environment(self):
87131
self.create_environment()
88-
self.import_export_then_validate()
132+
return self.import_export_then_validate()
89133

134+
@targets_models(Monitor)
90135
def test_monitor(self):
91136
self.create_monitor()
92-
self.import_export_then_validate()
137+
return self.import_export_then_validate()
93138

139+
@targets_models(MonitorEnvironment)
94140
def test_monitor_environment(self):
95141
monitor = self.create_monitor()
96142
env = Environment.objects.create(organization_id=monitor.organization_id, name="test_env")
97143
MonitorEnvironment.objects.create(
98144
monitor=monitor,
99145
environment=env,
100146
)
101-
self.import_export_then_validate()
147+
return self.import_export_then_validate()
102148

149+
@targets_models(Organization)
103150
def test_organization(self):
104151
user = self.create_user()
105152
self.create_organization(owner=user)
106-
self.import_export_then_validate()
153+
return self.import_export_then_validate()

0 commit comments

Comments
 (0)