test_releases.py 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. from __future__ import annotations
  2. import os
  3. import tempfile
  4. from pathlib import Path
  5. import yaml
  6. from django.db.models import Model
  7. from sentry.backup.comparators import get_default_comparators
  8. from sentry.backup.dependencies import NormalizedModelName
  9. from sentry.backup.imports import import_in_global_scope
  10. from sentry.backup.scopes import ExportScope
  11. from sentry.backup.validate import validate
  12. from sentry.testutils.helpers.backups import (
  13. NOOP_PRINTER,
  14. BackupTestCase,
  15. clear_database,
  16. export_to_file,
  17. )
  18. from sentry.testutils.pytest.fixtures import read_snapshot_file
  19. from sentry.testutils.silo import region_silo_test, strip_silo_mode_test_suffix
  20. from sentry.utils import json
  21. from tests.sentry.backup import expect_models, verify_models_in_output
  22. RELEASE_TESTED: set[NormalizedModelName] = set()
  23. @region_silo_test
  24. class ReleaseTests(BackupTestCase):
  25. """
  26. Ensure that exports from the last two released versions of self-hosted are still able to be
  27. imported.
  28. """
  29. def setUp(self):
  30. clear_database(reset_pks=True)
  31. @classmethod
  32. def get_snapshot_path(cls, release: str) -> str:
  33. root_dir = os.path.dirname(os.path.realpath(__file__))
  34. # Use the same data for monolith and region mode.
  35. class_name = strip_silo_mode_test_suffix(cls.__name__)
  36. return f"{root_dir}/snapshots/{class_name}/test_at_{release.replace('.', '_')}.pysnap"
  37. # Note: because we are using the 'insta_snapshot` feature of pysnap, the files will be
  38. # saved as annotated YAML files, not JSON. While this is not strictly a supported format
  39. # for relocation (it's JSON-only), since YAML is a superset of JSON, this should be
  40. # okay, and we can think of these files as valid JSON exports saved using a slightly
  41. # different presentation for ease of testing.
  42. @staticmethod
  43. def snapshot_inequality_comparator(refval: str, output: str) -> str | bool:
  44. refval_json = yaml.safe_load(refval) or dict()
  45. output_json = yaml.safe_load(output)
  46. result = validate(refval_json, output_json, get_default_comparators())
  47. if not result.empty():
  48. # Instead of returning a simple diff, which will differ in ways that are not
  49. # necessarily relevant to the comparison (newly minted tokens, etc), we just
  50. # return the validation comparison findings in pretty-printed form.
  51. return "The following inconsistences were found:\n\n" + result.pretty()
  52. return False
  53. @expect_models(RELEASE_TESTED, "__all__")
  54. def test_at_head(self, expected_models: list[type[Model]]):
  55. with tempfile.TemporaryDirectory() as tmp_dir:
  56. # Convert the existing snapshot from YAML to an equivalent temporary JSON file.
  57. snapshot_path = self.get_snapshot_path("head")
  58. _, snapshot_refval = read_snapshot_file(snapshot_path)
  59. snapshot_data = yaml.safe_load(snapshot_refval) or dict()
  60. tmp_refval_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.refval.json")
  61. with open(tmp_refval_path, "w") as f:
  62. json.dump(snapshot_data, f)
  63. # Take that temporary JSON file and import it. If `SENTRY_SNAPSHOTS_WRITEBACK` is set to
  64. # true, ignore the data in the existing snapshot file and generate a new exhaustive
  65. # instance instead.
  66. if os.environ.get("SENTRY_SNAPSHOTS_WRITEBACK", "0") != "0":
  67. self.create_exhaustive_instance(is_superadmin=True)
  68. else:
  69. with open(tmp_refval_path, "rb") as f:
  70. import_in_global_scope(f, printer=NOOP_PRINTER)
  71. # Export the database state for use in snapshot comparisons/generation.
  72. tmp_export_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.export.json")
  73. exported = export_to_file(tmp_export_path, ExportScope.Global)
  74. # Ensure that the exported data matches the original snapshot (that is, that importing
  75. # and then exporting produces a validation with no findings).
  76. self.insta_snapshot(
  77. exported,
  78. inequality_comparator=self.snapshot_inequality_comparator,
  79. reference_file=snapshot_path,
  80. )
  81. # Check the export so that we can ensure that all models were seen.
  82. verify_models_in_output(expected_models, exported)
  83. def test_at_24_1_0(self):
  84. with tempfile.TemporaryDirectory() as tmp_dir:
  85. _, snapshot_refval = read_snapshot_file(self.get_snapshot_path("24.1.0"))
  86. snapshot_data = yaml.safe_load(snapshot_refval)
  87. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  88. with open(tmp_path, "w") as f:
  89. json.dump(snapshot_data, f)
  90. with open(tmp_path, "rb") as f:
  91. import_in_global_scope(f, printer=NOOP_PRINTER)
  92. def test_at_23_12_1(self):
  93. with tempfile.TemporaryDirectory() as tmp_dir:
  94. _, snapshot_refval = read_snapshot_file(self.get_snapshot_path("23.12.1"))
  95. snapshot_data = yaml.safe_load(snapshot_refval)
  96. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  97. with open(tmp_path, "w") as f:
  98. json.dump(snapshot_data, f)
  99. with open(tmp_path, "rb") as f:
  100. import_in_global_scope(f, printer=NOOP_PRINTER)
  101. def test_at_23_12_0(self):
  102. with tempfile.TemporaryDirectory() as tmp_dir:
  103. _, snapshot_refval = read_snapshot_file(self.get_snapshot_path("23.12.0"))
  104. snapshot_data = yaml.safe_load(snapshot_refval)
  105. tmp_path = Path(tmp_dir).joinpath(f"{self._testMethodName}.json")
  106. with open(tmp_path, "w") as f:
  107. json.dump(snapshot_data, f)
  108. with open(tmp_path, "rb") as f:
  109. import_in_global_scope(f, printer=NOOP_PRINTER)