conftest.py 47 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307
  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=ungrouped-imports,wrong-import-position,redefined-outer-name,missing-docstring
  9. # Import python libs
  10. from __future__ import absolute_import, print_function, unicode_literals
  11. import os
  12. import sys
  13. import stat
  14. import pprint
  15. import shutil
  16. import socket
  17. import fnmatch
  18. import logging
  19. import tempfile
  20. import textwrap
  21. from contextlib import contextmanager
  22. TESTS_DIR = os.path.dirname(os.path.normpath(os.path.abspath(__file__)))
  23. CODE_DIR = os.path.dirname(TESTS_DIR)
  24. # Change to code checkout directory
  25. os.chdir(CODE_DIR)
  26. # Make sure the current directory is the first item in sys.path
  27. if CODE_DIR in sys.path:
  28. sys.path.remove(CODE_DIR)
  29. sys.path.insert(0, CODE_DIR)
  30. # Import test libs
  31. from tests.support.runtime import RUNTIME_VARS
  32. from tests.support.sminion import create_sminion
  33. # Import pytest libs
  34. import pytest
  35. import _pytest.logging
  36. import _pytest.skipping
  37. from _pytest.mark.evaluate import MarkEvaluator
  38. # Import 3rd-party libs
  39. import yaml
  40. import psutil
  41. from salt.ext import six
  42. # Import salt libs
  43. import salt.config
  44. import salt.loader
  45. import salt.log.setup
  46. import salt.log.mixins
  47. import salt.utils.files
  48. import salt.utils.path
  49. import salt.utils.platform
  50. import salt.utils.win_functions
  51. from salt.serializers import yaml
  52. from salt.utils.immutabletypes import freeze
  53. # Import Pytest Salt libs
  54. from pytestsalt.utils import cli_scripts
  55. # Coverage
  56. if 'COVERAGE_PROCESS_START' in os.environ:
  57. MAYBE_RUN_COVERAGE = True
  58. COVERAGERC_FILE = os.environ['COVERAGE_PROCESS_START']
  59. else:
  60. COVERAGERC_FILE = os.path.join(CODE_DIR, '.coveragerc')
  61. MAYBE_RUN_COVERAGE = sys.argv[0].endswith('pytest.py') or '_COVERAGE_RCFILE' in os.environ
  62. if MAYBE_RUN_COVERAGE:
  63. # Flag coverage to track suprocesses by pointing it to the right .coveragerc file
  64. os.environ[str('COVERAGE_PROCESS_START')] = str(COVERAGERC_FILE)
  65. # Define the pytest plugins we rely on
  66. # pylint: disable=invalid-name
  67. pytest_plugins = ['tempdir', 'helpers_namespace', 'salt-runtests-bridge']
  68. # Define where not to collect tests from
  69. collect_ignore = ['setup.py']
  70. # pylint: enable=invalid-name
  71. # Patch PyTest logging handlers
  72. # pylint: disable=protected-access,too-many-ancestors
  73. class LogCaptureHandler(salt.log.mixins.ExcInfoOnLogLevelFormatMixIn,
  74. _pytest.logging.LogCaptureHandler):
  75. '''
  76. Subclassing PyTest's LogCaptureHandler in order to add the
  77. exc_info_on_loglevel functionality.
  78. '''
  79. _pytest.logging.LogCaptureHandler = LogCaptureHandler
  80. class LiveLoggingStreamHandler(salt.log.mixins.ExcInfoOnLogLevelFormatMixIn,
  81. _pytest.logging._LiveLoggingStreamHandler):
  82. '''
  83. Subclassing PyTest's LiveLoggingStreamHandler in order to add the
  84. exc_info_on_loglevel functionality.
  85. '''
  86. _pytest.logging._LiveLoggingStreamHandler = LiveLoggingStreamHandler
  87. # pylint: enable=protected-access,too-many-ancestors
  88. # Reset logging root handlers
  89. for handler in logging.root.handlers[:]:
  90. logging.root.removeHandler(handler)
  91. # Reset the root logger to it's default level(because salt changed it)
  92. logging.root.setLevel(logging.WARNING)
  93. log = logging.getLogger('salt.testsuite')
  94. # ----- PyTest Tempdir Plugin Hooks --------------------------------------------------------------------------------->
  95. def pytest_tempdir_temproot():
  96. # Taken from https://github.com/saltstack/salt/blob/v2019.2.0/tests/support/paths.py
  97. # Avoid ${TMPDIR} and gettempdir() on MacOS as they yield a base path too long
  98. # for unix sockets: ``error: AF_UNIX path too long``
  99. # Gentoo Portage prefers ebuild tests are rooted in ${TMPDIR}
  100. if not sys.platform.startswith('darwin'):
  101. tempdir = os.environ.get('TMPDIR') or tempfile.gettempdir()
  102. else:
  103. tempdir = '/tmp'
  104. return os.path.abspath(os.path.realpath(tempdir))
  105. def pytest_tempdir_basename():
  106. '''
  107. Return the temporary directory basename for the salt test suite.
  108. '''
  109. return 'salt-tests-tmpdir'
  110. # <---- PyTest Tempdir Plugin Hooks ----------------------------------------------------------------------------------
  111. # ----- CLI Options Setup ------------------------------------------------------------------------------------------->
  112. def pytest_addoption(parser):
  113. '''
  114. register argparse-style options and ini-style config values.
  115. '''
  116. parser.addoption(
  117. '--sysinfo',
  118. default=False,
  119. action='store_true',
  120. help='Print some system information.'
  121. )
  122. parser.addoption(
  123. '--transport',
  124. default='zeromq',
  125. choices=('zeromq', 'tcp'),
  126. help=('Select which transport to run the integration tests with, '
  127. 'zeromq or tcp. Default: %default')
  128. )
  129. test_selection_group = parser.getgroup('Tests Selection')
  130. test_selection_group.addoption(
  131. '--ssh',
  132. '--ssh-tests',
  133. dest='ssh',
  134. action='store_true',
  135. default=False,
  136. help='Run salt-ssh tests. These tests will spin up a temporary '
  137. 'SSH server on your machine. In certain environments, this '
  138. 'may be insecure! Default: False'
  139. )
  140. test_selection_group.addoption(
  141. '--proxy',
  142. '--proxy-tests',
  143. dest='proxy',
  144. action='store_true',
  145. default=False,
  146. help='Run proxy tests'
  147. )
  148. test_selection_group.addoption(
  149. '--run-destructive',
  150. action='store_true',
  151. default=False,
  152. help='Run destructive tests. These tests can include adding '
  153. 'or removing users from your system for example. '
  154. 'Default: False'
  155. )
  156. test_selection_group.addoption(
  157. '--run-expensive',
  158. action='store_true',
  159. default=False,
  160. help='Run expensive tests. These tests usually involve costs '
  161. 'like for example bootstrapping a cloud VM. '
  162. 'Default: False'
  163. )
  164. output_options_group = parser.getgroup('Output Options')
  165. output_options_group.addoption(
  166. '--output-columns',
  167. default=80,
  168. type=int,
  169. help='Number of maximum columns to use on the output'
  170. )
  171. output_options_group.addoption(
  172. '--no-colors',
  173. '--no-colours',
  174. default=False,
  175. action='store_true',
  176. help='Disable colour printing.'
  177. )
  178. # ----- Test Groups --------------------------------------------------------------------------------------------->
  179. # This will allow running the tests in chunks
  180. test_selection_group.addoption(
  181. '--test-group-count', dest='test-group-count', type=int,
  182. help='The number of groups to split the tests into'
  183. )
  184. test_selection_group.addoption(
  185. '--test-group', dest='test-group', type=int,
  186. help='The group of tests that should be executed'
  187. )
  188. # <---- Test Groups ----------------------------------------------------------------------------------------------
  189. # <---- CLI Options Setup --------------------------------------------------------------------------------------------
  190. # ----- Register Markers -------------------------------------------------------------------------------------------->
  191. @pytest.mark.trylast
  192. def pytest_configure(config):
  193. '''
  194. called after command line options have been parsed
  195. and all plugins and initial conftest files been loaded.
  196. '''
  197. for dirname in os.listdir(CODE_DIR):
  198. if not os.path.isdir(dirname):
  199. continue
  200. if dirname != 'tests':
  201. config.addinivalue_line('norecursedirs', os.path.join(CODE_DIR, dirname))
  202. config.addinivalue_line('norecursedirs', os.path.join(CODE_DIR, 'tests/kitchen'))
  203. # pylint: disable=protected-access
  204. # Expose the markers we use to pytest CLI
  205. config.addinivalue_line(
  206. 'markers',
  207. 'destructive_test: Run destructive tests. These tests can include adding '
  208. 'or removing users from your system for example.'
  209. )
  210. config.addinivalue_line(
  211. 'markers',
  212. 'skip_if_not_root: Skip if the current user is not `root`.'
  213. )
  214. config.addinivalue_line(
  215. 'markers',
  216. 'skip_if_binaries_missing(*binaries, check_all=False, message=None): Skip if '
  217. 'any of the passed binaries are not found in path. If \'check_all\' is '
  218. '\'True\', then all binaries must be found.'
  219. )
  220. config.addinivalue_line(
  221. 'markers',
  222. 'requires_network(only_local_network=False): Skip if no networking is set up. '
  223. 'If \'only_local_network\' is \'True\', only the local network is checked.'
  224. )
  225. config.addinivalue_line(
  226. 'markers',
  227. 'requires_salt_modules(*required_module_names): Skip if at least one module is not available. '
  228. )
  229. # Make sure the test suite "knows" this is a pytest test run
  230. RUNTIME_VARS.PYTEST_SESSION = True
  231. # Provide a global timeout for each test(pytest-timeout).
  232. #if config._env_timeout is None:
  233. # # If no timeout is set, set it to the default timeout value
  234. # # Right now, we set it to 3 minutes which is absurd, but let's see how it goes
  235. # config._env_timeout = 3 * 60
  236. # We always want deferred timeouts. Ie, only take into account the test function time
  237. # to run, exclude fixture setup/teardown
  238. #config._env_timeout_func_only = True
  239. # <---- Register Markers ---------------------------------------------------------------------------------------------
  240. # ----- PyTest Tweaks ----------------------------------------------------------------------------------------------->
  241. def set_max_open_files_limits(min_soft=3072, min_hard=4096):
  242. # Get current limits
  243. if salt.utils.platform.is_windows():
  244. import win32file
  245. prev_hard = win32file._getmaxstdio()
  246. prev_soft = 512
  247. else:
  248. import resource
  249. prev_soft, prev_hard = resource.getrlimit(resource.RLIMIT_NOFILE)
  250. # Check minimum required limits
  251. set_limits = False
  252. if prev_soft < min_soft:
  253. soft = min_soft
  254. set_limits = True
  255. else:
  256. soft = prev_soft
  257. if prev_hard < min_hard:
  258. hard = min_hard
  259. set_limits = True
  260. else:
  261. hard = prev_hard
  262. # Increase limits
  263. if set_limits:
  264. log.debug(
  265. ' * Max open files settings is too low (soft: %s, hard: %s) for running the tests. '
  266. 'Trying to raise the limits to soft: %s, hard: %s',
  267. prev_soft,
  268. prev_hard,
  269. soft,
  270. hard
  271. )
  272. try:
  273. if salt.utils.platform.is_windows():
  274. hard = 2048 if hard > 2048 else hard
  275. win32file._setmaxstdio(hard)
  276. else:
  277. resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard))
  278. except Exception as err:
  279. log.error(
  280. 'Failed to raise the max open files settings -> %s. Please issue the following command '
  281. 'on your console: \'ulimit -u %s\'',
  282. err,
  283. soft,
  284. )
  285. exit(1)
  286. return soft, hard
  287. def pytest_report_header():
  288. soft, hard = set_max_open_files_limits()
  289. return 'max open files; soft: {}; hard: {}'.format(soft, hard)
  290. def pytest_runtest_logstart(nodeid):
  291. '''
  292. implements the runtest_setup/call/teardown protocol for
  293. the given test item, including capturing exceptions and calling
  294. reporting hooks.
  295. '''
  296. log.debug('>>>>> START >>>>> %s', nodeid)
  297. def pytest_runtest_logfinish(nodeid):
  298. '''
  299. called after ``pytest_runtest_call``
  300. '''
  301. log.debug('<<<<< END <<<<<<< %s', nodeid)
  302. # <---- PyTest Tweaks ------------------------------------------------------------------------------------------------
  303. # ----- Test Setup -------------------------------------------------------------------------------------------------->
  304. def _has_unittest_attr(item, attr):
  305. # XXX: This is a hack while we support both runtests.py and PyTest
  306. if hasattr(item.obj, attr):
  307. return True
  308. if item.cls and hasattr(item.cls, attr):
  309. return True
  310. if item.parent and hasattr(item.parent.obj, attr):
  311. return True
  312. return False
  313. @pytest.hookimpl(tryfirst=True)
  314. def pytest_runtest_setup(item):
  315. '''
  316. Fixtures injection based on markers or test skips based on CLI arguments
  317. '''
  318. destructive_tests_marker = item.get_closest_marker('destructive_test')
  319. if destructive_tests_marker is not None:
  320. if item.config.getoption('--run-destructive') is False:
  321. pytest.skip('Destructive tests are disabled')
  322. expensive_tests_marker = item.get_closest_marker('expensive_test')
  323. if expensive_tests_marker is not None:
  324. if item.config.getoption('--run-expensive') is False:
  325. pytest.skip('Expensive tests are disabled')
  326. skip_if_not_root_marker = item.get_closest_marker('skip_if_not_root')
  327. if skip_if_not_root_marker is not None:
  328. if not sys.platform.startswith('win'):
  329. if os.getuid() != 0:
  330. pytest.skip('You must be logged in as root to run this test')
  331. else:
  332. current_user = salt.utils.win_functions.get_current_user()
  333. if current_user != 'SYSTEM':
  334. if not salt.utils.win_functions.is_admin(current_user):
  335. pytest.skip('You must be logged in as an Administrator to run this test')
  336. skip_if_binaries_missing_marker = item.get_closest_marker('skip_if_binaries_missing')
  337. if skip_if_binaries_missing_marker is not None:
  338. binaries = skip_if_binaries_missing_marker.args
  339. if len(binaries) == 1:
  340. if isinstance(binaries[0], (list, tuple, set, frozenset)):
  341. binaries = binaries[0]
  342. check_all = skip_if_binaries_missing_marker.kwargs.get('check_all', False)
  343. message = skip_if_binaries_missing_marker.kwargs.get('message', None)
  344. if check_all:
  345. for binary in binaries:
  346. if salt.utils.path.which(binary) is None:
  347. pytest.skip(
  348. '{0}The "{1}" binary was not found'.format(
  349. message and '{0}. '.format(message) or '',
  350. binary
  351. )
  352. )
  353. elif salt.utils.path.which_bin(binaries) is None:
  354. pytest.skip(
  355. '{0}None of the following binaries was found: {1}'.format(
  356. message and '{0}. '.format(message) or '',
  357. ', '.join(binaries)
  358. )
  359. )
  360. requires_network_marker = item.get_closest_marker('requires_network')
  361. if requires_network_marker is not None:
  362. only_local_network = requires_network_marker.kwargs.get('only_local_network', False)
  363. has_local_network = False
  364. # First lets try if we have a local network. Inspired in verify_socket
  365. try:
  366. pubsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  367. retsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  368. pubsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  369. pubsock.bind(('', 18000))
  370. pubsock.close()
  371. retsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  372. retsock.bind(('', 18001))
  373. retsock.close()
  374. has_local_network = True
  375. except socket.error:
  376. # I wonder if we just have IPV6 support?
  377. try:
  378. pubsock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
  379. retsock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
  380. pubsock.setsockopt(
  381. socket.SOL_SOCKET, socket.SO_REUSEADDR, 1
  382. )
  383. pubsock.bind(('', 18000))
  384. pubsock.close()
  385. retsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  386. retsock.bind(('', 18001))
  387. retsock.close()
  388. has_local_network = True
  389. except socket.error:
  390. # Let's continue
  391. pass
  392. if only_local_network is True:
  393. if has_local_network is False:
  394. # Since we're only supposed to check local network, and no
  395. # local network was detected, skip the test
  396. pytest.skip('No local network was detected')
  397. # We are using the google.com DNS records as numerical IPs to avoid
  398. # DNS lookups which could greatly slow down this check
  399. for addr in ('173.194.41.198', '173.194.41.199', '173.194.41.200',
  400. '173.194.41.201', '173.194.41.206', '173.194.41.192',
  401. '173.194.41.193', '173.194.41.194', '173.194.41.195',
  402. '173.194.41.196', '173.194.41.197'):
  403. try:
  404. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  405. sock.settimeout(0.25)
  406. sock.connect((addr, 80))
  407. sock.close()
  408. # We connected? Stop the loop
  409. break
  410. except socket.error:
  411. # Let's check the next IP
  412. continue
  413. else:
  414. pytest.skip('No internet network connection was detected')
  415. requires_salt_modules_marker = item.get_closest_marker('requires_salt_modules')
  416. if requires_salt_modules_marker is not None:
  417. required_salt_modules = requires_salt_modules_marker.args
  418. if len(required_salt_modules) == 1 and isinstance(required_salt_modules[0], (list, tuple, set)):
  419. required_salt_modules = required_salt_modules[0]
  420. required_salt_modules = set(required_salt_modules)
  421. sminion = create_sminion()
  422. available_modules = list(sminion.functions)
  423. not_available_modules = set()
  424. try:
  425. cached_not_available_modules = sminion.__not_availiable_modules__
  426. except AttributeError:
  427. cached_not_available_modules = sminion.__not_availiable_modules__ = set()
  428. if cached_not_available_modules:
  429. for not_available_module in cached_not_available_modules:
  430. if not_available_module in required_salt_modules:
  431. not_available_modules.add(not_available_module)
  432. required_salt_modules.remove(not_available_module)
  433. for required_module_name in required_salt_modules:
  434. search_name = required_module_name
  435. if '.' not in search_name:
  436. search_name += '.*'
  437. if not fnmatch.filter(available_modules, search_name):
  438. not_available_modules.add(required_module_name)
  439. cached_not_available_modules.add(required_module_name)
  440. if not_available_modules:
  441. if len(not_available_modules) == 1:
  442. pytest.skip('Salt module \'{}\' is not available'.format(*not_available_modules))
  443. pytest.skip('Salt modules not available: {}'.format(', '.join(not_available_modules)))
  444. # <---- Test Setup ---------------------------------------------------------------------------------------------------
  445. # ----- Test Groups Selection --------------------------------------------------------------------------------------->
  446. def get_group_size(total_items, total_groups):
  447. '''
  448. Return the group size.
  449. '''
  450. return int(total_items / total_groups)
  451. def get_group(items, group_count, group_size, group_id):
  452. '''
  453. Get the items from the passed in group based on group size.
  454. '''
  455. start = group_size * (group_id - 1)
  456. end = start + group_size
  457. total_items = len(items)
  458. if start >= total_items:
  459. pytest.fail("Invalid test-group argument. start({})>=total_items({})".format(start, total_items))
  460. elif start < 0:
  461. pytest.fail("Invalid test-group argument. Start({})<0".format(start))
  462. if group_count == group_id and end < total_items:
  463. # If this is the last group and there are still items to test
  464. # which don't fit in this group based on the group items count
  465. # add them anyway
  466. end = total_items
  467. return items[start:end]
  468. @pytest.hookimpl(hookwrapper=True, tryfirst=True)
  469. def pytest_collection_modifyitems(config, items):
  470. # Let PyTest or other plugins handle the initial collection
  471. yield
  472. group_count = config.getoption('test-group-count')
  473. group_id = config.getoption('test-group')
  474. if not group_count or not group_id:
  475. # We're not selection tests using groups, don't do any filtering
  476. return
  477. total_items = len(items)
  478. group_size = get_group_size(total_items, group_count)
  479. tests_in_group = get_group(items, group_count, group_size, group_id)
  480. # Replace all items in the list
  481. items[:] = tests_in_group
  482. terminal_reporter = config.pluginmanager.get_plugin('terminalreporter')
  483. terminal_reporter.write(
  484. 'Running test group #{0} ({1} tests)\n'.format(
  485. group_id,
  486. len(items)
  487. ),
  488. yellow=True
  489. )
  490. # <---- Test Groups Selection ----------------------------------------------------------------------------------------
  491. # ----- Pytest Helpers ---------------------------------------------------------------------------------------------->
  492. if six.PY2:
  493. # backport mock_open from the python 3 unittest.mock library so that we can
  494. # mock read, readline, readlines, and file iteration properly
  495. file_spec = None
  496. def _iterate_read_data(read_data):
  497. # Helper for mock_open:
  498. # Retrieve lines from read_data via a generator so that separate calls to
  499. # readline, read, and readlines are properly interleaved
  500. data_as_list = ['{0}\n'.format(l) for l in read_data.split('\n')]
  501. if data_as_list[-1] == '\n':
  502. # If the last line ended in a newline, the list comprehension will have an
  503. # extra entry that's just a newline. Remove this.
  504. data_as_list = data_as_list[:-1]
  505. else:
  506. # If there wasn't an extra newline by itself, then the file being
  507. # emulated doesn't have a newline to end the last line remove the
  508. # newline that our naive format() added
  509. data_as_list[-1] = data_as_list[-1][:-1]
  510. for line in data_as_list:
  511. yield line
  512. @pytest.helpers.mock.register
  513. def mock_open(mock=None, read_data=''):
  514. """
  515. A helper function to create a mock to replace the use of `open`. It works
  516. for `open` called directly or used as a context manager.
  517. The `mock` argument is the mock object to configure. If `None` (the
  518. default) then a `MagicMock` will be created for you, with the API limited
  519. to methods or attributes available on standard file handles.
  520. `read_data` is a string for the `read` methoddline`, and `readlines` of the
  521. file handle to return. This is an empty string by default.
  522. """
  523. _mock = pytest.importorskip('mock', minversion='2.0.0')
  524. # pylint: disable=unused-argument
  525. def _readlines_side_effect(*args, **kwargs):
  526. if handle.readlines.return_value is not None:
  527. return handle.readlines.return_value
  528. return list(_data)
  529. def _read_side_effect(*args, **kwargs):
  530. if handle.read.return_value is not None:
  531. return handle.read.return_value
  532. return ''.join(_data)
  533. # pylint: enable=unused-argument
  534. def _readline_side_effect():
  535. if handle.readline.return_value is not None:
  536. while True:
  537. yield handle.readline.return_value
  538. for line in _data:
  539. yield line
  540. global file_spec # pylint: disable=global-statement
  541. if file_spec is None:
  542. file_spec = file # pylint: disable=undefined-variable
  543. if mock is None:
  544. mock = _mock.MagicMock(name='open', spec=open)
  545. handle = _mock.MagicMock(spec=file_spec)
  546. handle.__enter__.return_value = handle
  547. _data = _iterate_read_data(read_data)
  548. handle.write.return_value = None
  549. handle.read.return_value = None
  550. handle.readline.return_value = None
  551. handle.readlines.return_value = None
  552. handle.read.side_effect = _read_side_effect
  553. handle.readline.side_effect = _readline_side_effect()
  554. handle.readlines.side_effect = _readlines_side_effect
  555. mock.return_value = handle
  556. return mock
  557. else:
  558. @pytest.helpers.mock.register
  559. def mock_open(mock=None, read_data=''):
  560. _mock = pytest.importorskip('mock', minversion='2.0.0')
  561. return _mock.mock_open(mock=mock, read_data=read_data)
  562. @pytest.helpers.register
  563. @contextmanager
  564. def temp_directory(name=None):
  565. if name is not None:
  566. directory_path = os.path.join(RUNTIME_VARS.TMP, name)
  567. else:
  568. directory_path = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
  569. yield directory_path
  570. shutil.rmtree(directory_path, ignore_errors=True)
  571. @pytest.helpers.register
  572. @contextmanager
  573. def temp_file(name, contents=None, directory=None, strip_first_newline=True):
  574. if directory is None:
  575. directory = RUNTIME_VARS.TMP
  576. file_path = os.path.join(directory, name)
  577. file_directory = os.path.dirname(file_path)
  578. if contents is not None:
  579. if contents:
  580. if contents.startswith('\n') and strip_first_newline:
  581. contents = contents[1:]
  582. file_contents = textwrap.dedent(contents)
  583. else:
  584. file_contents = contents
  585. try:
  586. if not os.path.isdir(file_directory):
  587. os.makedirs(file_directory)
  588. if contents is not None:
  589. with salt.utils.files.fopen(file_path, 'w') as wfh:
  590. wfh.write(file_contents)
  591. yield file_path
  592. finally:
  593. try:
  594. os.unlink(file_path)
  595. except OSError:
  596. # Already deleted
  597. pass
  598. @pytest.helpers.register
  599. def temp_state_file(name, contents, saltenv='base', strip_first_newline=True):
  600. if saltenv == 'base':
  601. directory = RUNTIME_VARS.TMP_STATE_TREE
  602. elif saltenv == 'prod':
  603. directory = RUNTIME_VARS.TMP_PRODENV_STATE_TREE
  604. else:
  605. raise RuntimeError('"saltenv" can only be "base" or "prod", not "{}"'.format(saltenv))
  606. return temp_file(name, contents, directory=directory, strip_first_newline=strip_first_newline)
  607. # <---- Pytest Helpers -----------------------------------------------------------------------------------------------
  608. # ----- Fixtures Overrides ------------------------------------------------------------------------------------------>
  609. # ----- Generate CLI Scripts ---------------------------------------------------------------------------------------->
  610. @pytest.fixture(scope='session')
  611. def cli_master_script_name():
  612. '''
  613. Return the CLI script basename
  614. '''
  615. return 'cli_salt_master.py'
  616. @pytest.fixture(scope='session')
  617. def cli_minion_script_name():
  618. '''
  619. Return the CLI script basename
  620. '''
  621. return 'cli_salt_minion.py'
  622. @pytest.fixture(scope='session')
  623. def cli_salt_script_name():
  624. '''
  625. Return the CLI script basename
  626. '''
  627. return 'cli_salt.py'
  628. @pytest.fixture(scope='session')
  629. def cli_run_script_name():
  630. '''
  631. Return the CLI script basename
  632. '''
  633. return 'cli_salt_run.py'
  634. @pytest.fixture(scope='session')
  635. def cli_key_script_name():
  636. '''
  637. Return the CLI script basename
  638. '''
  639. return 'cli_salt_key.py'
  640. @pytest.fixture(scope='session')
  641. def cli_call_script_name():
  642. '''
  643. Return the CLI script basename
  644. '''
  645. return 'cli_salt_call.py'
  646. @pytest.fixture(scope='session')
  647. def cli_syndic_script_name():
  648. '''
  649. Return the CLI script basename
  650. '''
  651. return 'cli_salt_syndic.py'
  652. @pytest.fixture(scope='session')
  653. def cli_ssh_script_name():
  654. '''
  655. Return the CLI script basename
  656. '''
  657. return 'cli_salt_ssh.py'
  658. @pytest.fixture(scope='session')
  659. def cli_proxy_script_name():
  660. '''
  661. Return the CLI script basename
  662. '''
  663. return 'cli_salt_proxy.py'
  664. @pytest.fixture(scope='session')
  665. def cli_bin_dir(tempdir,
  666. request,
  667. python_executable_path,
  668. cli_master_script_name,
  669. cli_minion_script_name,
  670. cli_salt_script_name,
  671. cli_call_script_name,
  672. cli_key_script_name,
  673. cli_run_script_name,
  674. cli_ssh_script_name,
  675. cli_syndic_script_name,
  676. cli_proxy_script_name):
  677. '''
  678. Return the path to the CLI script directory to use
  679. '''
  680. tmp_cli_scripts_dir = tempdir.join('cli-scrips-bin')
  681. # Make sure we re-write the scripts every time we start the tests
  682. shutil.rmtree(tmp_cli_scripts_dir.strpath, ignore_errors=True)
  683. tmp_cli_scripts_dir.ensure(dir=True)
  684. cli_bin_dir_path = tmp_cli_scripts_dir.strpath
  685. # Now that we have the CLI directory created, lets generate the required CLI scripts to run salt's test suite
  686. for script_name in (cli_master_script_name,
  687. cli_minion_script_name,
  688. cli_call_script_name,
  689. cli_key_script_name,
  690. cli_run_script_name,
  691. cli_salt_script_name,
  692. cli_ssh_script_name,
  693. cli_syndic_script_name,
  694. cli_proxy_script_name):
  695. original_script_name = os.path.splitext(script_name)[0].split('cli_')[-1].replace('_', '-')
  696. cli_scripts.generate_script(
  697. bin_dir=cli_bin_dir_path,
  698. script_name=original_script_name,
  699. executable=sys.executable,
  700. code_dir=CODE_DIR,
  701. inject_sitecustomize=MAYBE_RUN_COVERAGE
  702. )
  703. # Return the CLI bin dir value
  704. return cli_bin_dir_path
  705. # <---- Generate CLI Scripts -----------------------------------------------------------------------------------------
  706. # ----- Salt Configuration ------------------------------------------------------------------------------------------>
  707. @pytest.fixture(scope='session')
  708. def session_master_of_masters_id():
  709. '''
  710. Returns the master of masters id
  711. '''
  712. return 'syndic_master'
  713. @pytest.fixture(scope='session')
  714. def session_master_id():
  715. '''
  716. Returns the session scoped master id
  717. '''
  718. return 'master'
  719. @pytest.fixture(scope='session')
  720. def session_minion_id():
  721. '''
  722. Returns the session scoped minion id
  723. '''
  724. return 'minion'
  725. @pytest.fixture(scope='session')
  726. def session_secondary_minion_id():
  727. '''
  728. Returns the session scoped secondary minion id
  729. '''
  730. return 'sub_minion'
  731. @pytest.fixture(scope='session')
  732. def session_syndic_id():
  733. '''
  734. Returns the session scoped syndic id
  735. '''
  736. return 'syndic'
  737. @pytest.fixture(scope='session')
  738. def session_proxy_id():
  739. '''
  740. Returns the session scoped proxy id
  741. '''
  742. return 'proxytest'
  743. @pytest.fixture(scope='session')
  744. def salt_fail_hard():
  745. '''
  746. Return the salt fail hard value
  747. '''
  748. return True
  749. @pytest.fixture(scope='session')
  750. def session_master_default_options(request, session_root_dir):
  751. with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, 'master')) as rfh:
  752. opts = yaml.deserialize(rfh.read())
  753. tests_known_hosts_file = session_root_dir.join('salt_ssh_known_hosts').strpath
  754. with salt.utils.files.fopen(tests_known_hosts_file, 'w') as known_hosts:
  755. known_hosts.write('')
  756. opts['known_hosts_file'] = tests_known_hosts_file
  757. opts['syndic_master'] = 'localhost'
  758. opts['transport'] = request.config.getoption('--transport')
  759. # Config settings to test `event_return`
  760. if 'returner_dirs' not in opts:
  761. opts['returner_dirs'] = []
  762. opts['returner_dirs'].append(os.path.join(RUNTIME_VARS.FILES, 'returners'))
  763. opts['event_return'] = 'runtests_noop'
  764. return opts
  765. @pytest.fixture(scope='session')
  766. def session_master_config_overrides(session_root_dir):
  767. ext_pillar = []
  768. if salt.utils.platform.is_windows():
  769. ext_pillar.append(
  770. {'cmd_yaml': 'type {0}'.format(os.path.join(RUNTIME_VARS.FILES, 'ext.yaml'))}
  771. )
  772. else:
  773. ext_pillar.append(
  774. {'cmd_yaml': 'cat {0}'.format(os.path.join(RUNTIME_VARS.FILES, 'ext.yaml'))}
  775. )
  776. ext_pillar.append(
  777. {'file_tree':
  778. {
  779. 'root_dir': os.path.join(RUNTIME_VARS.PILLAR_DIR, 'base', 'file_tree'),
  780. 'follow_dir_links': False,
  781. 'keep_newline': True
  782. }
  783. }
  784. )
  785. # We need to copy the extension modules into the new master root_dir or
  786. # it will be prefixed by it
  787. extension_modules_path = session_root_dir.join('extension_modules').strpath
  788. if not os.path.exists(extension_modules_path):
  789. shutil.copytree(
  790. os.path.join(
  791. RUNTIME_VARS.FILES, 'extension_modules'
  792. ),
  793. extension_modules_path
  794. )
  795. # Copy the autosign_file to the new master root_dir
  796. autosign_file_path = session_root_dir.join('autosign_file').strpath
  797. shutil.copyfile(
  798. os.path.join(RUNTIME_VARS.FILES, 'autosign_file'),
  799. autosign_file_path
  800. )
  801. # all read, only owner write
  802. autosign_file_permissions = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | stat.S_IWUSR
  803. os.chmod(autosign_file_path, autosign_file_permissions)
  804. pytest_stop_sending_events_file = session_root_dir.join('pytest_stop_sending_events_file').strpath
  805. with salt.utils.files.fopen(pytest_stop_sending_events_file, 'w') as wfh:
  806. wfh.write('')
  807. return {
  808. 'pillar_opts': True,
  809. 'ext_pillar': ext_pillar,
  810. 'extension_modules': extension_modules_path,
  811. 'file_roots': {
  812. 'base': [
  813. os.path.join(RUNTIME_VARS.FILES, 'file', 'base'),
  814. ],
  815. # Alternate root to test __env__ choices
  816. 'prod': [
  817. os.path.join(RUNTIME_VARS.FILES, 'file', 'prod'),
  818. ]
  819. },
  820. 'pillar_roots': {
  821. 'base': [
  822. os.path.join(RUNTIME_VARS.FILES, 'pillar', 'base'),
  823. ]
  824. },
  825. 'reactor': [
  826. {
  827. 'salt/minion/*/start': [
  828. os.path.join(RUNTIME_VARS.FILES, 'reactor-sync-minion.sls')
  829. ],
  830. },
  831. {
  832. 'salt/test/reactor': [
  833. os.path.join(RUNTIME_VARS.FILES, 'reactor-test.sls')
  834. ],
  835. }
  836. ],
  837. 'pytest_stop_sending_events_file': pytest_stop_sending_events_file
  838. }
  839. @pytest.fixture(scope='session')
  840. def session_minion_default_options(request, tempdir):
  841. with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, 'minion')) as rfh:
  842. opts = yaml.deserialize(rfh.read())
  843. opts['hosts.file'] = tempdir.join('hosts').strpath
  844. opts['aliases.file'] = tempdir.join('aliases').strpath
  845. opts['transport'] = request.config.getoption('--transport')
  846. return opts
  847. def _get_virtualenv_binary_path():
  848. try:
  849. return _get_virtualenv_binary_path.__virtualenv_binary__
  850. except AttributeError:
  851. # Under windows we can't seem to properly create a virtualenv off of another
  852. # virtualenv, we can on linux but we will still point to the virtualenv binary
  853. # outside the virtualenv running the test suite, if that's the case.
  854. try:
  855. real_prefix = sys.real_prefix
  856. # The above attribute exists, this is a virtualenv
  857. if salt.utils.platform.is_windows():
  858. virtualenv_binary = os.path.join(real_prefix, 'Scripts', 'virtualenv.exe')
  859. else:
  860. # We need to remove the virtualenv from PATH or we'll get the virtualenv binary
  861. # from within the virtualenv, we don't want that
  862. path = os.environ.get('PATH')
  863. if path is not None:
  864. path_items = path.split(os.pathsep)
  865. for item in path_items[:]:
  866. if item.startswith(sys.base_prefix):
  867. path_items.remove(item)
  868. os.environ['PATH'] = os.pathsep.join(path_items)
  869. virtualenv_binary = salt.utils.which('virtualenv')
  870. if path is not None:
  871. # Restore previous environ PATH
  872. os.environ['PATH'] = path
  873. if not virtualenv_binary.startswith(real_prefix):
  874. virtualenv_binary = None
  875. if virtualenv_binary and not os.path.exists(virtualenv_binary):
  876. # It doesn't exist?!
  877. virtualenv_binary = None
  878. except AttributeError:
  879. # We're not running inside a virtualenv
  880. virtualenv_binary = None
  881. _get_virtualenv_binary_path.__virtualenv_binary__ = virtualenv_binary
  882. return virtualenv_binary
  883. @pytest.fixture(scope='session')
  884. def session_minion_config_overrides():
  885. opts = {
  886. 'file_roots': {
  887. 'base': [
  888. os.path.join(RUNTIME_VARS.FILES, 'file', 'base'),
  889. ],
  890. # Alternate root to test __env__ choices
  891. 'prod': [
  892. os.path.join(RUNTIME_VARS.FILES, 'file', 'prod'),
  893. ]
  894. },
  895. 'pillar_roots': {
  896. 'base': [
  897. os.path.join(RUNTIME_VARS.FILES, 'pillar', 'base'),
  898. ]
  899. },
  900. }
  901. virtualenv_binary = _get_virtualenv_binary_path()
  902. if virtualenv_binary:
  903. opts['venv_bin'] = virtualenv_binary
  904. return opts
  905. @pytest.fixture(scope='session')
  906. def session_secondary_minion_default_options(request, tempdir):
  907. with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, 'sub_minion')) as rfh:
  908. opts = yaml.deserialize(rfh.read())
  909. opts['hosts.file'] = tempdir.join('hosts').strpath
  910. opts['aliases.file'] = tempdir.join('aliases').strpath
  911. opts['transport'] = request.config.getoption('--transport')
  912. return opts
  913. @pytest.fixture(scope='session')
  914. def session_seconary_minion_config_overrides():
  915. opts = {}
  916. virtualenv_binary = _get_virtualenv_binary_path()
  917. if virtualenv_binary:
  918. opts['venv_bin'] = virtualenv_binary
  919. return opts
  920. @pytest.fixture(scope='session')
  921. def session_master_of_masters_default_options(request, tempdir):
  922. with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, 'syndic_master')) as rfh:
  923. opts = yaml.deserialize(rfh.read())
  924. opts['hosts.file'] = tempdir.join('hosts').strpath
  925. opts['aliases.file'] = tempdir.join('aliases').strpath
  926. opts['transport'] = request.config.getoption('--transport')
  927. return opts
  928. @pytest.fixture(scope='session')
  929. def session_master_of_masters_config_overrides(session_master_of_masters_root_dir):
  930. if salt.utils.platform.is_windows():
  931. ext_pillar = {'cmd_yaml': 'type {0}'.format(os.path.join(RUNTIME_VARS.FILES, 'ext.yaml'))}
  932. else:
  933. ext_pillar = {'cmd_yaml': 'cat {0}'.format(os.path.join(RUNTIME_VARS.FILES, 'ext.yaml'))}
  934. # We need to copy the extension modules into the new master root_dir or
  935. # it will be prefixed by it
  936. extension_modules_path = session_master_of_masters_root_dir.join('extension_modules').strpath
  937. if not os.path.exists(extension_modules_path):
  938. shutil.copytree(
  939. os.path.join(
  940. RUNTIME_VARS.FILES, 'extension_modules'
  941. ),
  942. extension_modules_path
  943. )
  944. # Copy the autosign_file to the new master root_dir
  945. autosign_file_path = session_master_of_masters_root_dir.join('autosign_file').strpath
  946. shutil.copyfile(
  947. os.path.join(RUNTIME_VARS.FILES, 'autosign_file'),
  948. autosign_file_path
  949. )
  950. # all read, only owner write
  951. autosign_file_permissions = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | stat.S_IWUSR
  952. os.chmod(autosign_file_path, autosign_file_permissions)
  953. pytest_stop_sending_events_file = session_master_of_masters_root_dir.join('pytest_stop_sending_events_file').strpath
  954. with salt.utils.files.fopen(pytest_stop_sending_events_file, 'w') as wfh:
  955. wfh.write('')
  956. return {
  957. 'ext_pillar': [ext_pillar],
  958. 'extension_modules': extension_modules_path,
  959. 'file_roots': {
  960. 'base': [
  961. os.path.join(RUNTIME_VARS.FILES, 'file', 'base'),
  962. ],
  963. # Alternate root to test __env__ choices
  964. 'prod': [
  965. os.path.join(RUNTIME_VARS.FILES, 'file', 'prod'),
  966. ]
  967. },
  968. 'pillar_roots': {
  969. 'base': [
  970. os.path.join(RUNTIME_VARS.FILES, 'pillar', 'base'),
  971. ]
  972. },
  973. 'pytest_stop_sending_events_file': pytest_stop_sending_events_file
  974. }
  975. @pytest.fixture(scope='session')
  976. def session_syndic_master_default_options(request, tempdir):
  977. with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, 'syndic_master')) as rfh:
  978. opts = yaml.deserialize(rfh.read())
  979. opts['hosts.file'] = tempdir.join('hosts').strpath
  980. opts['aliases.file'] = tempdir.join('aliases').strpath
  981. opts['transport'] = request.config.getoption('--transport')
  982. return opts
  983. @pytest.fixture(scope='session')
  984. def session_syndic_default_options(request, tempdir):
  985. with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, 'syndic')) as rfh:
  986. opts = yaml.deserialize(rfh.read())
  987. opts['hosts.file'] = tempdir.join('hosts').strpath
  988. opts['aliases.file'] = tempdir.join('aliases').strpath
  989. opts['transport'] = request.config.getoption('--transport')
  990. return opts
  991. @pytest.fixture(scope='session')
  992. def session_proxy_default_options(request, tempdir):
  993. with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, 'proxy')) as rfh:
  994. opts = yaml.deserialize(rfh.read())
  995. opts['hosts.file'] = tempdir.join('hosts').strpath
  996. opts['aliases.file'] = tempdir.join('aliases').strpath
  997. opts['transport'] = request.config.getoption('--transport')
  998. return opts
  999. @pytest.fixture(scope='session', autouse=True)
  1000. def bridge_pytest_and_runtests(reap_stray_processes,
  1001. session_root_dir,
  1002. session_conf_dir,
  1003. session_secondary_conf_dir,
  1004. session_syndic_conf_dir,
  1005. session_master_of_masters_conf_dir,
  1006. session_base_env_pillar_tree_root_dir,
  1007. session_base_env_state_tree_root_dir,
  1008. session_prod_env_state_tree_root_dir,
  1009. session_master_config,
  1010. session_minion_config,
  1011. session_secondary_minion_config,
  1012. session_master_of_masters_config,
  1013. session_syndic_config):
  1014. # Make sure unittest2 classes know their paths
  1015. RUNTIME_VARS.TMP_ROOT_DIR = session_root_dir.realpath().strpath
  1016. RUNTIME_VARS.TMP_CONF_DIR = session_conf_dir.realpath().strpath
  1017. RUNTIME_VARS.TMP_SUB_MINION_CONF_DIR = session_secondary_conf_dir.realpath().strpath
  1018. RUNTIME_VARS.TMP_SYNDIC_MASTER_CONF_DIR = session_master_of_masters_conf_dir.realpath().strpath
  1019. RUNTIME_VARS.TMP_SYNDIC_MINION_CONF_DIR = session_syndic_conf_dir.realpath().strpath
  1020. RUNTIME_VARS.TMP_PILLAR_TREE = session_base_env_pillar_tree_root_dir.realpath().strpath
  1021. RUNTIME_VARS.TMP_STATE_TREE = session_base_env_state_tree_root_dir.realpath().strpath
  1022. RUNTIME_VARS.TMP_PRODENV_STATE_TREE = session_prod_env_state_tree_root_dir.realpath().strpath
  1023. # Make sure unittest2 uses the pytest generated configuration
  1024. RUNTIME_VARS.RUNTIME_CONFIGS['master'] = freeze(session_master_config)
  1025. RUNTIME_VARS.RUNTIME_CONFIGS['minion'] = freeze(session_minion_config)
  1026. RUNTIME_VARS.RUNTIME_CONFIGS['sub_minion'] = freeze(session_secondary_minion_config)
  1027. RUNTIME_VARS.RUNTIME_CONFIGS['syndic_master'] = freeze(session_master_of_masters_config)
  1028. RUNTIME_VARS.RUNTIME_CONFIGS['syndic'] = freeze(session_syndic_config)
  1029. RUNTIME_VARS.RUNTIME_CONFIGS['client_config'] = freeze(
  1030. salt.config.client_config(session_conf_dir.join('master').strpath)
  1031. )
  1032. # Copy configuration files and directories which are not automatically generated
  1033. for entry in os.listdir(RUNTIME_VARS.CONF_DIR):
  1034. if entry in ('master', 'minion', 'sub_minion', 'syndic', 'syndic_master', 'proxy'):
  1035. # These have runtime computed values and are handled by pytest-salt fixtures
  1036. continue
  1037. entry_path = os.path.join(RUNTIME_VARS.CONF_DIR, entry)
  1038. if os.path.isfile(entry_path):
  1039. shutil.copy(
  1040. entry_path,
  1041. os.path.join(RUNTIME_VARS.TMP_CONF_DIR, entry)
  1042. )
  1043. elif os.path.isdir(entry_path):
  1044. shutil.copytree(
  1045. entry_path,
  1046. os.path.join(RUNTIME_VARS.TMP_CONF_DIR, entry)
  1047. )
  1048. # <---- Salt Configuration -------------------------------------------------------------------------------------------
  1049. # <---- Fixtures Overrides -------------------------------------------------------------------------------------------
  1050. # ----- Custom Grains Mark Evaluator -------------------------------------------------------------------------------->
  1051. class GrainsMarkEvaluator(MarkEvaluator):
  1052. _cached_grains = None
  1053. def _getglobals(self):
  1054. item_globals = super(GrainsMarkEvaluator, self)._getglobals()
  1055. if GrainsMarkEvaluator._cached_grains is None:
  1056. sminion = create_sminion()
  1057. GrainsMarkEvaluator._cached_grains = sminion.opts['grains'].copy()
  1058. item_globals['grains'] = GrainsMarkEvaluator._cached_grains.copy()
  1059. return item_globals
  1060. # Patch PyTest's skipping MarkEvaluator to use our GrainsMarkEvaluator
  1061. _pytest.skipping.MarkEvaluator = GrainsMarkEvaluator
  1062. # <---- Custom Grains Mark Evaluator ---------------------------------------------------------------------------------
  1063. # ----- Custom Fixtures --------------------------------------------------------------------------------------------->
  1064. @pytest.fixture(scope='session')
  1065. def reap_stray_processes():
  1066. # Run tests
  1067. yield
  1068. children = psutil.Process(os.getpid()).children(recursive=True)
  1069. if not children:
  1070. log.info('No astray processes found')
  1071. return
  1072. def on_terminate(proc):
  1073. log.debug('Process %s terminated with exit code %s', proc, proc.returncode)
  1074. if children:
  1075. # Reverse the order, sublings first, parents after
  1076. children.reverse()
  1077. log.warning(
  1078. 'Test suite left %d astray processes running. Killing those processes:\n%s',
  1079. len(children),
  1080. pprint.pformat(children)
  1081. )
  1082. _, alive = psutil.wait_procs(children, timeout=3, callback=on_terminate)
  1083. for child in alive:
  1084. child.kill()
  1085. _, alive = psutil.wait_procs(alive, timeout=3, callback=on_terminate)
  1086. if alive:
  1087. # Give up
  1088. for child in alive:
  1089. log.warning('Process %s survived SIGKILL, giving up:\n%s', child, pprint.pformat(child.as_dict()))
  1090. @pytest.fixture(scope='session')
  1091. def grains(request):
  1092. sminion = create_sminion()
  1093. return sminion.opts['grains'].copy()
  1094. # <---- Custom Fixtures ----------------------------------------------------------------------------------------------