test_devimports.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  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. @functools.lru_cache
  16. def dev_dependencies() -> tuple[str, ...]:
  17. with open("requirements-dev-frozen.txt") as f:
  18. dev_packages = extract_packages(f.read())
  19. with open("requirements-frozen.txt") as f:
  20. prod_packages = extract_packages(f.read())
  21. # We have some packages that are both runtime + dev
  22. # but we only care about packages that are exclusively dev deps
  23. devonly = dev_packages - prod_packages
  24. module_names = []
  25. for mod, packages in importlib.metadata.packages_distributions().items():
  26. if devonly.intersection(packages):
  27. module_names.append(mod)
  28. return tuple(sorted(module_names))
  29. def validate_package(
  30. package: str,
  31. excluded: tuple[str, ...],
  32. xfail: tuple[str, ...],
  33. ) -> None:
  34. script = f"""\
  35. import builtins
  36. import sys
  37. DISALLOWED = frozenset({dev_dependencies()!r})
  38. EXCLUDED = {excluded!r}
  39. XFAIL = frozenset({xfail!r})
  40. orig = builtins.__import__
  41. def _import(name, globals=None, locals=None, fromlist=(), level=0):
  42. base, *_ = name.split('.')
  43. if level == 0 and base in DISALLOWED:
  44. raise ImportError(f'disallowed dev import: {{name}}')
  45. else:
  46. return orig(name, globals=globals, locals=locals, fromlist=fromlist, level=level)
  47. builtins.__import__ = _import
  48. import sentry.conf.server_mypy
  49. from django.conf import settings
  50. settings.DEBUG = False
  51. import pkgutil
  52. pkg = __import__({package!r})
  53. names = [
  54. name
  55. for _, name, _ in pkgutil.walk_packages(pkg.__path__, f'{{pkg.__name__}}.')
  56. if name not in XFAIL and not name.startswith(EXCLUDED)
  57. ]
  58. for name in names:
  59. try:
  60. __import__(name)
  61. except SystemExit:
  62. raise SystemExit(f'unexpected exit from {{name}}')
  63. except Exception:
  64. print(f'error importing {{name}}:', flush=True)
  65. print(flush=True)
  66. raise
  67. for xfail in {xfail!r}:
  68. try:
  69. __import__(xfail)
  70. except ImportError: # expected failure
  71. pass
  72. else:
  73. raise SystemExit(f'unexpected success importing {{xfail}}')
  74. """
  75. env = {"SENTRY_ENVIRONMENT": "production", "SETUPTOOLS_USE_DISTUTILS": "stdlib"}
  76. ret = subprocess.run(
  77. (sys.executable, "-c", script),
  78. env=env,
  79. stdout=subprocess.PIPE,
  80. stderr=subprocess.STDOUT,
  81. text=True,
  82. )
  83. if ret.returncode:
  84. raise AssertionError(ret.stdout)
  85. @pytest.mark.parametrize("pkg", ("sentry", "sentry_plugins"))
  86. def test_startup_imports(pkg):
  87. validate_package(pkg, EXCLUDED, XFAIL)