12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427 |
- # -*- coding: utf-8 -*-
- '''
- :codeauthor: Pedro Algarvio (pedro@algarvio.me)
- tests.conftest
- ~~~~~~~~~~~~~~
- Prepare py.test for our test suite
- '''
- # pylint: disable=wrong-import-order,wrong-import-position,3rd-party-local-module-not-gated
- # pylint: disable=redefined-outer-name,invalid-name
- # Import python libs
- from __future__ import absolute_import, print_function, unicode_literals
- import os
- import sys
- import stat
- import pprint
- import shutil
- import socket
- import logging
- import tempfile
- import textwrap
- from functools import partial, wraps
- from contextlib import contextmanager
- TESTS_DIR = os.path.dirname(os.path.normpath(os.path.abspath(__file__)))
- CODE_DIR = os.path.dirname(TESTS_DIR)
- # Change to code checkout directory
- os.chdir(CODE_DIR)
- # Make sure the current directory is the first item in sys.path
- if CODE_DIR in sys.path:
- sys.path.remove(CODE_DIR)
- sys.path.insert(0, CODE_DIR)
- # Import test libs
- from tests.support.runtests import RUNTIME_VARS
- from tests.support.sminion import create_sminion, check_required_sminion_attributes
- from tests.support.helpers import PRE_PYTEST_SKIP_REASON, PRE_PYTEST_SKIP_OR_NOT
- # Import pytest libs
- import pytest
- import _pytest.logging
- import _pytest.skipping
- from _pytest.mark.evaluate import MarkEvaluator
- # Import 3rd-party libs
- import psutil
- from salt.ext import six
- # Import salt libs
- import salt.loader
- import salt.config
- import salt.utils.files
- import salt.utils.path
- import salt.log.setup
- import salt.log.mixins
- import salt.utils.platform
- import salt.utils.win_functions
- from salt.serializers import yaml
- from salt.utils.immutabletypes import freeze
- # Import Pytest Salt libs
- from pytestsalt.utils import cli_scripts
- # Coverage
- if 'COVERAGE_PROCESS_START' in os.environ:
- MAYBE_RUN_COVERAGE = True
- COVERAGERC_FILE = os.environ['COVERAGE_PROCESS_START']
- else:
- COVERAGERC_FILE = os.path.join(CODE_DIR, '.coveragerc')
- MAYBE_RUN_COVERAGE = sys.argv[0].endswith('pytest.py') or '_COVERAGE_RCFILE' in os.environ
- if MAYBE_RUN_COVERAGE:
- # Flag coverage to track suprocesses by pointing it to the right .coveragerc file
- os.environ[str('COVERAGE_PROCESS_START')] = str(COVERAGERC_FILE)
- # Define the pytest plugins we rely on
- pytest_plugins = ['tempdir', 'helpers_namespace', 'salt-runtests-bridge']
- # Define where not to collect tests from
- collect_ignore = ['setup.py']
- # Patch PyTest logging handlers
- class LogCaptureHandler(salt.log.mixins.ExcInfoOnLogLevelFormatMixIn,
- logging.NullHandler):
- '''
- Subclassing PyTest's LogCaptureHandler in order to add the
- exc_info_on_loglevel functionality and actually make it a NullHandler,
- it's only used to print log messages emmited during tests, which we
- have explicitly disabled in pytest.ini
- '''
- _pytest.logging.LogCaptureHandler = LogCaptureHandler
- class LiveLoggingStreamHandler(salt.log.mixins.ExcInfoOnLogLevelFormatMixIn,
- _pytest.logging._LiveLoggingStreamHandler):
- '''
- Subclassing PyTest's LiveLoggingStreamHandler in order to add the
- exc_info_on_loglevel functionality.
- '''
- _pytest.logging._LiveLoggingStreamHandler = LiveLoggingStreamHandler
- # Reset logging root handlers
- for handler in logging.root.handlers[:]:
- logging.root.removeHandler(handler)
- # Reset the root logger to it's default level(because salt changed it)
- logging.root.setLevel(logging.WARNING)
- log = logging.getLogger('salt.testsuite')
- # ----- PyTest Tempdir Plugin Hooks --------------------------------------------------------------------------------->
- def pytest_tempdir_temproot():
- # Taken from https://github.com/saltstack/salt/blob/v2019.2.0/tests/support/paths.py
- # Avoid ${TMPDIR} and gettempdir() on MacOS as they yield a base path too long
- # for unix sockets: ``error: AF_UNIX path too long``
- # Gentoo Portage prefers ebuild tests are rooted in ${TMPDIR}
- if not sys.platform.startswith('darwin'):
- tempdir = os.environ.get('TMPDIR') or tempfile.gettempdir()
- else:
- tempdir = '/tmp'
- return os.path.abspath(os.path.realpath(tempdir))
- def pytest_tempdir_basename():
- '''
- Return the temporary directory basename for the salt test suite.
- '''
- return 'salt-tests-tmpdir'
- # <---- PyTest Tempdir Plugin Hooks ----------------------------------------------------------------------------------
- # ----- CLI Options Setup ------------------------------------------------------------------------------------------->
- def pytest_addoption(parser):
- '''
- register argparse-style options and ini-style config values.
- '''
- parser.addoption(
- '--sysinfo',
- default=False,
- action='store_true',
- help='Print some system information.'
- )
- parser.addoption(
- '--transport',
- default='zeromq',
- choices=('zeromq', 'tcp'),
- help=('Select which transport to run the integration tests with, '
- 'zeromq or tcp. Default: %default')
- )
- test_selection_group = parser.getgroup('Tests Selection')
- test_selection_group.addoption(
- '--ssh',
- '--ssh-tests',
- dest='ssh',
- action='store_true',
- default=False,
- help='Run salt-ssh tests. These tests will spin up a temporary '
- 'SSH server on your machine. In certain environments, this '
- 'may be insecure! Default: False'
- )
- test_selection_group.addoption(
- '--proxy',
- '--proxy-tests',
- dest='proxy',
- action='store_true',
- default=False,
- help='Run proxy tests'
- )
- test_selection_group.addoption(
- '--run-destructive',
- action='store_true',
- default=False,
- help='Run destructive tests. These tests can include adding '
- 'or removing users from your system for example. '
- 'Default: False'
- )
- test_selection_group.addoption(
- '--run-expensive',
- action='store_true',
- default=False,
- help='Run expensive tests. These tests usually involve costs '
- 'like for example bootstrapping a cloud VM. '
- 'Default: False'
- )
- output_options_group = parser.getgroup('Output Options')
- output_options_group.addoption(
- '--output-columns',
- default=80,
- type=int,
- help='Number of maximum columns to use on the output'
- )
- output_options_group.addoption(
- '--no-colors',
- '--no-colours',
- default=False,
- action='store_true',
- help='Disable colour printing.'
- )
- # ----- Test Groups --------------------------------------------------------------------------------------------->
- # This will allow running the tests in chunks
- test_selection_group.addoption(
- '--test-group-count', dest='test-group-count', type=int,
- help='The number of groups to split the tests into'
- )
- test_selection_group.addoption(
- '--test-group', dest='test-group', type=int,
- help='The group of tests that should be executed'
- )
- # <---- Test Groups ----------------------------------------------------------------------------------------------
- # <---- CLI Options Setup --------------------------------------------------------------------------------------------
- # ----- Register Markers -------------------------------------------------------------------------------------------->
- @pytest.mark.trylast
- def pytest_configure(config):
- '''
- called after command line options have been parsed
- and all plugins and initial conftest files been loaded.
- '''
- for dirname in os.listdir(CODE_DIR):
- if not os.path.isdir(dirname):
- continue
- if dirname != 'tests':
- config.addinivalue_line('norecursedirs', os.path.join(CODE_DIR, dirname))
- # Expose the markers we use to pytest CLI
- config.addinivalue_line(
- 'markers',
- 'destructive_test: Run destructive tests. These tests can include adding '
- 'or removing users from your system for example.'
- )
- config.addinivalue_line(
- 'markers',
- 'skip_if_not_root: Skip if the current user is not `root`.'
- )
- config.addinivalue_line(
- 'markers',
- 'skip_if_binaries_missing(*binaries, check_all=False, message=None): Skip if '
- 'any of the passed binaries are not found in path. If \'check_all\' is '
- '\'True\', then all binaries must be found.'
- )
- config.addinivalue_line(
- 'markers',
- 'requires_network(only_local_network=False): Skip if no networking is set up. '
- 'If \'only_local_network\' is \'True\', only the local network is checked.'
- )
- config.addinivalue_line(
- 'markers',
- 'requires_salt_modules(*required_module_names): Skip if at least one module is not available.'
- )
- config.addinivalue_line(
- 'markers',
- 'requires_salt_states(*required_state_names): Skip if at least one state module is not available.'
- )
- config.addinivalue_line(
- 'markers',
- 'windows_whitelisted: Mark test as whitelisted to run under Windows'
- )
- # Make sure the test suite "knows" this is a pytest test run
- RUNTIME_VARS.PYTEST_SESSION = True
- # <---- Register Markers ---------------------------------------------------------------------------------------------
- # ----- PyTest Tweaks ----------------------------------------------------------------------------------------------->
- def set_max_open_files_limits(min_soft=3072, min_hard=4096):
- # Get current limits
- if salt.utils.platform.is_windows():
- import win32file
- prev_hard = win32file._getmaxstdio()
- prev_soft = 512
- else:
- import resource
- prev_soft, prev_hard = resource.getrlimit(resource.RLIMIT_NOFILE)
- # Check minimum required limits
- set_limits = False
- if prev_soft < min_soft:
- soft = min_soft
- set_limits = True
- else:
- soft = prev_soft
- if prev_hard < min_hard:
- hard = min_hard
- set_limits = True
- else:
- hard = prev_hard
- # Increase limits
- if set_limits:
- log.debug(
- ' * Max open files settings is too low (soft: %s, hard: %s) for running the tests. '
- 'Trying to raise the limits to soft: %s, hard: %s',
- prev_soft,
- prev_hard,
- soft,
- hard
- )
- try:
- if salt.utils.platform.is_windows():
- hard = 2048 if hard > 2048 else hard
- win32file._setmaxstdio(hard)
- else:
- resource.setrlimit(resource.RLIMIT_NOFILE, (soft, hard))
- except Exception as err: # pylint: disable=broad-except
- log.error(
- 'Failed to raise the max open files settings -> %s. Please issue the following command '
- 'on your console: \'ulimit -u %s\'',
- err,
- soft,
- )
- exit(1)
- return soft, hard
- def pytest_report_header():
- soft, hard = set_max_open_files_limits()
- return 'max open files; soft: {}; hard: {}'.format(soft, hard)
- def pytest_runtest_logstart(nodeid):
- '''
- signal the start of running a single test item.
- This hook will be called **before** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and
- :func:`pytest_runtest_teardown` hooks.
- :param str nodeid: full id of the item
- :param location: a triple of ``(filename, linenum, testname)``
- '''
- log.debug('>>>>> START >>>>> %s', nodeid)
- def pytest_runtest_logfinish(nodeid):
- '''
- signal the complete finish of running a single test item.
- This hook will be called **after** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and
- :func:`pytest_runtest_teardown` hooks.
- :param str nodeid: full id of the item
- :param location: a triple of ``(filename, linenum, testname)``
- '''
- log.debug('<<<<< END <<<<<<< %s', nodeid)
- @pytest.hookimpl(hookwrapper=True, trylast=True)
- def pytest_collection_modifyitems(config, items):
- '''
- called after collection has been performed, may filter or re-order
- the items in-place.
- :param _pytest.main.Session session: the pytest session object
- :param _pytest.config.Config config: pytest config object
- :param List[_pytest.nodes.Item] items: list of item objects
- '''
- # Let PyTest or other plugins handle the initial collection
- yield
- groups_collection_modifyitems(config, items)
- log.warning('Mofifying collected tests to keep track of fixture usage')
- for item in items:
- for fixture in item.fixturenames:
- if fixture not in item._fixtureinfo.name2fixturedefs:
- continue
- for fixturedef in item._fixtureinfo.name2fixturedefs[fixture]:
- if fixturedef.scope == 'function':
- continue
- try:
- node_ids = fixturedef.node_ids
- except AttributeError:
- node_ids = fixturedef.node_ids = set()
- node_ids.add(item.nodeid)
- try:
- fixturedef.finish.__wrapped__
- except AttributeError:
- original_func = fixturedef.finish
- def wrapper(func, fixturedef):
- @wraps(func)
- def wrapped(self, request):
- try:
- return self._finished
- except AttributeError:
- if self.node_ids:
- log.debug(
- '%s is still going to be used, not terminating it. '
- 'Still in use on:\n%s',
- self,
- pprint.pformat(list(self.node_ids))
- )
- return
- log.debug('Finish called on %s', self)
- try:
- return func(request)
- finally:
- self._finished = True
- return partial(wrapped, fixturedef)
- fixturedef.finish = wrapper(fixturedef.finish, fixturedef)
- try:
- fixturedef.finish.__wrapped__
- except AttributeError:
- fixturedef.finish.__wrapped__ = original_func
- @pytest.hookimpl(trylast=True, hookwrapper=True)
- def pytest_runtest_protocol(item, nextitem):
- '''
- implements the runtest_setup/call/teardown protocol for
- the given test item, including capturing exceptions and calling
- reporting hooks.
- :arg item: test item for which the runtest protocol is performed.
- :arg nextitem: the scheduled-to-be-next test item (or None if this
- is the end my friend). This argument is passed on to
- :py:func:`pytest_runtest_teardown`.
- :return boolean: True if no further hook implementations should be invoked.
- Stops at first non-None result, see :ref:`firstresult`
- '''
- request = item._request
- used_fixture_defs = []
- for fixture in item.fixturenames:
- if fixture not in item._fixtureinfo.name2fixturedefs:
- continue
- for fixturedef in reversed(item._fixtureinfo.name2fixturedefs[fixture]):
- if fixturedef.scope == 'function':
- continue
- used_fixture_defs.append(fixturedef)
- try:
- # Run the test
- yield
- finally:
- for fixturedef in used_fixture_defs:
- fixturedef.node_ids.remove(item.nodeid)
- if not fixturedef.node_ids:
- # This fixture is not used in any more test functions
- fixturedef.finish(request)
- del request
- del used_fixture_defs
- # <---- PyTest Tweaks ------------------------------------------------------------------------------------------------
- # ----- Test Setup -------------------------------------------------------------------------------------------------->
- def _has_unittest_attr(item, attr):
- # XXX: This is a hack while we support both runtests.py and PyTest
- if hasattr(item.obj, attr):
- return True
- if item.cls and hasattr(item.cls, attr):
- return True
- if item.parent and hasattr(item.parent.obj, attr):
- return True
- return False
- @pytest.hookimpl(tryfirst=True)
- def pytest_runtest_setup(item):
- '''
- Fixtures injection based on markers or test skips based on CLI arguments
- '''
- integration_utils_tests_path = os.path.join(CODE_DIR, 'tests', 'integration', 'utils')
- if str(item.fspath).startswith(integration_utils_tests_path) and PRE_PYTEST_SKIP_OR_NOT is True:
- item._skipped_by_mark = True
- pytest.skip(PRE_PYTEST_SKIP_REASON)
- destructive_tests_marker = item.get_closest_marker('destructive_test')
- if destructive_tests_marker is not None or _has_unittest_attr(item, '__destructive_test__'):
- if item.config.getoption('--run-destructive') is False:
- item._skipped_by_mark = True
- pytest.skip('Destructive tests are disabled')
- expensive_tests_marker = item.get_closest_marker('expensive_test')
- if expensive_tests_marker is not None or _has_unittest_attr(item, '__expensive_test__'):
- if item.config.getoption('--run-expensive') is False:
- item._skipped_by_mark = True
- pytest.skip('Expensive tests are disabled')
- skip_if_not_root_marker = item.get_closest_marker('skip_if_not_root')
- if skip_if_not_root_marker is not None or _has_unittest_attr(item, '__skip_if_not_root__'):
- if not sys.platform.startswith('win'):
- if os.getuid() != 0:
- item._skipped_by_mark = True
- pytest.skip('You must be logged in as root to run this test')
- else:
- current_user = salt.utils.win_functions.get_current_user()
- if current_user != 'SYSTEM':
- if not salt.utils.win_functions.is_admin(current_user):
- item._skipped_by_mark = True
- pytest.skip('You must be logged in as an Administrator to run this test')
- skip_if_binaries_missing_marker = item.get_closest_marker('skip_if_binaries_missing')
- if skip_if_binaries_missing_marker is not None:
- binaries = skip_if_binaries_missing_marker.args
- if len(binaries) == 1:
- if isinstance(binaries[0], (list, tuple, set, frozenset)):
- binaries = binaries[0]
- check_all = skip_if_binaries_missing_marker.kwargs.get('check_all', False)
- message = skip_if_binaries_missing_marker.kwargs.get('message', None)
- if check_all:
- for binary in binaries:
- if salt.utils.path.which(binary) is None:
- item._skipped_by_mark = True
- pytest.skip(
- '{0}The "{1}" binary was not found'.format(
- message and '{0}. '.format(message) or '',
- binary
- )
- )
- elif salt.utils.path.which_bin(binaries) is None:
- item._skipped_by_mark = True
- pytest.skip(
- '{0}None of the following binaries was found: {1}'.format(
- message and '{0}. '.format(message) or '',
- ', '.join(binaries)
- )
- )
- requires_network_marker = item.get_closest_marker('requires_network')
- if requires_network_marker is not None:
- only_local_network = requires_network_marker.kwargs.get('only_local_network', False)
- has_local_network = False
- # First lets try if we have a local network. Inspired in verify_socket
- try:
- pubsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- retsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- pubsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- pubsock.bind(('', 18000))
- pubsock.close()
- retsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- retsock.bind(('', 18001))
- retsock.close()
- has_local_network = True
- except socket.error:
- # I wonder if we just have IPV6 support?
- try:
- pubsock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
- retsock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
- pubsock.setsockopt(
- socket.SOL_SOCKET, socket.SO_REUSEADDR, 1
- )
- pubsock.bind(('', 18000))
- pubsock.close()
- retsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
- retsock.bind(('', 18001))
- retsock.close()
- has_local_network = True
- except socket.error:
- # Let's continue
- pass
- if only_local_network is True:
- if has_local_network is False:
- # Since we're only supposed to check local network, and no
- # local network was detected, skip the test
- item._skipped_by_mark = True
- pytest.skip('No local network was detected')
- # We are using the google.com DNS records as numerical IPs to avoid
- # DNS lookups which could greatly slow down this check
- for addr in ('173.194.41.198', '173.194.41.199', '173.194.41.200',
- '173.194.41.201', '173.194.41.206', '173.194.41.192',
- '173.194.41.193', '173.194.41.194', '173.194.41.195',
- '173.194.41.196', '173.194.41.197'):
- try:
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- sock.settimeout(0.25)
- sock.connect((addr, 80))
- sock.close()
- # We connected? Stop the loop
- break
- except socket.error:
- # Let's check the next IP
- continue
- else:
- item._skipped_by_mark = True
- pytest.skip('No internet network connection was detected')
- requires_salt_modules_marker = item.get_closest_marker('requires_salt_modules')
- if requires_salt_modules_marker is not None:
- required_salt_modules = requires_salt_modules_marker.args
- if len(required_salt_modules) == 1 and isinstance(required_salt_modules[0], (list, tuple, set)):
- required_salt_modules = required_salt_modules[0]
- required_salt_modules = set(required_salt_modules)
- not_available_modules = check_required_sminion_attributes('functions', required_salt_modules)
- if not_available_modules:
- item._skipped_by_mark = True
- if len(not_available_modules) == 1:
- pytest.skip('Salt module \'{}\' is not available'.format(*not_available_modules))
- pytest.skip('Salt modules not available: {}'.format(', '.join(not_available_modules)))
- requires_salt_states_marker = item.get_closest_marker('requires_salt_states')
- if requires_salt_states_marker is not None:
- required_salt_states = requires_salt_states_marker.args
- if len(required_salt_states) == 1 and isinstance(required_salt_states[0], (list, tuple, set)):
- required_salt_states = required_salt_states[0]
- required_salt_states = set(required_salt_states)
- not_available_states = check_required_sminion_attributes('states', required_salt_states)
- if not_available_states:
- item._skipped_by_mark = True
- if len(not_available_states) == 1:
- pytest.skip('Salt state module \'{}\' is not available'.format(*not_available_states))
- pytest.skip('Salt state modules not available: {}'.format(', '.join(not_available_states)))
- if salt.utils.platform.is_windows():
- if not item.fspath.fnmatch(os.path.join(CODE_DIR, 'tests', 'unit', '*')):
- # Unit tests are whitelisted on windows by default, so, we're only
- # after all other tests
- windows_whitelisted_marker = item.get_closest_marker('windows_whitelisted')
- if windows_whitelisted_marker is None:
- item._skipped_by_mark = True
- pytest.skip('Test is not whitelisted for Windows')
- # <---- Test Setup ---------------------------------------------------------------------------------------------------
- # ----- Test Groups Selection --------------------------------------------------------------------------------------->
- def get_group_size(total_items, total_groups):
- '''
- Return the group size.
- '''
- return int(total_items / total_groups)
- def get_group(items, group_count, group_size, group_id):
- '''
- Get the items from the passed in group based on group size.
- '''
- start = group_size * (group_id - 1)
- end = start + group_size
- total_items = len(items)
- if start >= total_items:
- pytest.fail("Invalid test-group argument. start({})>=total_items({})".format(start, total_items))
- elif start < 0:
- pytest.fail("Invalid test-group argument. Start({})<0".format(start))
- if group_count == group_id and end < total_items:
- # If this is the last group and there are still items to test
- # which don't fit in this group based on the group items count
- # add them anyway
- end = total_items
- return items[start:end]
- def groups_collection_modifyitems(config, items):
- group_count = config.getoption('test-group-count')
- group_id = config.getoption('test-group')
- if not group_count or not group_id:
- # We're not selection tests using groups, don't do any filtering
- return
- total_items = len(items)
- group_size = get_group_size(total_items, group_count)
- tests_in_group = get_group(items, group_count, group_size, group_id)
- # Replace all items in the list
- items[:] = tests_in_group
- terminal_reporter = config.pluginmanager.get_plugin('terminalreporter')
- terminal_reporter.write(
- 'Running test group #{0} ({1} tests)\n'.format(
- group_id,
- len(items)
- ),
- yellow=True
- )
- # <---- Test Groups Selection ----------------------------------------------------------------------------------------
- # ----- Pytest Helpers ---------------------------------------------------------------------------------------------->
- if six.PY2:
- # backport mock_open from the python 3 unittest.mock library so that we can
- # mock read, readline, readlines, and file iteration properly
- file_spec = None
- def _iterate_read_data(read_data):
- # Helper for mock_open:
- # Retrieve lines from read_data via a generator so that separate calls to
- # readline, read, and readlines are properly interleaved
- data_as_list = ['{0}\n'.format(l) for l in read_data.split('\n')]
- if data_as_list[-1] == '\n':
- # If the last line ended in a newline, the list comprehension will have an
- # extra entry that's just a newline. Remove this.
- data_as_list = data_as_list[:-1]
- else:
- # If there wasn't an extra newline by itself, then the file being
- # emulated doesn't have a newline to end the last line remove the
- # newline that our naive format() added
- data_as_list[-1] = data_as_list[-1][:-1]
- for line in data_as_list:
- yield line
- @pytest.helpers.mock.register
- def mock_open(mock=None, read_data=''):
- """
- A helper function to create a mock to replace the use of `open`. It works
- for `open` called directly or used as a context manager.
- The `mock` argument is the mock object to configure. If `None` (the
- default) then a `MagicMock` will be created for you, with the API limited
- to methods or attributes available on standard file handles.
- `read_data` is a string for the `read` methoddline`, and `readlines` of the
- file handle to return. This is an empty string by default.
- """
- _mock = pytest.importorskip('mock', minversion='2.0.0')
- def _readlines_side_effect(*args, **kwargs):
- if handle.readlines.return_value is not None:
- return handle.readlines.return_value
- return list(_data)
- def _read_side_effect(*args, **kwargs):
- if handle.read.return_value is not None:
- return handle.read.return_value
- return ''.join(_data)
- def _readline_side_effect():
- if handle.readline.return_value is not None:
- while True:
- yield handle.readline.return_value
- for line in _data:
- yield line
- global file_spec
- if file_spec is None:
- file_spec = file # pylint: disable=undefined-variable
- if mock is None:
- mock = _mock.MagicMock(name='open', spec=open)
- handle = _mock.MagicMock(spec=file_spec)
- handle.__enter__.return_value = handle
- _data = _iterate_read_data(read_data)
- handle.write.return_value = None
- handle.read.return_value = None
- handle.readline.return_value = None
- handle.readlines.return_value = None
- handle.read.side_effect = _read_side_effect
- handle.readline.side_effect = _readline_side_effect()
- handle.readlines.side_effect = _readlines_side_effect
- mock.return_value = handle
- return mock
- else:
- @pytest.helpers.mock.register
- def mock_open(mock=None, read_data=''):
- _mock = pytest.importorskip('mock', minversion='2.0.0')
- return _mock.mock_open(mock=mock, read_data=read_data)
- @pytest.helpers.register
- @contextmanager
- def temp_directory(name=None):
- if name is not None:
- directory_path = os.path.join(RUNTIME_VARS.TMP, name)
- else:
- directory_path = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
- yield directory_path
- shutil.rmtree(directory_path, ignore_errors=True)
- @pytest.helpers.register
- @contextmanager
- def temp_file(name, contents=None, directory=None, strip_first_newline=True):
- if directory is None:
- directory = RUNTIME_VARS.TMP
- file_path = os.path.join(directory, name)
- file_directory = os.path.dirname(file_path)
- if contents is not None:
- if contents:
- if contents.startswith('\n') and strip_first_newline:
- contents = contents[1:]
- file_contents = textwrap.dedent(contents)
- else:
- file_contents = contents
- try:
- if not os.path.isdir(file_directory):
- os.makedirs(file_directory)
- if contents is not None:
- with salt.utils.files.fopen(file_path, 'w') as wfh:
- wfh.write(file_contents)
- yield file_path
- finally:
- try:
- os.unlink(file_path)
- except OSError:
- # Already deleted
- pass
- @pytest.helpers.register
- def temp_state_file(name, contents, saltenv='base', strip_first_newline=True):
- if saltenv == 'base':
- directory = RUNTIME_VARS.TMP_STATE_TREE
- elif saltenv == 'prod':
- directory = RUNTIME_VARS.TMP_PRODENV_STATE_TREE
- else:
- raise RuntimeError('"saltenv" can only be "base" or "prod", not "{}"'.format(saltenv))
- return temp_file(name, contents, directory=directory, strip_first_newline=strip_first_newline)
- # <---- Pytest Helpers -----------------------------------------------------------------------------------------------
- # ----- Fixtures Overrides ------------------------------------------------------------------------------------------>
- # ----- Generate CLI Scripts ---------------------------------------------------------------------------------------->
- @pytest.fixture(scope='session')
- def cli_master_script_name():
- '''
- Return the CLI script basename
- '''
- return 'cli_salt_master.py'
- @pytest.fixture(scope='session')
- def cli_minion_script_name():
- '''
- Return the CLI script basename
- '''
- return 'cli_salt_minion.py'
- @pytest.fixture(scope='session')
- def cli_salt_script_name():
- '''
- Return the CLI script basename
- '''
- return 'cli_salt.py'
- @pytest.fixture(scope='session')
- def cli_run_script_name():
- '''
- Return the CLI script basename
- '''
- return 'cli_salt_run.py'
- @pytest.fixture(scope='session')
- def cli_key_script_name():
- '''
- Return the CLI script basename
- '''
- return 'cli_salt_key.py'
- @pytest.fixture(scope='session')
- def cli_call_script_name():
- '''
- Return the CLI script basename
- '''
- return 'cli_salt_call.py'
- @pytest.fixture(scope='session')
- def cli_syndic_script_name():
- '''
- Return the CLI script basename
- '''
- return 'cli_salt_syndic.py'
- @pytest.fixture(scope='session')
- def cli_ssh_script_name():
- '''
- Return the CLI script basename
- '''
- return 'cli_salt_ssh.py'
- @pytest.fixture(scope='session')
- def cli_proxy_script_name():
- '''
- Return the CLI script basename
- '''
- return 'cli_salt_proxy.py'
- @pytest.fixture(scope='session')
- def cli_bin_dir(tempdir,
- request,
- python_executable_path,
- cli_master_script_name,
- cli_minion_script_name,
- cli_salt_script_name,
- cli_call_script_name,
- cli_key_script_name,
- cli_run_script_name,
- cli_ssh_script_name,
- cli_syndic_script_name,
- cli_proxy_script_name):
- '''
- Return the path to the CLI script directory to use
- '''
- tmp_cli_scripts_dir = tempdir.join('cli-scrips-bin')
- # Make sure we re-write the scripts every time we start the tests
- shutil.rmtree(tmp_cli_scripts_dir.strpath, ignore_errors=True)
- tmp_cli_scripts_dir.ensure(dir=True)
- cli_bin_dir_path = tmp_cli_scripts_dir.strpath
- # Now that we have the CLI directory created, lets generate the required CLI scripts to run salt's test suite
- for script_name in (cli_master_script_name,
- cli_minion_script_name,
- cli_call_script_name,
- cli_key_script_name,
- cli_run_script_name,
- cli_salt_script_name,
- cli_ssh_script_name,
- cli_syndic_script_name,
- cli_proxy_script_name):
- original_script_name = os.path.splitext(script_name)[0].split('cli_')[-1].replace('_', '-')
- cli_scripts.generate_script(
- bin_dir=cli_bin_dir_path,
- script_name=original_script_name,
- executable=sys.executable,
- code_dir=CODE_DIR,
- inject_sitecustomize=MAYBE_RUN_COVERAGE
- )
- # Return the CLI bin dir value
- return cli_bin_dir_path
- # <---- Generate CLI Scripts -----------------------------------------------------------------------------------------
- # ----- Salt Configuration ------------------------------------------------------------------------------------------>
- @pytest.fixture(scope='session')
- def session_master_of_masters_id():
- '''
- Returns the master of masters id
- '''
- return 'syndic_master'
- @pytest.fixture(scope='session')
- def session_master_id():
- '''
- Returns the session scoped master id
- '''
- return 'master'
- @pytest.fixture(scope='session')
- def session_minion_id():
- '''
- Returns the session scoped minion id
- '''
- return 'minion'
- @pytest.fixture(scope='session')
- def session_secondary_minion_id():
- '''
- Returns the session scoped secondary minion id
- '''
- return 'sub_minion'
- @pytest.fixture(scope='session')
- def session_syndic_id():
- '''
- Returns the session scoped syndic id
- '''
- return 'syndic'
- @pytest.fixture(scope='session')
- def session_proxy_id():
- '''
- Returns the session scoped proxy id
- '''
- return 'proxytest'
- @pytest.fixture(scope='session')
- def salt_fail_hard():
- '''
- Return the salt fail hard value
- '''
- return True
- @pytest.fixture(scope='session')
- def session_master_default_options(request, session_root_dir):
- with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, 'master')) as rfh:
- opts = yaml.deserialize(rfh.read())
- tests_known_hosts_file = session_root_dir.join('salt_ssh_known_hosts').strpath
- with salt.utils.files.fopen(tests_known_hosts_file, 'w') as known_hosts:
- known_hosts.write('')
- opts['known_hosts_file'] = tests_known_hosts_file
- opts['syndic_master'] = 'localhost'
- opts['transport'] = request.config.getoption('--transport')
- # Config settings to test `event_return`
- if 'returner_dirs' not in opts:
- opts['returner_dirs'] = []
- opts['returner_dirs'].append(os.path.join(RUNTIME_VARS.FILES, 'returners'))
- opts['event_return'] = 'runtests_noop'
- return opts
- @pytest.fixture(scope='session')
- def session_master_config_overrides(session_root_dir):
- ext_pillar = []
- if salt.utils.platform.is_windows():
- ext_pillar.append(
- {'cmd_yaml': 'type {0}'.format(os.path.join(RUNTIME_VARS.FILES, 'ext.yaml'))}
- )
- else:
- ext_pillar.append(
- {'cmd_yaml': 'cat {0}'.format(os.path.join(RUNTIME_VARS.FILES, 'ext.yaml'))}
- )
- ext_pillar.append(
- {
- 'file_tree': {
- 'root_dir': os.path.join(RUNTIME_VARS.PILLAR_DIR, 'base', 'file_tree'),
- 'follow_dir_links': False,
- 'keep_newline': True
- }
- }
- )
- # We need to copy the extension modules into the new master root_dir or
- # it will be prefixed by it
- extension_modules_path = session_root_dir.join('extension_modules').strpath
- if not os.path.exists(extension_modules_path):
- shutil.copytree(
- os.path.join(
- RUNTIME_VARS.FILES, 'extension_modules'
- ),
- extension_modules_path
- )
- # Copy the autosign_file to the new master root_dir
- autosign_file_path = session_root_dir.join('autosign_file').strpath
- shutil.copyfile(
- os.path.join(RUNTIME_VARS.FILES, 'autosign_file'),
- autosign_file_path
- )
- # all read, only owner write
- autosign_file_permissions = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | stat.S_IWUSR
- os.chmod(autosign_file_path, autosign_file_permissions)
- pytest_stop_sending_events_file = session_root_dir.join('pytest_stop_sending_events_file').strpath
- with salt.utils.files.fopen(pytest_stop_sending_events_file, 'w') as wfh:
- wfh.write('')
- return {
- 'pillar_opts': True,
- 'ext_pillar': ext_pillar,
- 'extension_modules': extension_modules_path,
- 'file_roots': {
- 'base': [
- os.path.join(RUNTIME_VARS.FILES, 'file', 'base'),
- ],
- # Alternate root to test __env__ choices
- 'prod': [
- os.path.join(RUNTIME_VARS.FILES, 'file', 'prod'),
- ]
- },
- 'pillar_roots': {
- 'base': [
- os.path.join(RUNTIME_VARS.FILES, 'pillar', 'base'),
- ]
- },
- 'reactor': [
- {
- 'salt/minion/*/start': [
- os.path.join(RUNTIME_VARS.FILES, 'reactor-sync-minion.sls')
- ],
- },
- {
- 'salt/test/reactor': [
- os.path.join(RUNTIME_VARS.FILES, 'reactor-test.sls')
- ],
- }
- ],
- 'pytest_stop_sending_events_file': pytest_stop_sending_events_file
- }
- @pytest.fixture(scope='session')
- def session_minion_default_options(request, tempdir):
- with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, 'minion')) as rfh:
- opts = yaml.deserialize(rfh.read())
- opts['hosts.file'] = tempdir.join('hosts').strpath
- opts['aliases.file'] = tempdir.join('aliases').strpath
- opts['transport'] = request.config.getoption('--transport')
- return opts
- def _get_virtualenv_binary_path():
- try:
- return _get_virtualenv_binary_path.__virtualenv_binary__
- except AttributeError:
- # Under windows we can't seem to properly create a virtualenv off of another
- # virtualenv, we can on linux but we will still point to the virtualenv binary
- # outside the virtualenv running the test suite, if that's the case.
- try:
- real_prefix = sys.real_prefix
- # The above attribute exists, this is a virtualenv
- if salt.utils.platform.is_windows():
- virtualenv_binary = os.path.join(real_prefix, 'Scripts', 'virtualenv.exe')
- else:
- # We need to remove the virtualenv from PATH or we'll get the virtualenv binary
- # from within the virtualenv, we don't want that
- path = os.environ.get('PATH')
- if path is not None:
- path_items = path.split(os.pathsep)
- for item in path_items[:]:
- if item.startswith(sys.base_prefix):
- path_items.remove(item)
- os.environ['PATH'] = os.pathsep.join(path_items)
- virtualenv_binary = salt.utils.path.which('virtualenv')
- if path is not None:
- # Restore previous environ PATH
- os.environ['PATH'] = path
- if not virtualenv_binary.startswith(real_prefix):
- virtualenv_binary = None
- if virtualenv_binary and not os.path.exists(virtualenv_binary):
- # It doesn't exist?!
- virtualenv_binary = None
- except AttributeError:
- # We're not running inside a virtualenv
- virtualenv_binary = None
- _get_virtualenv_binary_path.__virtualenv_binary__ = virtualenv_binary
- return virtualenv_binary
- @pytest.fixture(scope='session')
- def session_minion_config_overrides():
- opts = {
- 'file_roots': {
- 'base': [
- os.path.join(RUNTIME_VARS.FILES, 'file', 'base'),
- ],
- # Alternate root to test __env__ choices
- 'prod': [
- os.path.join(RUNTIME_VARS.FILES, 'file', 'prod'),
- ]
- },
- 'pillar_roots': {
- 'base': [
- os.path.join(RUNTIME_VARS.FILES, 'pillar', 'base'),
- ]
- },
- }
- virtualenv_binary = _get_virtualenv_binary_path()
- if virtualenv_binary:
- opts['venv_bin'] = virtualenv_binary
- return opts
- @pytest.fixture(scope='session')
- def session_secondary_minion_default_options(request, tempdir):
- with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, 'sub_minion')) as rfh:
- opts = yaml.deserialize(rfh.read())
- opts['hosts.file'] = tempdir.join('hosts').strpath
- opts['aliases.file'] = tempdir.join('aliases').strpath
- opts['transport'] = request.config.getoption('--transport')
- return opts
- @pytest.fixture(scope='session')
- def session_seconary_minion_config_overrides():
- opts = {}
- virtualenv_binary = _get_virtualenv_binary_path()
- if virtualenv_binary:
- opts['venv_bin'] = virtualenv_binary
- return opts
- @pytest.fixture(scope='session')
- def session_master_of_masters_default_options(request, tempdir):
- with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, 'syndic_master')) as rfh:
- opts = yaml.deserialize(rfh.read())
- opts['hosts.file'] = tempdir.join('hosts').strpath
- opts['aliases.file'] = tempdir.join('aliases').strpath
- opts['transport'] = request.config.getoption('--transport')
- return opts
- @pytest.fixture(scope='session')
- def session_master_of_masters_config_overrides(session_master_of_masters_root_dir):
- if salt.utils.platform.is_windows():
- ext_pillar = {'cmd_yaml': 'type {0}'.format(os.path.join(RUNTIME_VARS.FILES, 'ext.yaml'))}
- else:
- ext_pillar = {'cmd_yaml': 'cat {0}'.format(os.path.join(RUNTIME_VARS.FILES, 'ext.yaml'))}
- # We need to copy the extension modules into the new master root_dir or
- # it will be prefixed by it
- extension_modules_path = session_master_of_masters_root_dir.join('extension_modules').strpath
- if not os.path.exists(extension_modules_path):
- shutil.copytree(
- os.path.join(
- RUNTIME_VARS.FILES, 'extension_modules'
- ),
- extension_modules_path
- )
- # Copy the autosign_file to the new master root_dir
- autosign_file_path = session_master_of_masters_root_dir.join('autosign_file').strpath
- shutil.copyfile(
- os.path.join(RUNTIME_VARS.FILES, 'autosign_file'),
- autosign_file_path
- )
- # all read, only owner write
- autosign_file_permissions = stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH | stat.S_IWUSR
- os.chmod(autosign_file_path, autosign_file_permissions)
- pytest_stop_sending_events_file = session_master_of_masters_root_dir.join('pytest_stop_sending_events_file').strpath
- with salt.utils.files.fopen(pytest_stop_sending_events_file, 'w') as wfh:
- wfh.write('')
- return {
- 'ext_pillar': [ext_pillar],
- 'extension_modules': extension_modules_path,
- 'file_roots': {
- 'base': [
- os.path.join(RUNTIME_VARS.FILES, 'file', 'base'),
- ],
- # Alternate root to test __env__ choices
- 'prod': [
- os.path.join(RUNTIME_VARS.FILES, 'file', 'prod'),
- ]
- },
- 'pillar_roots': {
- 'base': [
- os.path.join(RUNTIME_VARS.FILES, 'pillar', 'base'),
- ]
- },
- 'pytest_stop_sending_events_file': pytest_stop_sending_events_file
- }
- @pytest.fixture(scope='session')
- def session_syndic_master_default_options(request, tempdir):
- with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, 'syndic_master')) as rfh:
- opts = yaml.deserialize(rfh.read())
- opts['hosts.file'] = tempdir.join('hosts').strpath
- opts['aliases.file'] = tempdir.join('aliases').strpath
- opts['transport'] = request.config.getoption('--transport')
- return opts
- @pytest.fixture(scope='session')
- def session_syndic_default_options(request, tempdir):
- with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, 'syndic')) as rfh:
- opts = yaml.deserialize(rfh.read())
- opts['hosts.file'] = tempdir.join('hosts').strpath
- opts['aliases.file'] = tempdir.join('aliases').strpath
- opts['transport'] = request.config.getoption('--transport')
- return opts
- @pytest.fixture(scope='session')
- def session_proxy_default_options(request, tempdir):
- with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, 'proxy')) as rfh:
- opts = yaml.deserialize(rfh.read())
- opts['hosts.file'] = tempdir.join('hosts').strpath
- opts['aliases.file'] = tempdir.join('aliases').strpath
- opts['transport'] = request.config.getoption('--transport')
- return opts
- @pytest.fixture(scope='session', autouse=True)
- def bridge_pytest_and_runtests(reap_stray_processes,
- session_root_dir,
- session_conf_dir,
- session_secondary_conf_dir,
- session_syndic_conf_dir,
- session_master_of_masters_conf_dir,
- session_base_env_pillar_tree_root_dir,
- session_base_env_state_tree_root_dir,
- session_prod_env_state_tree_root_dir,
- session_master_config,
- session_minion_config,
- session_secondary_minion_config,
- session_master_of_masters_config,
- session_syndic_config):
- # Make sure unittest2 classes know their paths
- RUNTIME_VARS.TMP_ROOT_DIR = session_root_dir.realpath().strpath
- RUNTIME_VARS.TMP_CONF_DIR = session_conf_dir.realpath().strpath
- RUNTIME_VARS.TMP_SUB_MINION_CONF_DIR = session_secondary_conf_dir.realpath().strpath
- RUNTIME_VARS.TMP_SYNDIC_MASTER_CONF_DIR = session_master_of_masters_conf_dir.realpath().strpath
- RUNTIME_VARS.TMP_SYNDIC_MINION_CONF_DIR = session_syndic_conf_dir.realpath().strpath
- RUNTIME_VARS.TMP_PILLAR_TREE = session_base_env_pillar_tree_root_dir.realpath().strpath
- RUNTIME_VARS.TMP_STATE_TREE = session_base_env_state_tree_root_dir.realpath().strpath
- RUNTIME_VARS.TMP_PRODENV_STATE_TREE = session_prod_env_state_tree_root_dir.realpath().strpath
- # Make sure unittest2 uses the pytest generated configuration
- RUNTIME_VARS.RUNTIME_CONFIGS['master'] = freeze(session_master_config)
- RUNTIME_VARS.RUNTIME_CONFIGS['minion'] = freeze(session_minion_config)
- RUNTIME_VARS.RUNTIME_CONFIGS['sub_minion'] = freeze(session_secondary_minion_config)
- RUNTIME_VARS.RUNTIME_CONFIGS['syndic_master'] = freeze(session_master_of_masters_config)
- RUNTIME_VARS.RUNTIME_CONFIGS['syndic'] = freeze(session_syndic_config)
- RUNTIME_VARS.RUNTIME_CONFIGS['client_config'] = freeze(
- salt.config.client_config(session_conf_dir.join('master').strpath)
- )
- # Copy configuration files and directories which are not automatically generated
- for entry in os.listdir(RUNTIME_VARS.CONF_DIR):
- if entry in ('master', 'minion', 'sub_minion', 'syndic', 'syndic_master', 'proxy'):
- # These have runtime computed values and are handled by pytest-salt fixtures
- continue
- entry_path = os.path.join(RUNTIME_VARS.CONF_DIR, entry)
- if os.path.isfile(entry_path):
- shutil.copy(
- entry_path,
- os.path.join(RUNTIME_VARS.TMP_CONF_DIR, entry)
- )
- elif os.path.isdir(entry_path):
- shutil.copytree(
- entry_path,
- os.path.join(RUNTIME_VARS.TMP_CONF_DIR, entry)
- )
- # <---- Salt Configuration -------------------------------------------------------------------------------------------
- # <---- Fixtures Overrides -------------------------------------------------------------------------------------------
- # ----- Custom Grains Mark Evaluator -------------------------------------------------------------------------------->
- class GrainsMarkEvaluator(MarkEvaluator):
- _cached_grains = None
- def _getglobals(self):
- item_globals = super(GrainsMarkEvaluator, self)._getglobals()
- if GrainsMarkEvaluator._cached_grains is None:
- sminion = create_sminion()
- GrainsMarkEvaluator._cached_grains = sminion.opts['grains'].copy()
- item_globals['grains'] = GrainsMarkEvaluator._cached_grains.copy()
- return item_globals
- # Patch PyTest's skipping MarkEvaluator to use our GrainsMarkEvaluator
- _pytest.skipping.MarkEvaluator = GrainsMarkEvaluator
- # <---- Custom Grains Mark Evaluator ---------------------------------------------------------------------------------
- # ----- Custom Fixtures --------------------------------------------------------------------------------------------->
- @pytest.fixture(scope='session')
- def reap_stray_processes():
- # Run tests
- yield
- children = psutil.Process(os.getpid()).children(recursive=True)
- if not children:
- log.info('No astray processes found')
- return
- def on_terminate(proc):
- log.debug('Process %s terminated with exit code %s', proc, proc.returncode)
- if children:
- # Reverse the order, sublings first, parents after
- children.reverse()
- log.warning(
- 'Test suite left %d astray processes running. Killing those processes:\n%s',
- len(children),
- pprint.pformat(children)
- )
- _, alive = psutil.wait_procs(children, timeout=3, callback=on_terminate)
- for child in alive:
- try:
- child.kill()
- except psutil.NoSuchProcess:
- continue
- _, alive = psutil.wait_procs(alive, timeout=3, callback=on_terminate)
- if alive:
- # Give up
- for child in alive:
- log.warning('Process %s survived SIGKILL, giving up:\n%s', child, pprint.pformat(child.as_dict()))
- @pytest.fixture(scope='session')
- def grains(request):
- sminion = create_sminion()
- return sminion.opts['grains'].copy()
- # <---- Custom Fixtures ----------------------------------------------------------------------------------------------
|