conftest.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. # -*- coding: utf-8 -*-
  2. '''
  3. tests.functional.conftest
  4. ~~~~~~~~~~~~~~~~~~~~~~~~~
  5. PyTest boilerplate code for Salt functional testing
  6. '''
  7. # pylint: disable=redefined-outer-name
  8. # Import Python libs
  9. from __future__ import absolute_import, unicode_literals, print_function
  10. import os
  11. import logging
  12. import functools
  13. # Import 3rd-party libs
  14. import pytest
  15. import tornado.gen
  16. import tornado.ioloop
  17. # Import Salt libs
  18. import salt.minion
  19. import salt.config
  20. import salt.runner
  21. import salt.utils.event
  22. import salt.utils.files
  23. import salt.utils.platform
  24. import salt.utils.verify
  25. # Import testing libs
  26. from tests.support.comparables import StateReturn, StateReturnError
  27. from tests.support.runtime import RUNTIME_VARS
  28. from tests.support.sminion import build_minion_opts, create_sminion
  29. log = logging.getLogger(__name__)
  30. class FunctionalMinion(salt.minion.SMinion):
  31. def __init__(self, opts, context=None):
  32. if context is None:
  33. context = {}
  34. super(FunctionalMinion, self).__init__(opts, context=context)
  35. self.__context = context
  36. self.__event = None
  37. self.__event_publisher = None
  38. def start_event_listener(self, io_loop=None):
  39. log.info('Starting %r minion event listener', self.opts['id'])
  40. # start up the event publisher, so we can see and react to events
  41. if self.__event_publisher is None:
  42. self.__event_publisher = salt.utils.event.AsyncEventPublisher(
  43. self.opts,
  44. io_loop=io_loop,
  45. )
  46. if self.__event is None:
  47. self.__event = salt.utils.event.get_event('minion', opts=self.opts, io_loop=io_loop)
  48. self.__event.subscribe('')
  49. # event.set_event_handler returns a tornado coroutine, make sure we run it
  50. io_loop.add_future(self.__event.set_event_handler(self.handle_event), lambda future: future.result())
  51. io_loop.add_callback(log.info, 'Started %r minion event listener', self.opts['id'])
  52. def stop_event_listenter(self):
  53. log.info('Stopping %r minion event listener', self.opts['id'])
  54. if self.__event is not None:
  55. event = self.__event
  56. self.__event = None
  57. event.unsubscribe('')
  58. event.destroy()
  59. if self.__event_publisher is not None:
  60. event_publisher = self.__event_publisher
  61. self.__event_publisher = None
  62. event_publisher.close()
  63. log.info('Stopped %r minion event listener', self.opts['id'])
  64. @tornado.gen.coroutine
  65. def handle_event(self, package):
  66. '''
  67. Handle an event from the epull_sock (all local minion events)
  68. '''
  69. tag, _ = salt.utils.event.SaltEvent.unpack(package)
  70. log.debug(
  71. 'Minion \'%s\' is handling event tag \'%s\'',
  72. self.opts['id'], tag
  73. )
  74. handled_tags = (
  75. 'beacons_refresh',
  76. 'grains_refresh',
  77. 'matchers_refresh',
  78. 'manage_schedule',
  79. 'manage_beacons',
  80. '_minion_mine',
  81. 'module_refresh',
  82. 'pillar_refresh'
  83. )
  84. # Run the appropriate function
  85. for tag_function in handled_tags:
  86. if tag.startswith(tag_function):
  87. self.gen_modules(context=self.__context)
  88. break
  89. def gen_modules(self, initial_load=False, context=None):
  90. super(FunctionalMinion, self).gen_modules(initial_load=initial_load, context=context)
  91. # Make sure state.sls and state.single returns are StateReturn instances for easier comparissons
  92. self.functions.state.sls = StateModuleCallWrapper(self.functions.state.sls)
  93. self.functions.state.single = StateModuleCallWrapper(self.functions.state.single)
  94. self.functions.state.template = StateModuleCallWrapper(self.functions.state.template)
  95. self.functions.state.template_str = StateModuleCallWrapper(self.functions.state.template_str)
  96. self.functions.state.highstate = StateModuleCallWrapper(self.functions.state.highstate)
  97. # For state execution modules, because we'd have to almost copy/paste what salt.modules.state.single
  98. # does, we actually "proxy" the call through salt.modules.state.single instead of calling the state
  99. # execution modules directly.
  100. # Let's load all modules now
  101. self.states._load_all()
  102. # Now, we proxy loaded modules through salt.modules.state.single
  103. for module_name in list(self.states.loaded_modules):
  104. for func_name in list(self.states.loaded_modules[module_name]):
  105. full_func_name = '{}.{}'.format(module_name, func_name)
  106. replacement_function = functools.partial(self.functions.state.single, full_func_name)
  107. self.states._dict[full_func_name] = replacement_function
  108. self.states.loaded_modules[module_name][func_name] = replacement_function
  109. setattr(self.states.loaded_modules[module_name], func_name, replacement_function)
  110. class StateModuleCallWrapper(object):
  111. '''
  112. Wraps salt.modules.state functions
  113. '''
  114. def __init__(self, function):
  115. self._function = function
  116. def __call__(self, *args, **kwargs):
  117. result = self._function(*args, **kwargs)
  118. if isinstance(result, list):
  119. return StateReturnError(result)
  120. return StateReturn(result)
  121. @pytest.fixture
  122. def io_loop():
  123. '''
  124. This is the IOLoop that will run the minion's event system while running tests.
  125. Some tests also use the tornado http backend(salt.utils.http), this is also the
  126. loop that will be used
  127. '''
  128. io_loop = tornado.ioloop.IOLoop()
  129. io_loop.make_current()
  130. yield io_loop
  131. io_loop.clear_current()
  132. io_loop.close(all_fds=True)
  133. def get_test_timeout(pyfuncitem):
  134. # Default value is 10 seconds
  135. timeout = 1
  136. marker = pyfuncitem.get_closest_marker("timeout")
  137. if marker:
  138. timeout = marker.kwargs.get("seconds", timeout)
  139. return timeout
  140. @pytest.mark.tryfirst
  141. def pytest_pyfunc_call(pyfuncitem):
  142. '''
  143. Because we need an IOLoop running, we change how pytest runs the test function in case
  144. the io_loop fixture is present
  145. '''
  146. io_loop = pyfuncitem.funcargs.get('io_loop')
  147. if io_loop is None:
  148. # Let pytest run the test as it usually does
  149. return
  150. funcargs = pyfuncitem.funcargs
  151. testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames}
  152. io_loop.run_sync(
  153. lambda: pyfuncitem.obj(**testargs), timeout=get_test_timeout(pyfuncitem)
  154. )
  155. # prevent other pyfunc calls from executing
  156. return True
  157. @pytest.fixture(scope='session')
  158. def loader_context_dictionary():
  159. return {}
  160. @pytest.fixture(scope='session')
  161. def sminion(loader_context_dictionary):
  162. sminion = create_sminion(minion_id='functional-tests-minion',
  163. initial_conf_file=os.path.join(RUNTIME_VARS.CONF_DIR, 'minion'),
  164. sminion_cls=FunctionalMinion,
  165. loader_context=loader_context_dictionary,
  166. # We don't actually need this minion cached.
  167. # It will last for the whole testing session
  168. cache_sminion=False)
  169. return sminion
  170. @pytest.fixture(autouse=True)
  171. def __minion_loader_cleanup(sminion,
  172. loader_context_dictionary,
  173. utils,
  174. functions,
  175. serializers,
  176. returners,
  177. proxy,
  178. states,
  179. rend,
  180. matchers,
  181. executors):
  182. # Maintain a copy of the sminion opts dictionary to restore after running the tests
  183. salt_opts_copy = sminion.opts.copy()
  184. # Run tests
  185. yield
  186. # Make sure we haven't left any temp state tree states or pillar behind
  187. # Tests should clean them up
  188. for path in (RUNTIME_VARS.TMP_STATE_TREE,
  189. RUNTIME_VARS.TMP_PILLAR_TREE,
  190. RUNTIME_VARS.TMP_PRODENV_STATE_TREE):
  191. path_entries = os.listdir(path)
  192. if path_entries != []:
  193. pytest.fail(
  194. 'Files left behind in \'{}\': {}'.format(path, path_entries)
  195. )
  196. # Clear the context after running the tests
  197. loader_context_dictionary.clear()
  198. # Reset the options dictionary
  199. sminion.opts = salt_opts_copy
  200. utils.opts = salt_opts_copy
  201. functions.opts = salt_opts_copy
  202. serializers.opts = salt_opts_copy
  203. returners.opts = salt_opts_copy
  204. proxy.opts = salt_opts_copy
  205. states.opts = salt_opts_copy
  206. rend.opts = salt_opts_copy
  207. matchers.opts = salt_opts_copy
  208. executors.opts = salt_opts_copy
  209. @pytest.fixture
  210. def minion(sminion, io_loop):
  211. sminion.start_event_listener(io_loop)
  212. yield sminion
  213. sminion.stop_event_listenter()
  214. @pytest.fixture
  215. def grains(minion):
  216. return minion.opts['grains'].copy()
  217. @pytest.fixture
  218. def pillar(minion):
  219. return minion.opts['pillar'].copy()
  220. @pytest.fixture
  221. def utils(minion):
  222. return minion.utils
  223. @pytest.fixture
  224. def functions(minion):
  225. _functions = minion.functions
  226. return _functions
  227. @pytest.fixture
  228. def modules(functions):
  229. return functions
  230. @pytest.fixture
  231. def serializers(minion):
  232. return minion.serializers
  233. @pytest.fixture
  234. def returners(minion):
  235. return minion.returners
  236. @pytest.fixture
  237. def proxy(minion):
  238. return minion.proxy
  239. @pytest.fixture
  240. def states(minion):
  241. return minion.states
  242. @pytest.fixture
  243. def rend(minion):
  244. return minion.rend
  245. @pytest.fixture
  246. def matchers(minion):
  247. return minion.matchers
  248. @pytest.fixture
  249. def executors(minion):
  250. return minion.executors
  251. @pytest.fixture(scope='session')
  252. def _runner_client(loader_context_dictionary):
  253. runner_opts = build_minion_opts(minion_id='functional-tests-runner',
  254. initial_conf_file=os.path.join(RUNTIME_VARS.CONF_DIR, 'master'),
  255. # We don't actually need these options cached.
  256. # They will last for the whole testing session
  257. cache_opts=False)
  258. return salt.runner.RunnerClient(runner_opts, context=loader_context_dictionary)
  259. @pytest.fixture
  260. def runners(_runner_client, loader_context_dictionary):
  261. # Keep a copy of the runner clients options to restore after the test finishes
  262. runner_opts_copy = _runner_client.opts.copy()
  263. yield _runner_client.functions
  264. # Cleanup
  265. loader_context_dictionary.clear()
  266. _runner_client.opts = _runner_client._functions.opts = runner_opts_copy
  267. def pytest_assertrepr_compare(config, op, left, right):
  268. explanation = []
  269. if isinstance(left, StateReturn) or isinstance(right, StateReturn):
  270. if not isinstance(left, StateReturn):
  271. left = StateReturn(left)
  272. if not isinstance(right, StateReturn):
  273. right = StateReturn(right)
  274. explanation.extend(left.explain_comparisson_with(right))
  275. if isinstance(left, StateReturnError):
  276. explanation.extend(left.explain_comparisson_with(right))
  277. if isinstance(right, StateReturnError):
  278. explanation.extend(right.explain_comparisson_with(left))
  279. if explanation:
  280. return explanation