conftest.py 39 KB

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