loader.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. """
  2. tests.support.pytest.loader
  3. ~~~~~~~~~~~~~~~~~~~~~~~~~~~
  4. Salt's Loader PyTest Mock Support
  5. """
  6. import logging
  7. import sys
  8. import types
  9. from collections import deque
  10. import attr
  11. from tests.support.mock import patch
  12. log = logging.getLogger(__name__)
  13. @attr.s(init=True, slots=True, frozen=True)
  14. class LoaderModuleMock:
  15. setup_loader_modules = attr.ib(init=True)
  16. # These dunders should always exist at the module global scope
  17. salt_module_dunders = attr.ib(
  18. init=True,
  19. repr=False,
  20. kw_only=True,
  21. default=(
  22. "__opts__",
  23. "__salt__",
  24. "__runner__",
  25. "__context__",
  26. "__utils__",
  27. "__ext_pillar__",
  28. "__thorium__",
  29. "__states__",
  30. "__serializers__",
  31. "__ret__",
  32. "__grains__",
  33. "__pillar__",
  34. "__sdb__",
  35. ),
  36. )
  37. # These dunders might exist at the module global scope
  38. salt_module_dunders_optional = attr.ib(
  39. init=True, repr=False, kw_only=True, default=("__proxy__",),
  40. )
  41. # These dunders might exist at the function global scope
  42. salt_module_dunder_attributes = attr.ib(
  43. init=True,
  44. repr=False,
  45. kw_only=True,
  46. default=(
  47. # Salt states attributes
  48. "__env__",
  49. "__low__",
  50. "__instance_id__",
  51. "__orchestration_jid__",
  52. # Salt runners attributes
  53. "__jid_event__",
  54. # Salt cloud attributes
  55. "__active_provider_name__",
  56. # Proxy Minions
  57. "__proxyenabled__",
  58. ),
  59. )
  60. _finalizers = attr.ib(
  61. init=False, repr=False, hash=False, default=attr.Factory(deque)
  62. )
  63. def start(self):
  64. module_globals = {dunder: {} for dunder in self.salt_module_dunders}
  65. for module, globals_to_mock in self.setup_loader_modules.items():
  66. log.trace(
  67. "Setting up loader globals for %s; globals: %s", module, globals_to_mock
  68. )
  69. if not isinstance(module, types.ModuleType):
  70. raise RuntimeError(
  71. "The dictionary keys returned by setup_loader_modules() "
  72. "must be an imported module, not {}".format(type(module))
  73. )
  74. if not isinstance(globals_to_mock, dict):
  75. raise RuntimeError(
  76. "The dictionary values returned by setup_loader_modules() "
  77. "must be a dictionary, not {}".format(type(globals_to_mock))
  78. )
  79. for key in self.salt_module_dunders:
  80. if not hasattr(module, key):
  81. # Set the dunder name as an attribute on the module if not present
  82. setattr(module, key, {})
  83. # Remove the added attribute after the test finishes
  84. self.addfinalizer(delattr, module, key)
  85. # Patch sys.modules as the first step
  86. self._patch_sys_modules(globals_to_mock)
  87. # Now patch the module globals
  88. # We actually want to grab a copy of the module globals so that if mocking
  89. # multiple modules, and at least one of the modules has a function to path,
  90. # the patch only happens on the module it's supposed to patch and not all of them.
  91. # It's not a deepcopy because we want to maintain the reference to the salt dunders
  92. # added in the start of this function
  93. self._patch_module_globals(module, globals_to_mock, module_globals.copy())
  94. def stop(self):
  95. while self._finalizers:
  96. func, args, kwargs = self._finalizers.popleft()
  97. func_repr = self._format_callback(func, args, kwargs)
  98. try:
  99. log.trace("Calling finalizer %s", func_repr)
  100. func(*args, **kwargs)
  101. except Exception as exc: # pylint: disable=broad-except
  102. log.error(
  103. "Failed to run finalizer %s: %s", func_repr, exc, exc_info=True,
  104. )
  105. def addfinalizer(self, func, *args, **kwargs):
  106. """
  107. Register a function to run when stopping
  108. """
  109. self._finalizers.append((func, args, kwargs))
  110. def _format_callback(self, callback, args, kwargs):
  111. callback_str = "{}(".format(callback.__qualname__)
  112. if args:
  113. callback_str += ", ".join([repr(arg) for arg in args])
  114. if kwargs:
  115. callback_str += ", ".join(
  116. ["{}={!r}".format(k, v) for (k, v) in kwargs.items()]
  117. )
  118. callback_str += ")"
  119. return callback_str
  120. def _patch_sys_modules(self, mocks):
  121. if "sys.modules" not in mocks:
  122. return
  123. sys_modules = mocks["sys.modules"]
  124. if not isinstance(sys_modules, dict):
  125. raise RuntimeError(
  126. "'sys.modules' must be a dictionary not: {}".format(type(sys_modules))
  127. )
  128. patcher = patch.dict(sys.modules, values=sys_modules)
  129. patcher.start()
  130. self.addfinalizer(patcher.stop)
  131. def _patch_module_globals(self, module, mocks, module_globals):
  132. salt_dunder_dicts = self.salt_module_dunders + self.salt_module_dunders_optional
  133. allowed_salt_dunders = salt_dunder_dicts + self.salt_module_dunder_attributes
  134. for key in mocks:
  135. if key == "sys.modules":
  136. # sys.modules is addressed on another function
  137. continue
  138. if key.startswith("__"):
  139. if key in ("__init__", "__virtual__"):
  140. raise RuntimeError(
  141. "No need to patch {!r}. Passed loader module dict: {}".format(
  142. key, self.setup_loader_modules,
  143. )
  144. )
  145. elif key not in allowed_salt_dunders:
  146. raise RuntimeError(
  147. "Don't know how to handle {!r}. Passed loader module dict: {}".format(
  148. key, self.setup_loader_modules,
  149. )
  150. )
  151. elif key in salt_dunder_dicts and not hasattr(module, key):
  152. # Add the key as a dictionary attribute to the module so it can be patched by `patch.dict`'
  153. setattr(module, key, {})
  154. # Remove the added attribute after the test finishes
  155. self.addfinalizer(delattr, module, key)
  156. if not hasattr(module, key):
  157. # Set the key as an attribute so it can be patched
  158. setattr(module, key, None)
  159. # Remove the added attribute after the test finishes
  160. self.addfinalizer(delattr, module, key)
  161. module_globals[key] = mocks[key]
  162. # Patch the module!
  163. log.trace("Patching globals for %s; globals: %s", module, module_globals)
  164. patcher = patch.multiple(module, **module_globals)
  165. patcher.start()
  166. self.addfinalizer(patcher.stop)
  167. def __enter__(self):
  168. self.start()
  169. return self
  170. def __exit__(self, *args):
  171. self.stop()