conftest.py 47 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326
  1. # -*- coding: utf-8 -*-
  2. """
  3. :codeauthor: Pedro Algarvio (pedro@algarvio.me)
  4. tests.conftest
  5. ~~~~~~~~~~~~~~
  6. Prepare py.test for our test suite
  7. """
  8. # pylint: disable=wrong-import-order,wrong-import-position,3rd-party-local-module-not-gated
  9. # pylint: disable=redefined-outer-name,invalid-name,3rd-party-module-not-gated
  10. from __future__ import absolute_import, print_function, unicode_literals
  11. import logging
  12. import os
  13. import pprint
  14. import shutil
  15. import stat
  16. import sys
  17. import tempfile
  18. import textwrap
  19. from contextlib import contextmanager
  20. from functools import partial, wraps
  21. import _pytest.logging
  22. import _pytest.skipping
  23. import psutil
  24. import pytest
  25. import salt.config
  26. import salt.loader
  27. import salt.log.mixins
  28. import salt.log.setup
  29. import salt.utils.files
  30. import salt.utils.path
  31. import salt.utils.platform
  32. import salt.utils.win_functions
  33. import saltfactories.utils.compat
  34. from _pytest.mark.evaluate import MarkEvaluator
  35. from salt.ext import six
  36. from salt.serializers import yaml
  37. from salt.utils.immutabletypes import freeze
  38. from tests.support.helpers import PRE_PYTEST_SKIP_OR_NOT, PRE_PYTEST_SKIP_REASON
  39. from tests.support.runtests import RUNTIME_VARS
  40. from tests.support.sminion import check_required_sminion_attributes, create_sminion
  41. TESTS_DIR = os.path.dirname(os.path.normpath(os.path.abspath(__file__)))
  42. CODE_DIR = os.path.dirname(TESTS_DIR)
  43. # Change to code checkout directory
  44. os.chdir(CODE_DIR)
  45. # Make sure the current directory is the first item in sys.path
  46. if CODE_DIR in sys.path:
  47. sys.path.remove(CODE_DIR)
  48. sys.path.insert(0, CODE_DIR)
  49. # Coverage
  50. if "COVERAGE_PROCESS_START" in os.environ:
  51. MAYBE_RUN_COVERAGE = True
  52. COVERAGERC_FILE = os.environ["COVERAGE_PROCESS_START"]
  53. else:
  54. COVERAGERC_FILE = os.path.join(CODE_DIR, ".coveragerc")
  55. MAYBE_RUN_COVERAGE = (
  56. sys.argv[0].endswith("pytest.py") or "_COVERAGE_RCFILE" in os.environ
  57. )
  58. if MAYBE_RUN_COVERAGE:
  59. # Flag coverage to track suprocesses by pointing it to the right .coveragerc file
  60. os.environ[str("COVERAGE_PROCESS_START")] = str(COVERAGERC_FILE)
  61. # Define the pytest plugins we rely on
  62. pytest_plugins = ["tempdir", "helpers_namespace", "salt-runtests-bridge"]
  63. # Define where not to collect tests from
  64. collect_ignore = ["setup.py"]
  65. # Patch PyTest logging handlers
  66. class LogCaptureHandler(
  67. salt.log.mixins.ExcInfoOnLogLevelFormatMixIn, _pytest.logging.LogCaptureHandler
  68. ):
  69. """
  70. Subclassing PyTest's LogCaptureHandler in order to add the
  71. exc_info_on_loglevel functionality and actually make it a NullHandler,
  72. it's only used to print log messages emmited during tests, which we
  73. have explicitly disabled in pytest.ini
  74. """
  75. _pytest.logging.LogCaptureHandler = LogCaptureHandler
  76. class LiveLoggingStreamHandler(
  77. salt.log.mixins.ExcInfoOnLogLevelFormatMixIn,
  78. _pytest.logging._LiveLoggingStreamHandler,
  79. ):
  80. """
  81. Subclassing PyTest's LiveLoggingStreamHandler in order to add the
  82. exc_info_on_loglevel functionality.
  83. """
  84. _pytest.logging._LiveLoggingStreamHandler = LiveLoggingStreamHandler
  85. # Reset logging root handlers
  86. for handler in logging.root.handlers[:]:
  87. logging.root.removeHandler(handler)
  88. # Reset the root logger to its default level(because salt changed it)
  89. logging.root.setLevel(logging.WARNING)
  90. log = logging.getLogger("salt.testsuite")
  91. # ----- PyTest Tempdir Plugin Hooks --------------------------------------------------------------------------------->
  92. def pytest_tempdir_basename():
  93. """
  94. Return the temporary directory basename for the salt test suite.
  95. """
  96. return "salt-tests-tmpdir"
  97. # <---- PyTest Tempdir Plugin Hooks ----------------------------------------------------------------------------------
  98. # ----- CLI Options Setup ------------------------------------------------------------------------------------------->
  99. def pytest_addoption(parser):
  100. """
  101. register argparse-style options and ini-style config values.
  102. """
  103. test_selection_group = parser.getgroup("Tests Selection")
  104. test_selection_group.addoption(
  105. "--transport",
  106. default="zeromq",
  107. choices=("zeromq", "tcp"),
  108. help=(
  109. "Select which transport to run the integration tests with, "
  110. "zeromq or tcp. Default: %default"
  111. ),
  112. )
  113. test_selection_group.addoption(
  114. "--ssh",
  115. "--ssh-tests",
  116. dest="ssh",
  117. action="store_true",
  118. default=False,
  119. help="Run salt-ssh tests. These tests will spin up a temporary "
  120. "SSH server on your machine. In certain environments, this "
  121. "may be insecure! Default: False",
  122. )
  123. test_selection_group.addoption(
  124. "--proxy",
  125. "--proxy-tests",
  126. dest="proxy",
  127. action="store_true",
  128. default=False,
  129. help="Run proxy tests",
  130. )
  131. test_selection_group.addoption(
  132. "--run-slow", action="store_true", default=False, help="Run slow tests.",
  133. )
  134. output_options_group = parser.getgroup("Output Options")
  135. output_options_group.addoption(
  136. "--output-columns",
  137. default=80,
  138. type=int,
  139. help="Number of maximum columns to use on the output",
  140. )
  141. output_options_group.addoption(
  142. "--no-colors",
  143. "--no-colours",
  144. default=False,
  145. action="store_true",
  146. help="Disable colour printing.",
  147. )
  148. # ----- Test Groups --------------------------------------------------------------------------------------------->
  149. # This will allow running the tests in chunks
  150. test_selection_group.addoption(
  151. "--test-group-count",
  152. dest="test-group-count",
  153. type=int,
  154. help="The number of groups to split the tests into",
  155. )
  156. test_selection_group.addoption(
  157. "--test-group",
  158. dest="test-group",
  159. type=int,
  160. help="The group of tests that should be executed",
  161. )
  162. # <---- Test Groups ----------------------------------------------------------------------------------------------
  163. # <---- CLI Options Setup --------------------------------------------------------------------------------------------
  164. # ----- Register Markers -------------------------------------------------------------------------------------------->
  165. @pytest.mark.trylast
  166. def pytest_configure(config):
  167. """
  168. called after command line options have been parsed
  169. and all plugins and initial conftest files been loaded.
  170. """
  171. for dirname in os.listdir(CODE_DIR):
  172. if not os.path.isdir(dirname):
  173. continue
  174. if dirname != "tests":
  175. config.addinivalue_line("norecursedirs", os.path.join(CODE_DIR, dirname))
  176. # Expose the markers we use to pytest CLI
  177. config.addinivalue_line(
  178. "markers",
  179. "requires_salt_modules(*required_module_names): Skip if at least one module is not available.",
  180. )
  181. config.addinivalue_line(
  182. "markers",
  183. "requires_salt_states(*required_state_names): Skip if at least one state module is not available.",
  184. )
  185. config.addinivalue_line(
  186. "markers", "windows_whitelisted: Mark test as whitelisted to run under Windows"
  187. )
  188. # Make sure the test suite "knows" this is a pytest test run
  189. RUNTIME_VARS.PYTEST_SESSION = True
  190. # "Flag" the slotTest decorator if we're skipping slow tests or not
  191. os.environ["SLOW_TESTS"] = str(config.getoption("--run-slow"))
  192. # <---- Register Markers ---------------------------------------------------------------------------------------------
  193. # ----- PyTest Tweaks ----------------------------------------------------------------------------------------------->
  194. def set_max_open_files_limits(min_soft=3072, min_hard=4096):
  195. # Get current limits
  196. if salt.utils.platform.is_windows():
  197. import win32file
  198. prev_hard = win32file._getmaxstdio()
  199. prev_soft = 512
  200. else:
  201. import resource
  202. prev_soft, prev_hard = resource.getrlimit(resource.RLIMIT_NOFILE)
  203. # Check minimum required limits
  204. set_limits = False
  205. if prev_soft < min_soft:
  206. soft = min_soft
  207. set_limits = True
  208. else:
  209. soft = prev_soft
  210. if prev_hard < min_hard:
  211. hard = min_hard
  212. set_limits = True
  213. else:
  214. hard = prev_hard
  215. # Increase limits
  216. if set_limits:
  217. log.debug(
  218. " * Max open files settings is too low (soft: %s, hard: %s) for running the tests. "
  219. "Trying to raise the limits to soft: %s, hard: %s",
  220. prev_soft,
  221. prev_hard,
  222. soft,
  223. hard,
  224. )
  225. try:
  226. if salt.utils.platform.is_windows():
  227. hard = 2048 if hard > 2048 else hard
  228. win32file._setmaxstdio(hard)
  229. else:
  230. resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard))
  231. except Exception as err: # pylint: disable=broad-except
  232. log.error(
  233. "Failed to raise the max open files settings -> %s. Please issue the following command "
  234. "on your console: 'ulimit -u %s'",
  235. err,
  236. soft,
  237. )
  238. exit(1)
  239. return soft, hard
  240. def pytest_report_header():
  241. soft, hard = set_max_open_files_limits()
  242. return "max open files; soft: {}; hard: {}".format(soft, hard)
  243. @pytest.hookimpl(hookwrapper=True, trylast=True)
  244. def pytest_collection_modifyitems(config, items):
  245. """
  246. called after collection has been performed, may filter or re-order
  247. the items in-place.
  248. :param _pytest.main.Session session: the pytest session object
  249. :param _pytest.config.Config config: pytest config object
  250. :param List[_pytest.nodes.Item] items: list of item objects
  251. """
  252. # Let PyTest or other plugins handle the initial collection
  253. yield
  254. groups_collection_modifyitems(config, items)
  255. log.warning("Mofifying collected tests to keep track of fixture usage")
  256. for item in items:
  257. for fixture in item.fixturenames:
  258. if fixture not in item._fixtureinfo.name2fixturedefs:
  259. continue
  260. for fixturedef in item._fixtureinfo.name2fixturedefs[fixture]:
  261. if fixturedef.scope == "function":
  262. continue
  263. try:
  264. node_ids = fixturedef.node_ids
  265. except AttributeError:
  266. node_ids = fixturedef.node_ids = set()
  267. node_ids.add(item.nodeid)
  268. try:
  269. fixturedef.finish.__wrapped__
  270. except AttributeError:
  271. original_func = fixturedef.finish
  272. def wrapper(func, fixturedef):
  273. @wraps(func)
  274. def wrapped(self, request):
  275. try:
  276. return self._finished
  277. except AttributeError:
  278. if self.node_ids:
  279. if (
  280. not request.session.shouldfail
  281. and not request.session.shouldstop
  282. ):
  283. log.debug(
  284. "%s is still going to be used, not terminating it. "
  285. "Still in use on:\n%s",
  286. self,
  287. pprint.pformat(list(self.node_ids)),
  288. )
  289. return
  290. log.debug("Finish called on %s", self)
  291. try:
  292. return func(request)
  293. finally:
  294. self._finished = True
  295. return partial(wrapped, fixturedef)
  296. fixturedef.finish = wrapper(fixturedef.finish, fixturedef)
  297. try:
  298. fixturedef.finish.__wrapped__
  299. except AttributeError:
  300. fixturedef.finish.__wrapped__ = original_func
  301. @pytest.hookimpl(trylast=True, hookwrapper=True)
  302. def pytest_runtest_protocol(item, nextitem):
  303. """
  304. implements the runtest_setup/call/teardown protocol for
  305. the given test item, including capturing exceptions and calling
  306. reporting hooks.
  307. :arg item: test item for which the runtest protocol is performed.
  308. :arg nextitem: the scheduled-to-be-next test item (or None if this
  309. is the end my friend). This argument is passed on to
  310. :py:func:`pytest_runtest_teardown`.
  311. :return boolean: True if no further hook implementations should be invoked.
  312. Stops at first non-None result, see :ref:`firstresult`
  313. """
  314. request = item._request
  315. used_fixture_defs = []
  316. for fixture in item.fixturenames:
  317. if fixture not in item._fixtureinfo.name2fixturedefs:
  318. continue
  319. for fixturedef in reversed(item._fixtureinfo.name2fixturedefs[fixture]):
  320. if fixturedef.scope == "function":
  321. continue
  322. used_fixture_defs.append(fixturedef)
  323. try:
  324. # Run the test
  325. yield
  326. finally:
  327. for fixturedef in used_fixture_defs:
  328. if item.nodeid in fixturedef.node_ids:
  329. fixturedef.node_ids.remove(item.nodeid)
  330. if not fixturedef.node_ids:
  331. # This fixture is not used in any more test functions
  332. fixturedef.finish(request)
  333. del request
  334. del used_fixture_defs
  335. def pytest_runtest_teardown(item, nextitem):
  336. """
  337. called after ``pytest_runtest_call``.
  338. :arg nextitem: the scheduled-to-be-next test item (None if no further
  339. test item is scheduled). This argument can be used to
  340. perform exact teardowns, i.e. calling just enough finalizers
  341. so that nextitem only needs to call setup-functions.
  342. """
  343. # PyTest doesn't reset the capturing log handler when done with it.
  344. # Reset it to free used memory and python objects
  345. # We currently have PyTest's log_print setting set to false, if it was
  346. # set to true, the call bellow would make PyTest not print any logs at all.
  347. item.catch_log_handler.reset()
  348. # <---- PyTest Tweaks ------------------------------------------------------------------------------------------------
  349. # ----- Test Setup -------------------------------------------------------------------------------------------------->
  350. def _has_unittest_attr(item, attr):
  351. # XXX: This is a hack while we support both runtests.py and PyTest
  352. if hasattr(item.obj, attr):
  353. return True
  354. if item.cls and hasattr(item.cls, attr):
  355. return True
  356. if item.parent and hasattr(item.parent.obj, attr):
  357. return True
  358. return False
  359. @pytest.hookimpl(tryfirst=True)
  360. def pytest_runtest_setup(item):
  361. """
  362. Fixtures injection based on markers or test skips based on CLI arguments
  363. """
  364. integration_utils_tests_path = os.path.join(
  365. CODE_DIR, "tests", "integration", "utils"
  366. )
  367. if (
  368. str(item.fspath).startswith(integration_utils_tests_path)
  369. and PRE_PYTEST_SKIP_OR_NOT is True
  370. ):
  371. item._skipped_by_mark = True
  372. pytest.skip(PRE_PYTEST_SKIP_REASON)
  373. if saltfactories.utils.compat.has_unittest_attr(item, "__slow_test__"):
  374. if item.config.getoption("--run-slow") is False:
  375. item._skipped_by_mark = True
  376. pytest.skip("Slow tests are disabled!")
  377. requires_salt_modules_marker = item.get_closest_marker("requires_salt_modules")
  378. if requires_salt_modules_marker is not None:
  379. required_salt_modules = requires_salt_modules_marker.args
  380. if len(required_salt_modules) == 1 and isinstance(
  381. required_salt_modules[0], (list, tuple, set)
  382. ):
  383. required_salt_modules = required_salt_modules[0]
  384. required_salt_modules = set(required_salt_modules)
  385. not_available_modules = check_required_sminion_attributes(
  386. "functions", required_salt_modules
  387. )
  388. if not_available_modules:
  389. item._skipped_by_mark = True
  390. if len(not_available_modules) == 1:
  391. pytest.skip(
  392. "Salt module '{}' is not available".format(*not_available_modules)
  393. )
  394. pytest.skip(
  395. "Salt modules not available: {}".format(
  396. ", ".join(not_available_modules)
  397. )
  398. )
  399. requires_salt_states_marker = item.get_closest_marker("requires_salt_states")
  400. if requires_salt_states_marker is not None:
  401. required_salt_states = requires_salt_states_marker.args
  402. if len(required_salt_states) == 1 and isinstance(
  403. required_salt_states[0], (list, tuple, set)
  404. ):
  405. required_salt_states = required_salt_states[0]
  406. required_salt_states = set(required_salt_states)
  407. not_available_states = check_required_sminion_attributes(
  408. "states", required_salt_states
  409. )
  410. if not_available_states:
  411. item._skipped_by_mark = True
  412. if len(not_available_states) == 1:
  413. pytest.skip(
  414. "Salt state module '{}' is not available".format(
  415. *not_available_states
  416. )
  417. )
  418. pytest.skip(
  419. "Salt state modules not available: {}".format(
  420. ", ".join(not_available_states)
  421. )
  422. )
  423. if salt.utils.platform.is_windows():
  424. if not item.fspath.fnmatch(os.path.join(CODE_DIR, "tests", "unit", "*")):
  425. # Unit tests are whitelisted on windows by default, so, we're only
  426. # after all other tests
  427. windows_whitelisted_marker = item.get_closest_marker("windows_whitelisted")
  428. if windows_whitelisted_marker is None:
  429. item._skipped_by_mark = True
  430. pytest.skip("Test is not whitelisted for Windows")
  431. # <---- Test Setup ---------------------------------------------------------------------------------------------------
  432. # ----- Test Groups Selection --------------------------------------------------------------------------------------->
  433. def get_group_size_and_start(total_items, total_groups, group_id):
  434. """
  435. Calculate group size and start index.
  436. """
  437. base_size = total_items // total_groups
  438. rem = total_items % total_groups
  439. start = base_size * (group_id - 1) + min(group_id - 1, rem)
  440. size = base_size + 1 if group_id <= rem else base_size
  441. return (start, size)
  442. def get_group(items, total_groups, group_id):
  443. """
  444. Get the items from the passed in group based on group size.
  445. """
  446. if not 0 < group_id <= total_groups:
  447. raise ValueError("Invalid test-group argument")
  448. start, size = get_group_size_and_start(len(items), total_groups, group_id)
  449. selected = items[start : start + size]
  450. deselected = items[:start] + items[start + size :]
  451. assert len(selected) + len(deselected) == len(items)
  452. return selected, deselected
  453. def groups_collection_modifyitems(config, items):
  454. group_count = config.getoption("test-group-count")
  455. group_id = config.getoption("test-group")
  456. if not group_count or not group_id:
  457. # We're not selection tests using groups, don't do any filtering
  458. return
  459. total_items = len(items)
  460. tests_in_group, deselected = get_group(items, group_count, group_id)
  461. # Replace all items in the list
  462. items[:] = tests_in_group
  463. if deselected:
  464. config.hook.pytest_deselected(items=deselected)
  465. terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
  466. terminal_reporter.write(
  467. "Running test group #{0} ({1} tests)\n".format(group_id, len(items)),
  468. yellow=True,
  469. )
  470. # <---- Test Groups Selection ----------------------------------------------------------------------------------------
  471. # ----- Pytest Helpers ---------------------------------------------------------------------------------------------->
  472. if six.PY2:
  473. # backport mock_open from the python 3 unittest.mock library so that we can
  474. # mock read, readline, readlines, and file iteration properly
  475. file_spec = None
  476. def _iterate_read_data(read_data):
  477. # Helper for mock_open:
  478. # Retrieve lines from read_data via a generator so that separate calls to
  479. # readline, read, and readlines are properly interleaved
  480. data_as_list = ["{0}\n".format(l) for l in read_data.split("\n")]
  481. if data_as_list[-1] == "\n":
  482. # If the last line ended in a newline, the list comprehension will have an
  483. # extra entry that's just a newline. Remove this.
  484. data_as_list = data_as_list[:-1]
  485. else:
  486. # If there wasn't an extra newline by itself, then the file being
  487. # emulated doesn't have a newline to end the last line remove the
  488. # newline that our naive format() added
  489. data_as_list[-1] = data_as_list[-1][:-1]
  490. for line in data_as_list:
  491. yield line
  492. @pytest.helpers.mock.register
  493. def mock_open(mock=None, read_data=""):
  494. """
  495. A helper function to create a mock to replace the use of `open`. It works
  496. for `open` called directly or used as a context manager.
  497. The `mock` argument is the mock object to configure. If `None` (the
  498. default) then a `MagicMock` will be created for you, with the API limited
  499. to methods or attributes available on standard file handles.
  500. `read_data` is a string for the `read` methoddline`, and `readlines` of the
  501. file handle to return. This is an empty string by default.
  502. """
  503. _mock = pytest.importorskip("mock", minversion="2.0.0")
  504. def _readlines_side_effect(*args, **kwargs):
  505. if handle.readlines.return_value is not None:
  506. return handle.readlines.return_value
  507. return list(_data)
  508. def _read_side_effect(*args, **kwargs):
  509. if handle.read.return_value is not None:
  510. return handle.read.return_value
  511. return "".join(_data)
  512. def _readline_side_effect():
  513. if handle.readline.return_value is not None:
  514. while True:
  515. yield handle.readline.return_value
  516. for line in _data:
  517. yield line
  518. global file_spec
  519. if file_spec is None:
  520. file_spec = file # pylint: disable=undefined-variable
  521. if mock is None:
  522. mock = _mock.MagicMock(name="open", spec=open)
  523. handle = _mock.MagicMock(spec=file_spec)
  524. handle.__enter__.return_value = handle
  525. _data = _iterate_read_data(read_data)
  526. handle.write.return_value = None
  527. handle.read.return_value = None
  528. handle.readline.return_value = None
  529. handle.readlines.return_value = None
  530. handle.read.side_effect = _read_side_effect
  531. handle.readline.side_effect = _readline_side_effect()
  532. handle.readlines.side_effect = _readlines_side_effect
  533. mock.return_value = handle
  534. return mock
  535. else:
  536. @pytest.helpers.mock.register
  537. def mock_open(mock=None, read_data=""):
  538. _mock = pytest.importorskip("mock", minversion="2.0.0")
  539. return _mock.mock_open(mock=mock, read_data=read_data)
  540. @pytest.helpers.register
  541. @contextmanager
  542. def temp_directory(name=None):
  543. if name is not None:
  544. directory_path = os.path.join(RUNTIME_VARS.TMP, name)
  545. else:
  546. directory_path = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  547. if not os.path.isdir(directory_path):
  548. os.makedirs(directory_path)
  549. yield directory_path
  550. shutil.rmtree(directory_path, ignore_errors=True)
  551. @pytest.helpers.register
  552. @contextmanager
  553. def temp_file(name, contents=None, directory=None, strip_first_newline=True):
  554. if directory is None:
  555. directory = RUNTIME_VARS.TMP
  556. file_path = os.path.join(directory, name)
  557. file_directory = os.path.dirname(file_path)
  558. if contents is not None:
  559. if contents:
  560. if contents.startswith("\n") and strip_first_newline:
  561. contents = contents[1:]
  562. file_contents = textwrap.dedent(contents)
  563. else:
  564. file_contents = contents
  565. try:
  566. if not os.path.isdir(file_directory):
  567. os.makedirs(file_directory)
  568. if contents is not None:
  569. with salt.utils.files.fopen(file_path, "w") as wfh:
  570. wfh.write(file_contents)
  571. yield file_path
  572. finally:
  573. try:
  574. os.unlink(file_path)
  575. except OSError:
  576. # Already deleted
  577. pass
  578. @pytest.helpers.register
  579. def temp_state_file(name, contents, saltenv="base", strip_first_newline=True):
  580. if saltenv == "base":
  581. directory = RUNTIME_VARS.TMP_STATE_TREE
  582. elif saltenv == "prod":
  583. directory = RUNTIME_VARS.TMP_PRODENV_STATE_TREE
  584. else:
  585. raise RuntimeError(
  586. '"saltenv" can only be "base" or "prod", not "{}"'.format(saltenv)
  587. )
  588. return temp_file(
  589. name, contents, directory=directory, strip_first_newline=strip_first_newline
  590. )
  591. @pytest.helpers.register
  592. def temp_pillar_file(name, contents, saltenv="base", strip_first_newline=True):
  593. if saltenv == "base":
  594. directory = RUNTIME_VARS.TMP_PILLAR_TREE
  595. elif saltenv == "prod":
  596. directory = RUNTIME_VARS.TMP_PRODENV_PILLAR_TREE
  597. else:
  598. raise RuntimeError(
  599. '"saltenv" can only be "base" or "prod", not "{}"'.format(saltenv)
  600. )
  601. return temp_file(
  602. name, contents, directory=directory, strip_first_newline=strip_first_newline
  603. )
  604. # <---- Pytest Helpers -----------------------------------------------------------------------------------------------
  605. # ----- Fixtures Overrides ------------------------------------------------------------------------------------------>
  606. @pytest.fixture(scope="session")
  607. def salt_factories_config():
  608. """
  609. Return a dictionary with the keyworkd arguments for SaltFactoriesManager
  610. """
  611. return {
  612. "executable": sys.executable,
  613. "code_dir": CODE_DIR,
  614. "inject_coverage": MAYBE_RUN_COVERAGE,
  615. "inject_sitecustomize": MAYBE_RUN_COVERAGE,
  616. "start_timeout": 120
  617. if (os.environ.get("JENKINS_URL") or os.environ.get("CI"))
  618. else 60,
  619. }
  620. # <---- Pytest Helpers -----------------------------------------------------------------------------------------------
  621. # ----- Fixtures Overrides ------------------------------------------------------------------------------------------>
  622. def _get_virtualenv_binary_path():
  623. try:
  624. return _get_virtualenv_binary_path.__virtualenv_binary__
  625. except AttributeError:
  626. # Under windows we can't seem to properly create a virtualenv off of another
  627. # virtualenv, we can on linux but we will still point to the virtualenv binary
  628. # outside the virtualenv running the test suite, if that's the case.
  629. try:
  630. real_prefix = sys.real_prefix
  631. # The above attribute exists, this is a virtualenv
  632. if salt.utils.platform.is_windows():
  633. virtualenv_binary = os.path.join(
  634. real_prefix, "Scripts", "virtualenv.exe"
  635. )
  636. else:
  637. # We need to remove the virtualenv from PATH or we'll get the virtualenv binary
  638. # from within the virtualenv, we don't want that
  639. path = os.environ.get("PATH")
  640. if path is not None:
  641. path_items = path.split(os.pathsep)
  642. for item in path_items[:]:
  643. if item.startswith(sys.base_prefix):
  644. path_items.remove(item)
  645. os.environ["PATH"] = os.pathsep.join(path_items)
  646. virtualenv_binary = salt.utils.path.which("virtualenv")
  647. if path is not None:
  648. # Restore previous environ PATH
  649. os.environ["PATH"] = path
  650. if not virtualenv_binary.startswith(real_prefix):
  651. virtualenv_binary = None
  652. if virtualenv_binary and not os.path.exists(virtualenv_binary):
  653. # It doesn't exist?!
  654. virtualenv_binary = None
  655. except AttributeError:
  656. # We're not running inside a virtualenv
  657. virtualenv_binary = None
  658. _get_virtualenv_binary_path.__virtualenv_binary__ = virtualenv_binary
  659. return virtualenv_binary
  660. @pytest.fixture(scope="session")
  661. def integration_files_dir(salt_factories):
  662. """
  663. Fixture which returns the salt integration files directory path.
  664. Creates the directory if it does not yet exist.
  665. """
  666. dirname = salt_factories.root_dir.join("integration-files")
  667. dirname.ensure(dir=True)
  668. return dirname
  669. @pytest.fixture(scope="session")
  670. def state_tree_root_dir(integration_files_dir):
  671. """
  672. Fixture which returns the salt state tree root directory path.
  673. Creates the directory if it does not yet exist.
  674. """
  675. dirname = integration_files_dir.join("state-tree")
  676. dirname.ensure(dir=True)
  677. return dirname
  678. @pytest.fixture(scope="session")
  679. def pillar_tree_root_dir(integration_files_dir):
  680. """
  681. Fixture which returns the salt pillar tree root directory path.
  682. Creates the directory if it does not yet exist.
  683. """
  684. dirname = integration_files_dir.join("pillar-tree")
  685. dirname.ensure(dir=True)
  686. return dirname
  687. @pytest.fixture(scope="session")
  688. def base_env_state_tree_root_dir(state_tree_root_dir):
  689. """
  690. Fixture which returns the salt base environment state tree directory path.
  691. Creates the directory if it does not yet exist.
  692. """
  693. dirname = state_tree_root_dir.join("base")
  694. dirname.ensure(dir=True)
  695. RUNTIME_VARS.TMP_STATE_TREE = dirname.realpath().strpath
  696. return dirname
  697. @pytest.fixture(scope="session")
  698. def prod_env_state_tree_root_dir(state_tree_root_dir):
  699. """
  700. Fixture which returns the salt prod environment state tree directory path.
  701. Creates the directory if it does not yet exist.
  702. """
  703. dirname = state_tree_root_dir.join("prod")
  704. dirname.ensure(dir=True)
  705. RUNTIME_VARS.TMP_PRODENV_STATE_TREE = dirname.realpath().strpath
  706. return dirname
  707. @pytest.fixture(scope="session")
  708. def base_env_pillar_tree_root_dir(pillar_tree_root_dir):
  709. """
  710. Fixture which returns the salt base environment pillar tree directory path.
  711. Creates the directory if it does not yet exist.
  712. """
  713. dirname = pillar_tree_root_dir.join("base")
  714. dirname.ensure(dir=True)
  715. RUNTIME_VARS.TMP_PILLAR_TREE = dirname.realpath().strpath
  716. return dirname
  717. @pytest.fixture(scope="session")
  718. def prod_env_pillar_tree_root_dir(pillar_tree_root_dir):
  719. """
  720. Fixture which returns the salt prod environment pillar tree directory path.
  721. Creates the directory if it does not yet exist.
  722. """
  723. dirname = pillar_tree_root_dir.join("prod")
  724. dirname.ensure(dir=True)
  725. RUNTIME_VARS.TMP_PRODENV_PILLAR_TREE = dirname.realpath().strpath
  726. return dirname
  727. @pytest.fixture(scope="session")
  728. def salt_syndic_master_config(request, salt_factories):
  729. root_dir = salt_factories._get_root_dir_for_daemon("syndic_master")
  730. with salt.utils.files.fopen(
  731. os.path.join(RUNTIME_VARS.CONF_DIR, "syndic_master")
  732. ) as rfh:
  733. config_defaults = yaml.deserialize(rfh.read())
  734. tests_known_hosts_file = root_dir.join("salt_ssh_known_hosts").strpath
  735. with salt.utils.files.fopen(tests_known_hosts_file, "w") as known_hosts:
  736. known_hosts.write("")
  737. config_defaults["root_dir"] = root_dir.strpath
  738. config_defaults["known_hosts_file"] = tests_known_hosts_file
  739. config_defaults["syndic_master"] = "localhost"
  740. config_defaults["transport"] = request.config.getoption("--transport")
  741. config_overrides = {}
  742. ext_pillar = []
  743. if salt.utils.platform.is_windows():
  744. ext_pillar.append(
  745. {
  746. "cmd_yaml": "type {0}".format(
  747. os.path.join(RUNTIME_VARS.FILES, "ext.yaml")
  748. )
  749. }
  750. )
  751. else:
  752. ext_pillar.append(
  753. {"cmd_yaml": "cat {0}".format(os.path.join(RUNTIME_VARS.FILES, "ext.yaml"))}
  754. )
  755. # We need to copy the extension modules into the new master root_dir or
  756. # it will be prefixed by it
  757. extension_modules_path = root_dir.join("extension_modules").strpath
  758. if not os.path.exists(extension_modules_path):
  759. shutil.copytree(
  760. os.path.join(RUNTIME_VARS.FILES, "extension_modules"),
  761. extension_modules_path,
  762. )
  763. # Copy the autosign_file to the new master root_dir
  764. autosign_file_path = root_dir.join("autosign_file").strpath
  765. shutil.copyfile(
  766. os.path.join(RUNTIME_VARS.FILES, "autosign_file"), autosign_file_path
  767. )
  768. # all read, only owner write
  769. autosign_file_permissions = (
  770. stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | stat.S_IWUSR
  771. )
  772. os.chmod(autosign_file_path, autosign_file_permissions)
  773. config_overrides.update(
  774. {
  775. "ext_pillar": ext_pillar,
  776. "extension_modules": extension_modules_path,
  777. "file_roots": {
  778. "base": [
  779. RUNTIME_VARS.TMP_STATE_TREE,
  780. os.path.join(RUNTIME_VARS.FILES, "file", "base"),
  781. ],
  782. # Alternate root to test __env__ choices
  783. "prod": [
  784. RUNTIME_VARS.TMP_PRODENV_STATE_TREE,
  785. os.path.join(RUNTIME_VARS.FILES, "file", "prod"),
  786. ],
  787. },
  788. "pillar_roots": {
  789. "base": [
  790. RUNTIME_VARS.TMP_PILLAR_TREE,
  791. os.path.join(RUNTIME_VARS.FILES, "pillar", "base"),
  792. ],
  793. "prod": [RUNTIME_VARS.TMP_PRODENV_PILLAR_TREE],
  794. },
  795. }
  796. )
  797. return salt_factories.configure_master(
  798. request,
  799. "syndic_master",
  800. order_masters=True,
  801. config_defaults=config_defaults,
  802. config_overrides=config_overrides,
  803. )
  804. @pytest.fixture(scope="session")
  805. def salt_syndic_config(request, salt_factories, salt_syndic_master_config):
  806. return salt_factories.configure_syndic(
  807. request, "syndic", master_of_masters_id="syndic_master"
  808. )
  809. @pytest.fixture(scope="session")
  810. def salt_master_config(request, salt_factories, salt_syndic_master_config):
  811. root_dir = salt_factories._get_root_dir_for_daemon("master")
  812. conf_dir = root_dir.join("conf").ensure(dir=True)
  813. with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, "master")) as rfh:
  814. config_defaults = yaml.deserialize(rfh.read())
  815. tests_known_hosts_file = root_dir.join("salt_ssh_known_hosts").strpath
  816. with salt.utils.files.fopen(tests_known_hosts_file, "w") as known_hosts:
  817. known_hosts.write("")
  818. config_defaults["root_dir"] = root_dir.strpath
  819. config_defaults["known_hosts_file"] = tests_known_hosts_file
  820. config_defaults["syndic_master"] = "localhost"
  821. config_defaults["transport"] = request.config.getoption("--transport")
  822. config_defaults["reactor"] = [
  823. {"salt/test/reactor": [os.path.join(RUNTIME_VARS.FILES, "reactor-test.sls")]}
  824. ]
  825. config_overrides = {}
  826. ext_pillar = []
  827. if salt.utils.platform.is_windows():
  828. ext_pillar.append(
  829. {
  830. "cmd_yaml": "type {0}".format(
  831. os.path.join(RUNTIME_VARS.FILES, "ext.yaml")
  832. )
  833. }
  834. )
  835. else:
  836. ext_pillar.append(
  837. {"cmd_yaml": "cat {0}".format(os.path.join(RUNTIME_VARS.FILES, "ext.yaml"))}
  838. )
  839. ext_pillar.append(
  840. {
  841. "file_tree": {
  842. "root_dir": os.path.join(RUNTIME_VARS.PILLAR_DIR, "base", "file_tree"),
  843. "follow_dir_links": False,
  844. "keep_newline": True,
  845. }
  846. }
  847. )
  848. config_overrides["pillar_opts"] = True
  849. # We need to copy the extension modules into the new master root_dir or
  850. # it will be prefixed by it
  851. extension_modules_path = root_dir.join("extension_modules").strpath
  852. if not os.path.exists(extension_modules_path):
  853. shutil.copytree(
  854. os.path.join(RUNTIME_VARS.FILES, "extension_modules"),
  855. extension_modules_path,
  856. )
  857. # Copy the autosign_file to the new master root_dir
  858. autosign_file_path = root_dir.join("autosign_file").strpath
  859. shutil.copyfile(
  860. os.path.join(RUNTIME_VARS.FILES, "autosign_file"), autosign_file_path
  861. )
  862. # all read, only owner write
  863. autosign_file_permissions = (
  864. stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | stat.S_IWUSR
  865. )
  866. os.chmod(autosign_file_path, autosign_file_permissions)
  867. config_overrides.update(
  868. {
  869. "ext_pillar": ext_pillar,
  870. "extension_modules": extension_modules_path,
  871. "file_roots": {
  872. "base": [
  873. RUNTIME_VARS.TMP_STATE_TREE,
  874. os.path.join(RUNTIME_VARS.FILES, "file", "base"),
  875. ],
  876. # Alternate root to test __env__ choices
  877. "prod": [
  878. RUNTIME_VARS.TMP_PRODENV_STATE_TREE,
  879. os.path.join(RUNTIME_VARS.FILES, "file", "prod"),
  880. ],
  881. },
  882. "pillar_roots": {
  883. "base": [
  884. RUNTIME_VARS.TMP_PILLAR_TREE,
  885. os.path.join(RUNTIME_VARS.FILES, "pillar", "base"),
  886. ],
  887. "prod": [RUNTIME_VARS.TMP_PRODENV_PILLAR_TREE],
  888. },
  889. }
  890. )
  891. # Let's copy over the test cloud config files and directories into the running master config directory
  892. for entry in os.listdir(RUNTIME_VARS.CONF_DIR):
  893. if not entry.startswith("cloud"):
  894. continue
  895. source = os.path.join(RUNTIME_VARS.CONF_DIR, entry)
  896. dest = conf_dir.join(entry).strpath
  897. if os.path.isdir(source):
  898. shutil.copytree(source, dest)
  899. else:
  900. shutil.copyfile(source, dest)
  901. return salt_factories.configure_master(
  902. request,
  903. "master",
  904. master_of_masters_id="syndic_master",
  905. config_defaults=config_defaults,
  906. config_overrides=config_overrides,
  907. )
  908. @pytest.fixture(scope="session")
  909. def salt_minion_config(request, salt_factories, salt_master_config):
  910. with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, "minion")) as rfh:
  911. config_defaults = yaml.deserialize(rfh.read())
  912. config_defaults["hosts.file"] = os.path.join(RUNTIME_VARS.TMP, "hosts")
  913. config_defaults["aliases.file"] = os.path.join(RUNTIME_VARS.TMP, "aliases")
  914. config_defaults["transport"] = request.config.getoption("--transport")
  915. config_overrides = {
  916. "file_roots": {
  917. "base": [
  918. RUNTIME_VARS.TMP_STATE_TREE,
  919. os.path.join(RUNTIME_VARS.FILES, "file", "base"),
  920. ],
  921. # Alternate root to test __env__ choices
  922. "prod": [
  923. RUNTIME_VARS.TMP_PRODENV_STATE_TREE,
  924. os.path.join(RUNTIME_VARS.FILES, "file", "prod"),
  925. ],
  926. },
  927. "pillar_roots": {
  928. "base": [
  929. RUNTIME_VARS.TMP_PILLAR_TREE,
  930. os.path.join(RUNTIME_VARS.FILES, "pillar", "base"),
  931. ],
  932. "prod": [RUNTIME_VARS.TMP_PRODENV_PILLAR_TREE],
  933. },
  934. }
  935. virtualenv_binary = _get_virtualenv_binary_path()
  936. if virtualenv_binary:
  937. config_overrides["venv_bin"] = virtualenv_binary
  938. return salt_factories.configure_minion(
  939. request,
  940. "minion",
  941. master_id="master",
  942. config_defaults=config_defaults,
  943. config_overrides=config_overrides,
  944. )
  945. @pytest.fixture(scope="session")
  946. def salt_sub_minion_config(request, salt_factories, salt_master_config):
  947. with salt.utils.files.fopen(
  948. os.path.join(RUNTIME_VARS.CONF_DIR, "sub_minion")
  949. ) as rfh:
  950. config_defaults = yaml.deserialize(rfh.read())
  951. config_defaults["hosts.file"] = os.path.join(RUNTIME_VARS.TMP, "hosts")
  952. config_defaults["aliases.file"] = os.path.join(RUNTIME_VARS.TMP, "aliases")
  953. config_defaults["transport"] = request.config.getoption("--transport")
  954. config_overrides = {
  955. "file_roots": {
  956. "base": [
  957. RUNTIME_VARS.TMP_STATE_TREE,
  958. os.path.join(RUNTIME_VARS.FILES, "file", "base"),
  959. ],
  960. # Alternate root to test __env__ choices
  961. "prod": [
  962. RUNTIME_VARS.TMP_PRODENV_STATE_TREE,
  963. os.path.join(RUNTIME_VARS.FILES, "file", "prod"),
  964. ],
  965. },
  966. "pillar_roots": {
  967. "base": [
  968. RUNTIME_VARS.TMP_PILLAR_TREE,
  969. os.path.join(RUNTIME_VARS.FILES, "pillar", "base"),
  970. ],
  971. "prod": [RUNTIME_VARS.TMP_PRODENV_PILLAR_TREE],
  972. },
  973. }
  974. virtualenv_binary = _get_virtualenv_binary_path()
  975. if virtualenv_binary:
  976. config_overrides["venv_bin"] = virtualenv_binary
  977. return salt_factories.configure_minion(
  978. request,
  979. "sub_minion",
  980. master_id="master",
  981. config_defaults=config_defaults,
  982. config_overrides=config_overrides,
  983. )
  984. @pytest.hookspec(firstresult=True)
  985. def pytest_saltfactories_syndic_configuration_defaults(
  986. request, factories_manager, root_dir, syndic_id, syndic_master_port
  987. ):
  988. """
  989. Hook which should return a dictionary tailored for the provided syndic_id with 3 keys:
  990. * `master`: The default config for the master running along with the syndic
  991. * `minion`: The default config for the master running along with the syndic
  992. * `syndic`: The default config for the master running along with the syndic
  993. Stops at the first non None result
  994. """
  995. factory_opts = {"master": None, "minion": None, "syndic": None}
  996. if syndic_id == "syndic":
  997. with salt.utils.files.fopen(
  998. os.path.join(RUNTIME_VARS.CONF_DIR, "syndic")
  999. ) as rfh:
  1000. opts = yaml.deserialize(rfh.read())
  1001. opts["hosts.file"] = os.path.join(RUNTIME_VARS.TMP, "hosts")
  1002. opts["aliases.file"] = os.path.join(RUNTIME_VARS.TMP, "aliases")
  1003. opts["transport"] = request.config.getoption("--transport")
  1004. factory_opts["syndic"] = opts
  1005. return factory_opts
  1006. @pytest.hookspec(firstresult=True)
  1007. def pytest_saltfactories_syndic_configuration_overrides(
  1008. request, factories_manager, syndic_id, config_defaults
  1009. ):
  1010. """
  1011. Hook which should return a dictionary tailored for the provided syndic_id.
  1012. This dictionary will override the default_options dictionary.
  1013. The returned dictionary should contain 3 keys:
  1014. * `master`: The config overrides for the master running along with the syndic
  1015. * `minion`: The config overrides for the master running along with the syndic
  1016. * `syndic`: The config overridess for the master running along with the syndic
  1017. The `default_options` parameter be None or have 3 keys, `master`, `minion`, `syndic`,
  1018. while will contain the default options for each of the daemons.
  1019. Stops at the first non None result
  1020. """
  1021. @pytest.fixture(scope="session", autouse=True)
  1022. def bridge_pytest_and_runtests(
  1023. reap_stray_processes,
  1024. base_env_state_tree_root_dir,
  1025. prod_env_state_tree_root_dir,
  1026. base_env_pillar_tree_root_dir,
  1027. prod_env_pillar_tree_root_dir,
  1028. salt_factories,
  1029. salt_syndic_master_config,
  1030. salt_syndic_config,
  1031. salt_master_config,
  1032. salt_minion_config,
  1033. salt_sub_minion_config,
  1034. ):
  1035. # Make sure unittest2 uses the pytest generated configuration
  1036. RUNTIME_VARS.RUNTIME_CONFIGS["master"] = freeze(salt_master_config)
  1037. RUNTIME_VARS.RUNTIME_CONFIGS["minion"] = freeze(salt_minion_config)
  1038. RUNTIME_VARS.RUNTIME_CONFIGS["sub_minion"] = freeze(salt_sub_minion_config)
  1039. RUNTIME_VARS.RUNTIME_CONFIGS["syndic_master"] = freeze(salt_syndic_master_config)
  1040. RUNTIME_VARS.RUNTIME_CONFIGS["syndic"] = freeze(salt_syndic_config)
  1041. RUNTIME_VARS.RUNTIME_CONFIGS["client_config"] = freeze(
  1042. salt.config.client_config(salt_master_config["conf_file"])
  1043. )
  1044. # Make sure unittest2 classes know their paths
  1045. RUNTIME_VARS.TMP_ROOT_DIR = salt_factories.root_dir.realpath().strpath
  1046. RUNTIME_VARS.TMP_CONF_DIR = os.path.dirname(salt_master_config["conf_file"])
  1047. RUNTIME_VARS.TMP_MINION_CONF_DIR = os.path.dirname(salt_minion_config["conf_file"])
  1048. RUNTIME_VARS.TMP_SUB_MINION_CONF_DIR = os.path.dirname(
  1049. salt_sub_minion_config["conf_file"]
  1050. )
  1051. RUNTIME_VARS.TMP_SYNDIC_MASTER_CONF_DIR = os.path.dirname(
  1052. salt_syndic_master_config["conf_file"]
  1053. )
  1054. RUNTIME_VARS.TMP_SYNDIC_MINION_CONF_DIR = os.path.dirname(
  1055. salt_syndic_config["conf_file"]
  1056. )
  1057. # <---- Salt Configuration -------------------------------------------------------------------------------------------
  1058. # <---- Fixtures Overrides -------------------------------------------------------------------------------------------
  1059. # ----- Custom Grains Mark Evaluator -------------------------------------------------------------------------------->
  1060. class GrainsMarkEvaluator(MarkEvaluator):
  1061. _cached_grains = None
  1062. def _getglobals(self):
  1063. item_globals = super(GrainsMarkEvaluator, self)._getglobals()
  1064. if GrainsMarkEvaluator._cached_grains is None:
  1065. sminion = create_sminion()
  1066. GrainsMarkEvaluator._cached_grains = sminion.opts["grains"].copy()
  1067. item_globals["grains"] = GrainsMarkEvaluator._cached_grains.copy()
  1068. return item_globals
  1069. # Patch PyTest's skipping MarkEvaluator to use our GrainsMarkEvaluator
  1070. _pytest.skipping.MarkEvaluator = GrainsMarkEvaluator
  1071. # <---- Custom Grains Mark Evaluator ---------------------------------------------------------------------------------
  1072. # ----- Custom Fixtures --------------------------------------------------------------------------------------------->
  1073. @pytest.fixture(scope="session")
  1074. def reap_stray_processes():
  1075. # Run tests
  1076. yield
  1077. children = psutil.Process(os.getpid()).children(recursive=True)
  1078. if not children:
  1079. log.info("No astray processes found")
  1080. return
  1081. def on_terminate(proc):
  1082. log.debug("Process %s terminated with exit code %s", proc, proc.returncode)
  1083. if children:
  1084. # Reverse the order, sublings first, parents after
  1085. children.reverse()
  1086. log.warning(
  1087. "Test suite left %d astray processes running. Killing those processes:\n%s",
  1088. len(children),
  1089. pprint.pformat(children),
  1090. )
  1091. _, alive = psutil.wait_procs(children, timeout=3, callback=on_terminate)
  1092. for child in alive:
  1093. try:
  1094. child.kill()
  1095. except psutil.NoSuchProcess:
  1096. continue
  1097. _, alive = psutil.wait_procs(alive, timeout=3, callback=on_terminate)
  1098. if alive:
  1099. # Give up
  1100. for child in alive:
  1101. log.warning(
  1102. "Process %s survived SIGKILL, giving up:\n%s",
  1103. child,
  1104. pprint.pformat(child.as_dict()),
  1105. )
  1106. @pytest.fixture(scope="session")
  1107. def grains(request):
  1108. sminion = create_sminion()
  1109. return sminion.opts["grains"].copy()
  1110. # <---- Custom Fixtures ----------------------------------------------------------------------------------------------