conftest.py 47 KB

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