conftest.py 50 KB

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