test_devimports.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. from __future__ import annotations
  2. import functools
  3. import importlib.metadata
  4. import subprocess
  5. import sys
  6. import pytest
  7. XFAIL = (
  8. # XXX: ideally these should get fixed
  9. "sentry.sentry_metrics.client.snuba",
  10. "sentry.web.debug_urls",
  11. )
  12. EXCLUDED = ("sentry.testutils.", "sentry.web.frontend.debug.")
  13. def extract_packages(text_content: str) -> set[str]:
  14. return {line.split("==")[0] for line in text_content.splitlines() if "==" in line}
  15. def package_top_level(package: str) -> list[str]:
  16. # Ideally we'd use importlib.metadata.packages_distributions()
  17. # but we're not on py3.10 yet.
  18. # Inspired by https://github.com/python/cpython/blob/e25d8b40cd70744513e190b1ca153087382b6b09/Lib/importlib/metadata/__init__.py#L934
  19. dist = importlib.metadata.distribution(package)
  20. top_level = dist.read_text("top_level.txt")
  21. if top_level:
  22. return top_level.split()
  23. else:
  24. return []
  25. @functools.lru_cache
  26. def dev_dependencies() -> tuple[str, ...]:
  27. with open("requirements-dev-frozen.txt") as f:
  28. dev_packages = extract_packages(f.read())
  29. with open("requirements-frozen.txt") as f:
  30. prod_packages = extract_packages(f.read())
  31. module_names = []
  32. # We have some packages that are both runtime + dev
  33. # but we only care about packages that are exclusively dev deps
  34. for package in dev_packages - prod_packages:
  35. module_names.extend(package_top_level(package))
  36. return tuple(sorted(module_names))
  37. def validate_package(
  38. package: str,
  39. excluded: tuple[str, ...],
  40. xfail: tuple[str, ...],
  41. ) -> None:
  42. script = f"""\
  43. import builtins
  44. import sys
  45. DISALLOWED = frozenset({dev_dependencies()!r})
  46. EXCLUDED = {excluded!r}
  47. XFAIL = frozenset({xfail!r})
  48. orig = builtins.__import__
  49. def _import(name, globals=None, locals=None, fromlist=(), level=0):
  50. base, *_ = name.split('.')
  51. if level == 0 and base in DISALLOWED:
  52. raise ImportError(f'disallowed dev import: {{name}}')
  53. else:
  54. return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)
  55. builtins.__import__ = _import
  56. import sentry.conf.server_mypy
  57. from django.conf import settings
  58. settings.DEBUG = False
  59. import pkgutil
  60. pkg = __import__({package!r})
  61. names = [
  62. name
  63. for _, name, _ in pkgutil.walk_packages(pkg.__path__, f'{{pkg.__name__}}.')
  64. if name not in XFAIL and not name.startswith(EXCLUDED)
  65. ]
  66. for name in names:
  67. try:
  68. __import__(name)
  69. except SystemExit:
  70. raise SystemExit(f'unexpected exit from {{name}}')
  71. except Exception:
  72. print(f'error importing {{name}}:', flush=True)
  73. print(flush=True)
  74. raise
  75. for xfail in {xfail!r}:
  76. try:
  77. __import__(xfail)
  78. except ImportError: # expected failure
  79. pass
  80. else:
  81. raise SystemExit(f'unexpected success importing {{xfail}}')
  82. """
  83. env = {"SENTRY_ENVIRONMENT": "production"}
  84. ret = subprocess.run(
  85. (sys.executable, "-c", script),
  86. env=env,
  87. stdout=subprocess.PIPE,
  88. stderr=subprocess.STDOUT,
  89. text=True,
  90. )
  91. if ret.returncode:
  92. raise AssertionError(ret.stdout)
  93. @pytest.mark.parametrize("pkg", ("sentry", "sentry_plugins"))
  94. def test_startup_imports(pkg):
  95. validate_package(pkg, EXCLUDED, XFAIL)