1
0

conftest.py 50 KB

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