mirror of
https://github.com/saltstack/salt.git
synced 2025-04-10 06:41:40 +00:00
1702 lines
62 KiB
Python
1702 lines
62 KiB
Python
# pylint: disable=wrong-import-order,wrong-import-position,3rd-party-local-module-not-gated
|
|
# pylint: disable=redefined-outer-name,invalid-name,3rd-party-module-not-gated
|
|
|
|
|
|
import logging
|
|
import os
|
|
import pathlib
|
|
import re
|
|
import shutil
|
|
import stat
|
|
import sys
|
|
from functools import lru_cache
|
|
from unittest import TestCase # pylint: disable=blacklisted-module
|
|
|
|
import _pytest.logging
|
|
import _pytest.skipping
|
|
import more_itertools
|
|
import pytest
|
|
import pytestskipmarkers
|
|
|
|
import salt
|
|
import salt._logging
|
|
import salt._logging.mixins
|
|
import salt.config
|
|
import salt.utils.files
|
|
import salt.utils.path
|
|
import salt.utils.platform
|
|
import salt.utils.win_functions
|
|
from salt.serializers import yaml
|
|
from salt.utils.immutabletypes import freeze
|
|
from tests.support.helpers import (
|
|
PRE_PYTEST_SKIP_OR_NOT,
|
|
PRE_PYTEST_SKIP_REASON,
|
|
get_virtualenv_binary_path,
|
|
)
|
|
from tests.support.pytest.helpers import * # pylint: disable=unused-wildcard-import,wildcard-import
|
|
from tests.support.runtests import RUNTIME_VARS
|
|
from tests.support.sminion import check_required_sminion_attributes, create_sminion
|
|
|
|
TESTS_DIR = pathlib.Path(__file__).resolve().parent
|
|
PYTESTS_DIR = TESTS_DIR / "pytests"
|
|
CODE_DIR = TESTS_DIR.parent
|
|
|
|
# Change to code checkout directory
|
|
os.chdir(str(CODE_DIR))
|
|
|
|
# Make sure the current directory is the first item in sys.path
|
|
if str(CODE_DIR) in sys.path:
|
|
sys.path.remove(str(CODE_DIR))
|
|
if os.environ.get("ONEDIR_TESTRUN", "0") == "0":
|
|
sys.path.insert(0, str(CODE_DIR))
|
|
|
|
os.environ["REPO_ROOT_DIR"] = str(CODE_DIR)
|
|
|
|
# Coverage
|
|
if "COVERAGE_PROCESS_START" in os.environ:
|
|
MAYBE_RUN_COVERAGE = True
|
|
COVERAGERC_FILE = os.environ["COVERAGE_PROCESS_START"]
|
|
else:
|
|
COVERAGERC_FILE = str(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["COVERAGE_PROCESS_START"] = str(COVERAGERC_FILE)
|
|
|
|
# Variable defining a FIPS test run or not
|
|
FIPS_TESTRUN = os.environ.get("FIPS_TESTRUN", "0") == "1"
|
|
|
|
# Define the pytest plugins we rely on
|
|
pytest_plugins = ["helpers_namespace"]
|
|
|
|
# Define where not to collect tests from
|
|
collect_ignore = ["setup.py"]
|
|
|
|
|
|
# Patch PyTest logging handlers
|
|
class LogCaptureHandler(
|
|
salt._logging.mixins.ExcInfoOnLogLevelFormatMixin, _pytest.logging.LogCaptureHandler
|
|
):
|
|
"""
|
|
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._logging.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)
|
|
handler.close()
|
|
|
|
|
|
# Reset the root logger to its default level(because salt changed it)
|
|
logging.root.setLevel(logging.WARNING)
|
|
|
|
log = logging.getLogger("salt.testsuite")
|
|
|
|
|
|
# ----- CLI Options Setup ------------------------------------------------------------------------------------------->
|
|
def pytest_addoption(parser):
|
|
"""
|
|
register argparse-style options and ini-style config values.
|
|
"""
|
|
test_selection_group = parser.getgroup("Tests Selection")
|
|
test_selection_group.addoption(
|
|
"--from-filenames",
|
|
default=None,
|
|
help=(
|
|
"Pass a comma-separated list of file paths, and any test module which"
|
|
" corresponds to the specified file(s) will run. For example, if 'setup.py'"
|
|
" was passed, then the corresponding test files defined in"
|
|
" 'tests/filename_map.yml' would run. Absolute paths are assumed to be"
|
|
" files containing relative paths, one per line. Providing the paths in a"
|
|
" file can help get around shell character limits when the list of files is"
|
|
" long."
|
|
),
|
|
)
|
|
# Add deprecated CLI flag until we completely switch to PyTest
|
|
test_selection_group.addoption(
|
|
"--names-file", default=None, help="Deprecated option"
|
|
)
|
|
test_selection_group.addoption(
|
|
"--transport",
|
|
default="zeromq",
|
|
choices=("zeromq", "tcp"),
|
|
help=(
|
|
"Select which transport to run the integration tests with, zeromq or tcp."
|
|
" Default: %(default)s"
|
|
),
|
|
)
|
|
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(
|
|
"--no-fast",
|
|
"--no-fast-tests",
|
|
dest="fast",
|
|
action="store_true",
|
|
default=False,
|
|
help="Don't run salt-fast tests. Default: %(default)s",
|
|
)
|
|
test_selection_group.addoption(
|
|
"--run-slow",
|
|
"--slow",
|
|
"--slow-tests",
|
|
dest="slow",
|
|
action="store_true",
|
|
default=False,
|
|
help="Run slow tests. Default: %(default)s",
|
|
)
|
|
test_selection_group.addoption(
|
|
"--core",
|
|
"--core-tests",
|
|
dest="core",
|
|
action="store_true",
|
|
default=False,
|
|
help=(
|
|
"Run salt-core tests. These tests test the engine of salt! "
|
|
"Default: %(default)s"
|
|
),
|
|
)
|
|
test_selection_group.addoption(
|
|
"--flaky",
|
|
"--flaky-jail",
|
|
dest="flaky",
|
|
action="store_true",
|
|
default=False,
|
|
help=(
|
|
"Run salt-flaky jail tests. These tests are in jail for being flaky! "
|
|
"One day they will be made not flaky."
|
|
"Default: %(default)s"
|
|
),
|
|
)
|
|
test_selection_group.addoption(
|
|
"--proxy",
|
|
"--proxy-tests",
|
|
dest="proxy",
|
|
action="store_true",
|
|
default=False,
|
|
help="Run proxy tests (DEPRECATED)",
|
|
)
|
|
|
|
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.hookimpl(trylast=True)
|
|
def pytest_configure(config):
|
|
"""
|
|
called after command line options have been parsed
|
|
and all plugins and initial conftest files been loaded.
|
|
"""
|
|
try:
|
|
assert config._onedir_check_complete
|
|
return
|
|
except AttributeError:
|
|
if os.environ.get("ONEDIR_TESTRUN", "0") == "1":
|
|
if pathlib.Path(salt.__file__).parent == CODE_DIR / "salt":
|
|
raise pytest.UsageError(
|
|
"Apparently running the test suite against the onedir build "
|
|
"of salt, however, the imported salt package is pointing to "
|
|
"the respository checkout instead of the onedir package.\n\n"
|
|
f" * sys.path: {sys.path}"
|
|
)
|
|
config._onedir_check_complete = True
|
|
|
|
for dirname in CODE_DIR.iterdir():
|
|
if not dirname.is_dir():
|
|
continue
|
|
if dirname != TESTS_DIR:
|
|
config.addinivalue_line("norecursedirs", str(CODE_DIR / dirname))
|
|
config.addinivalue_line(
|
|
"norecursedirs",
|
|
str(TESTS_DIR / "unit" / "modules" / "inspectlib" / "tree_test"),
|
|
)
|
|
|
|
# Expose the markers we use to pytest CLI
|
|
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"
|
|
)
|
|
config.addinivalue_line(
|
|
"markers", "requires_sshd_server: Mark test that require an SSH server running"
|
|
)
|
|
config.addinivalue_line(
|
|
"markers",
|
|
"slow_test: Mark test as being slow. These tests are skipped by default unless"
|
|
" `--run-slow` is passed",
|
|
)
|
|
config.addinivalue_line(
|
|
"markers",
|
|
"core_test: Mark test as being core. These tests are skipped by default unless"
|
|
" `--core-tests` is passed",
|
|
)
|
|
config.addinivalue_line(
|
|
"markers",
|
|
"flaky_jail: Mark test as being jlaky. These tests are skipped by default unless"
|
|
" `--flaky-jail` is passed",
|
|
)
|
|
config.addinivalue_line(
|
|
"markers",
|
|
"async_timeout: Timeout, in seconds, for asynchronous test functions(`async def`)",
|
|
)
|
|
config.addinivalue_line(
|
|
"markers",
|
|
"requires_random_entropy(minimum={}, timeout={}, skip=True): Mark test as "
|
|
"requiring a minimum value of random entropy. In the case where the value is lower "
|
|
"than the provided 'minimum', an attempt will be made to raise that value up until "
|
|
"the provided 'timeout' minutes have passed, at which time, depending on the value "
|
|
"of 'skip' the test will skip or fail. For entropy poolsizes of 256 bits, the min "
|
|
"is adjusted to 192.".format(
|
|
EntropyGenerator.minimum_entropy, EntropyGenerator.max_minutes
|
|
),
|
|
)
|
|
config.addinivalue_line(
|
|
"markers",
|
|
"skip_initial_gh_actions_failure(skip=<boolean or callable, reason=None): Skip known test failures "
|
|
"under the new GH Actions setup if the environment variable SKIP_INITIAL_GH_ACTIONS_FAILURES "
|
|
"is equal to '1' and the 'skip' keyword argument is either `True` or it's a callable that "
|
|
"when called returns `True`. If `skip` is a callable, it should accept a single argument "
|
|
"'grains', which is the grains dictionary.",
|
|
)
|
|
config.addinivalue_line(
|
|
"markers",
|
|
"skip_initial_onedir_failure(skip=<boolean or callable, reason=None): Skip known test failures "
|
|
"under the new onedir builds if the environment variable SKIP_INITIAL_ONEDIR_FAILURES "
|
|
"is equal to '1' and the 'skip' keyword argument is either `True` or it's a callable that "
|
|
"when called returns `True`. If `skip` is a callable, it should accept a single argument "
|
|
"'grains', which is the grains dictionary.",
|
|
)
|
|
config.addinivalue_line(
|
|
"markers",
|
|
"timeout_unless_on_windows(*args, **kwargs): Apply the 'timeout' marker unless running "
|
|
"on Windows.",
|
|
)
|
|
# "Flag" the slowTest decorator if we're skipping slow tests or not
|
|
os.environ["SLOW_TESTS"] = str(config.getoption("--run-slow"))
|
|
|
|
|
|
# <---- 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(config):
|
|
soft, hard = set_max_open_files_limits()
|
|
transport = config.getoption("--transport")
|
|
return f"max open files: soft={soft}; hard={hard}\nsalt-transport: {transport}"
|
|
|
|
|
|
def pytest_itemcollected(item):
|
|
"""We just collected a test item."""
|
|
try:
|
|
pathlib.Path(item.fspath.strpath).resolve().relative_to(PYTESTS_DIR)
|
|
# Test is under tests/pytests
|
|
if item.cls and issubclass(item.cls, TestCase):
|
|
pytest.fail(
|
|
"The tests under {0!r} MUST NOT use unittest's TestCase class or a"
|
|
" subclass of it. Please move {1!r} outside of {0!r}".format(
|
|
str(PYTESTS_DIR.relative_to(CODE_DIR)), item.nodeid
|
|
)
|
|
)
|
|
except ValueError:
|
|
# Test is not under tests/pytests
|
|
if not item.cls or (item.cls and not issubclass(item.cls, TestCase)):
|
|
pytest.fail(
|
|
"The test {!r} appears to be written for pytest but it's not under"
|
|
" {!r}. Please move it there.".format(
|
|
item.nodeid,
|
|
str(PYTESTS_DIR.relative_to(CODE_DIR)),
|
|
)
|
|
)
|
|
|
|
|
|
@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)
|
|
from_filenames_collection_modifyitems(config, items)
|
|
|
|
timeout_marker_tests_paths = (
|
|
str(PYTESTS_DIR / "pkg"),
|
|
str(PYTESTS_DIR / "scenarios"),
|
|
)
|
|
for item in items:
|
|
marker = item.get_closest_marker("timeout_unless_on_windows")
|
|
if marker is not None:
|
|
if not salt.utils.platform.is_windows():
|
|
# Apply the marker since we're not on windows
|
|
marker_kwargs = marker.kwargs.copy()
|
|
if "func_only" not in marker_kwargs:
|
|
# Default to counting only the test execution for the timeouts, ie,
|
|
# withough including the fixtures setup time towards the timeout.
|
|
marker_kwargs["func_only"] = True
|
|
item.add_marker(pytest.mark.timeout(*marker.args, **marker_kwargs))
|
|
else:
|
|
if (
|
|
not salt.utils.platform.is_windows()
|
|
and not str(pathlib.Path(item.fspath).resolve()).startswith(
|
|
timeout_marker_tests_paths
|
|
)
|
|
and not item.get_closest_marker("timeout")
|
|
):
|
|
# Let's apply the timeout marker on the test, if the marker
|
|
# is not already applied
|
|
# Default to counting only the test execution for the timeouts, ie,
|
|
# withough including the fixtures setup time towards the timeout.
|
|
item.add_marker(pytest.mark.timeout(90, func_only=True))
|
|
|
|
|
|
def pytest_markeval_namespace(config):
|
|
"""
|
|
Called when constructing the globals dictionary used for evaluating string conditions in xfail/skipif markers.
|
|
|
|
This is useful when the condition for a marker requires objects that are expensive or impossible to obtain during
|
|
collection time, which is required by normal boolean conditions.
|
|
|
|
:param config: The pytest config object.
|
|
:returns: A dictionary of additional globals to add.
|
|
"""
|
|
return {"grains": _grains_for_marker()}
|
|
|
|
|
|
# <---- PyTest Tweaks ------------------------------------------------------------------------------------------------
|
|
|
|
|
|
# ----- Test Setup -------------------------------------------------------------------------------------------------->
|
|
@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 = str(TESTS_DIR / "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)
|
|
test_group_count = sum(
|
|
bool(item.get_closest_marker(group))
|
|
for group in ("core_test", "slow_test", "flaky_jail")
|
|
)
|
|
if item.get_closest_marker("core_test") and item.get_closest_marker("slow_test"):
|
|
raise pytest.UsageError(
|
|
"Tests can only be in one test group. ('core_test', 'slow_test')"
|
|
)
|
|
|
|
if item.get_closest_marker("flaky_jail"):
|
|
if not item.config.getoption("--flaky-jail"):
|
|
raise pytest.skip.Exception(
|
|
"flaky jail tests are disabled, pass '--flaky-jail' to enable them.",
|
|
_use_item_location=True,
|
|
)
|
|
else:
|
|
if item.get_closest_marker("core_test"):
|
|
if not item.config.getoption("--core-tests"):
|
|
raise pytest.skip.Exception(
|
|
"Core tests are disabled, pass '--core-tests' to enable them.",
|
|
_use_item_location=True,
|
|
)
|
|
if item.get_closest_marker("slow_test"):
|
|
if not item.config.getoption("--slow-tests"):
|
|
raise pytest.skip.Exception(
|
|
"Slow tests are disabled, pass '--run-slow' to enable them.",
|
|
_use_item_location=True,
|
|
)
|
|
if test_group_count == 0 and item.config.getoption("--no-fast-tests"):
|
|
raise pytest.skip.Exception(
|
|
"Fast tests have been disabled by '--no-fast-tests'.",
|
|
_use_item_location=True,
|
|
)
|
|
|
|
requires_sshd_server_marker = item.get_closest_marker("requires_sshd_server")
|
|
if requires_sshd_server_marker is not None:
|
|
if not item.config.getoption("--ssh-tests"):
|
|
item._skipped_by_mark = True
|
|
pytest.skip("SSH tests are disabled, pass '--ssh-tests' to enable them.")
|
|
item.fixturenames.append("sshd_server")
|
|
item.fixturenames.append("salt_ssh_roster_file")
|
|
|
|
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)
|
|
)
|
|
)
|
|
|
|
skip_initial_gh_actions_failures_env_set = (
|
|
os.environ.get("SKIP_INITIAL_GH_ACTIONS_FAILURES", "0") == "1"
|
|
)
|
|
skip_initial_gh_actions_failure_marker = item.get_closest_marker(
|
|
"skip_initial_gh_actions_failure"
|
|
)
|
|
if (
|
|
skip_initial_gh_actions_failure_marker is not None
|
|
and skip_initial_gh_actions_failures_env_set
|
|
):
|
|
if skip_initial_gh_actions_failure_marker.args:
|
|
raise pytest.UsageError(
|
|
"'skip_initial_gh_actions_failure' marker does not accept any arguments "
|
|
"only keyword arguments."
|
|
)
|
|
kwargs = skip_initial_gh_actions_failure_marker.kwargs.copy()
|
|
skip = kwargs.pop("skip", True)
|
|
if skip and not callable(skip) and not isinstance(skip, bool):
|
|
raise pytest.UsageError(
|
|
"The 'skip' keyword argument to the 'skip_initial_gh_actions_failure' marker "
|
|
"requires a boolean or callable, not '{}'.".format(type(skip))
|
|
)
|
|
reason = kwargs.pop("reason", None)
|
|
if reason is None:
|
|
reason = "Test skipped because it's a know GH Actions initial failure that needs to be fixed"
|
|
if kwargs:
|
|
raise pytest.UsageError(
|
|
"'skip_initial_gh_actions_failure' marker does not accept any keyword arguments "
|
|
"except 'skip' and 'reason'."
|
|
)
|
|
if skip and callable(skip):
|
|
grains = _grains_for_marker()
|
|
skip = skip(grains)
|
|
|
|
if skip:
|
|
raise pytest.skip.Exception(reason, _use_item_location=True)
|
|
|
|
skip_initial_onedir_failures_env_set = (
|
|
os.environ.get("SKIP_INITIAL_ONEDIR_FAILURES", "0") == "1"
|
|
)
|
|
skip_initial_onedir_failure_marker = item.get_closest_marker(
|
|
"skip_initial_onedir_failure"
|
|
)
|
|
if (
|
|
skip_initial_onedir_failure_marker is not None
|
|
and skip_initial_onedir_failures_env_set
|
|
):
|
|
if skip_initial_onedir_failure_marker.args:
|
|
raise pytest.UsageError(
|
|
"'skip_initial_onedir_failure' marker does not accept any arguments "
|
|
"only keyword arguments."
|
|
)
|
|
kwargs = skip_initial_onedir_failure_marker.kwargs.copy()
|
|
skip = kwargs.pop("skip", True)
|
|
if skip and not callable(skip) and not isinstance(skip, bool):
|
|
raise pytest.UsageError(
|
|
"The 'skip' keyword argument to the 'skip_initial_onedir_failure' marker "
|
|
"requires a boolean or callable, not '{}'.".format(type(skip))
|
|
)
|
|
reason = kwargs.pop("reason", None)
|
|
if reason is None:
|
|
reason = "Test skipped because it's a know GH Actions initial failure that needs to be fixed"
|
|
if kwargs:
|
|
raise pytest.UsageError(
|
|
"'skip_initial_onedir_failure' marker does not accept any keyword arguments "
|
|
"except 'skip' and 'reason'."
|
|
)
|
|
if skip and callable(skip):
|
|
grains = _grains_for_marker()
|
|
skip = skip(grains)
|
|
|
|
if skip:
|
|
raise pytest.skip.Exception(reason, _use_item_location=True)
|
|
|
|
requires_random_entropy_marker = item.get_closest_marker("requires_random_entropy")
|
|
if requires_random_entropy_marker is not None:
|
|
if requires_random_entropy_marker.args:
|
|
raise pytest.UsageError(
|
|
"'requires_random_entropy' marker does not accept any arguments "
|
|
"only keyword arguments."
|
|
)
|
|
kwargs = requires_random_entropy_marker.kwargs.copy()
|
|
skip = kwargs.pop("skip", None)
|
|
if skip and not isinstance(skip, bool):
|
|
raise pytest.UsageError(
|
|
"The 'skip' keyword argument to the 'requires_random_entropy' marker "
|
|
"requires a boolean not '{}'.".format(type(skip))
|
|
)
|
|
minimum_entropy = kwargs.pop("minimum", None)
|
|
if minimum_entropy is not None:
|
|
if not isinstance(minimum_entropy, int):
|
|
raise pytest.UsageError(
|
|
"The 'minimum' keyword argument to the 'requires_random_entropy' marker "
|
|
"must be an integer not '{}'.".format(type(minimum_entropy))
|
|
)
|
|
if minimum_entropy <= 0:
|
|
raise pytest.UsageError(
|
|
"The 'minimum' keyword argument to the 'requires_random_entropy' marker "
|
|
"must be an positive integer not '{}'.".format(minimum_entropy)
|
|
)
|
|
max_minutes = kwargs.pop("timeout", None)
|
|
if max_minutes is not None:
|
|
if not isinstance(max_minutes, int):
|
|
raise pytest.UsageError(
|
|
"The 'timeout' keyword argument to the 'requires_random_entropy' marker "
|
|
"must be an integer not '{}'.".format(type(max_minutes))
|
|
)
|
|
if max_minutes <= 0:
|
|
raise pytest.UsageError(
|
|
"The 'timeout' keyword argument to the 'requires_random_entropy' marker "
|
|
"must be an positive integer not '{}'.".format(max_minutes)
|
|
)
|
|
if kwargs:
|
|
raise pytest.UsageError(
|
|
"Unsupported keyword arguments passed to the 'requires_random_entropy' "
|
|
"marker: {}".format(", ".join(list(kwargs)))
|
|
)
|
|
entropy_generator = EntropyGenerator(
|
|
minimum_entropy=minimum_entropy, max_minutes=max_minutes, skip=skip
|
|
)
|
|
entropy_generator.generate_entropy()
|
|
|
|
if salt.utils.platform.is_windows():
|
|
auto_whitelisted_paths = (
|
|
str(TESTS_DIR / "unit"),
|
|
str(PYTESTS_DIR / "unit"),
|
|
str(PYTESTS_DIR / "pkg"),
|
|
)
|
|
if not str(pathlib.Path(item.fspath).resolve()).startswith(
|
|
auto_whitelisted_paths
|
|
):
|
|
# 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 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
|
|
|
|
if group_count == 1:
|
|
# Just one group, don't do any filtering
|
|
return
|
|
|
|
terminal_reporter = config.pluginmanager.get_plugin("terminalreporter")
|
|
|
|
if config.getoption("--last-failed") or config.getoption("--failed-first"):
|
|
# This is a test failure rerun, applying test groups would break this
|
|
terminal_reporter.write(
|
|
"\nNot splitting collected tests into chunks since --lf/--last-failed or "
|
|
"-ff/--failed-first was passed on the CLI.\n",
|
|
yellow=True,
|
|
)
|
|
return
|
|
|
|
total_items = len(items)
|
|
|
|
# Devide into test groups
|
|
test_groups = more_itertools.divide(group_count, items)
|
|
# Pick the right group
|
|
tests_in_group = list(test_groups.pop(group_id - 1))
|
|
# The rest are deselected tests
|
|
deselected = list(more_itertools.collapse(test_groups))
|
|
# Sanity check
|
|
assert len(tests_in_group) + len(deselected) == total_items
|
|
# Replace all items in the list
|
|
items[:] = tests_in_group
|
|
if deselected:
|
|
config.hook.pytest_deselected(items=deselected)
|
|
|
|
terminal_reporter.write(
|
|
f"Running test group #{group_id}(out of #{group_count}) ({len(items)} out of {total_items} tests)\n",
|
|
yellow=True,
|
|
)
|
|
|
|
|
|
# <---- Test Groups Selection ----------------------------------------------------------------------------------------
|
|
|
|
|
|
# ----- Fixtures Overrides ------------------------------------------------------------------------------------------>
|
|
@pytest.fixture(scope="session")
|
|
def salt_factories_default_root_dir(salt_factories_default_root_dir):
|
|
"""
|
|
The root directory from where to base all salt-factories paths.
|
|
|
|
For example, in a salt system installation, this would be ``/``.
|
|
|
|
.. admonition:: Attention
|
|
|
|
If `root_dir` is returned on the `salt_factories_config()` fixture
|
|
dictionary, then that's the value used, and not the one returned by
|
|
this fixture.
|
|
"""
|
|
if os.environ.get("CI") and pytestskipmarkers.utils.platform.is_windows():
|
|
tempdir = pathlib.Path(
|
|
os.environ.get("RUNNER_TEMP", r"C:\Windows\Temp")
|
|
).resolve()
|
|
return tempdir / "stsuite"
|
|
|
|
return salt_factories_default_root_dir / "stsuite"
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def salt_factories_config():
|
|
"""
|
|
Return a dictionary with the keyworkd arguments for FactoriesManager
|
|
"""
|
|
if os.environ.get("JENKINS_URL") or os.environ.get("CI"):
|
|
start_timeout = 120
|
|
else:
|
|
start_timeout = 60
|
|
|
|
if os.environ.get("ONEDIR_TESTRUN", "0") == "1":
|
|
code_dir = None
|
|
else:
|
|
code_dir = str(CODE_DIR)
|
|
|
|
kwargs = {
|
|
"code_dir": code_dir,
|
|
"start_timeout": start_timeout,
|
|
"inject_sitecustomize": MAYBE_RUN_COVERAGE,
|
|
}
|
|
if MAYBE_RUN_COVERAGE:
|
|
kwargs["coverage_rc_path"] = str(COVERAGERC_FILE)
|
|
else:
|
|
kwargs["coverage_rc_path"] = None
|
|
kwargs["coverage_db_path"] = os.environ.get("COVERAGE_FILE")
|
|
return kwargs
|
|
|
|
|
|
@pytest.fixture
|
|
def tmpdir(tmpdir):
|
|
raise pytest.UsageError(
|
|
"The `tmpdir` fixture uses Pytest's `pypath` implementation which "
|
|
"is getting deprecated in favor of `pathlib`. Please use the "
|
|
"`tmp_path` fixture instead."
|
|
)
|
|
|
|
|
|
# <---- Fixtures Overrides -------------------------------------------------------------------------------------------
|
|
|
|
|
|
# ----- Salt Factories ---------------------------------------------------------------------------------------------->
|
|
@pytest.fixture(scope="session")
|
|
def integration_files_dir(salt_factories):
|
|
"""
|
|
Fixture which returns the salt integration files directory path.
|
|
Creates the directory if it does not yet exist.
|
|
"""
|
|
dirname = salt_factories.root_dir / "integration-files"
|
|
dirname.mkdir(exist_ok=True)
|
|
for child in (PYTESTS_DIR / "integration" / "files").iterdir():
|
|
destpath = dirname / child.name
|
|
if child.is_dir():
|
|
shutil.copytree(str(child), str(destpath), dirs_exist_ok=True)
|
|
else:
|
|
shutil.copyfile(str(child), str(destpath))
|
|
return dirname
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def functional_files_dir(salt_factories):
|
|
"""
|
|
Fixture which returns the salt functional files directory path.
|
|
Creates the directory if it does not yet exist.
|
|
"""
|
|
dirname = salt_factories.root_dir / "functional-files"
|
|
dirname.mkdir(exist_ok=True)
|
|
for child in (PYTESTS_DIR / "functional" / "files").iterdir():
|
|
if child.is_dir():
|
|
shutil.copytree(str(child), str(dirname / child.name))
|
|
else:
|
|
shutil.copyfile(str(child), str(dirname / child.name))
|
|
return dirname
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def state_tree_root_dir(integration_files_dir):
|
|
"""
|
|
Fixture which returns the salt state tree root directory path.
|
|
Creates the directory if it does not yet exist.
|
|
"""
|
|
dirname = integration_files_dir / "state-tree"
|
|
dirname.mkdir(exist_ok=True)
|
|
return dirname
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def pillar_tree_root_dir(integration_files_dir):
|
|
"""
|
|
Fixture which returns the salt pillar tree root directory path.
|
|
Creates the directory if it does not yet exist.
|
|
"""
|
|
dirname = integration_files_dir / "pillar-tree"
|
|
dirname.mkdir(exist_ok=True)
|
|
return dirname
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def base_env_state_tree_root_dir(state_tree_root_dir):
|
|
"""
|
|
Fixture which returns the salt base environment state tree directory path.
|
|
Creates the directory if it does not yet exist.
|
|
"""
|
|
dirname = state_tree_root_dir / "base"
|
|
dirname.mkdir(exist_ok=True)
|
|
RUNTIME_VARS.TMP_STATE_TREE = str(dirname.resolve())
|
|
RUNTIME_VARS.TMP_BASEENV_STATE_TREE = RUNTIME_VARS.TMP_STATE_TREE
|
|
return dirname
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def prod_env_state_tree_root_dir(state_tree_root_dir):
|
|
"""
|
|
Fixture which returns the salt prod environment state tree directory path.
|
|
Creates the directory if it does not yet exist.
|
|
"""
|
|
dirname = state_tree_root_dir / "prod"
|
|
dirname.mkdir(exist_ok=True)
|
|
RUNTIME_VARS.TMP_PRODENV_STATE_TREE = str(dirname.resolve())
|
|
return dirname
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def base_env_pillar_tree_root_dir(pillar_tree_root_dir):
|
|
"""
|
|
Fixture which returns the salt base environment pillar tree directory path.
|
|
Creates the directory if it does not yet exist.
|
|
"""
|
|
dirname = pillar_tree_root_dir / "base"
|
|
dirname.mkdir(exist_ok=True)
|
|
RUNTIME_VARS.TMP_PILLAR_TREE = str(dirname.resolve())
|
|
RUNTIME_VARS.TMP_BASEENV_PILLAR_TREE = RUNTIME_VARS.TMP_PILLAR_TREE
|
|
return dirname
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def ext_pillar_file_tree_root_dir(pillar_tree_root_dir):
|
|
"""
|
|
Fixture which returns the salt pillar file tree directory path.
|
|
Creates the directory if it does not yet exist.
|
|
"""
|
|
dirname = pillar_tree_root_dir / "file-tree"
|
|
dirname.mkdir(exist_ok=True)
|
|
return dirname
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def prod_env_pillar_tree_root_dir(pillar_tree_root_dir):
|
|
"""
|
|
Fixture which returns the salt prod environment pillar tree directory path.
|
|
Creates the directory if it does not yet exist.
|
|
"""
|
|
dirname = pillar_tree_root_dir / "prod"
|
|
dirname.mkdir(exist_ok=True)
|
|
RUNTIME_VARS.TMP_PRODENV_PILLAR_TREE = str(dirname.resolve())
|
|
return dirname
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def salt_syndic_master_factory(
|
|
request,
|
|
salt_factories,
|
|
base_env_state_tree_root_dir,
|
|
base_env_pillar_tree_root_dir,
|
|
prod_env_state_tree_root_dir,
|
|
prod_env_pillar_tree_root_dir,
|
|
):
|
|
root_dir = salt_factories.get_root_dir_for_daemon("syndic_master")
|
|
conf_dir = root_dir / "conf"
|
|
conf_dir.mkdir(exist_ok=True)
|
|
|
|
with salt.utils.files.fopen(
|
|
os.path.join(RUNTIME_VARS.CONF_DIR, "syndic_master")
|
|
) as rfh:
|
|
config_defaults = yaml.deserialize(rfh.read())
|
|
|
|
tests_known_hosts_file = str(root_dir / "salt_ssh_known_hosts")
|
|
with salt.utils.files.fopen(tests_known_hosts_file, "w") as known_hosts:
|
|
known_hosts.write("")
|
|
|
|
config_defaults["root_dir"] = str(root_dir)
|
|
config_defaults["known_hosts_file"] = tests_known_hosts_file
|
|
config_defaults["syndic_master"] = "localhost"
|
|
config_defaults["transport"] = request.config.getoption("--transport")
|
|
|
|
config_overrides = {
|
|
"log_level_logfile": "quiet",
|
|
"fips_mode": FIPS_TESTRUN,
|
|
"publish_signing_algorithm": (
|
|
"PKCS1v15-SHA224" if FIPS_TESTRUN else "PKCS1v15-SHA1"
|
|
),
|
|
}
|
|
ext_pillar = []
|
|
if salt.utils.platform.is_windows():
|
|
ext_pillar.append(
|
|
{"cmd_yaml": "type {}".format(os.path.join(RUNTIME_VARS.FILES, "ext.yaml"))}
|
|
)
|
|
else:
|
|
ext_pillar.append(
|
|
{"cmd_yaml": "cat {}".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 = str(root_dir / "extension_modules")
|
|
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 = str(root_dir / "autosign_file")
|
|
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)
|
|
|
|
config_overrides.update(
|
|
{
|
|
"ext_pillar": ext_pillar,
|
|
"extension_modules": extension_modules_path,
|
|
"file_roots": {
|
|
"base": [
|
|
str(base_env_state_tree_root_dir),
|
|
os.path.join(RUNTIME_VARS.FILES, "file", "base"),
|
|
],
|
|
# Alternate root to test __env__ choices
|
|
"prod": [
|
|
str(prod_env_state_tree_root_dir),
|
|
os.path.join(RUNTIME_VARS.FILES, "file", "prod"),
|
|
],
|
|
},
|
|
"pillar_roots": {
|
|
"base": [
|
|
str(base_env_pillar_tree_root_dir),
|
|
os.path.join(RUNTIME_VARS.FILES, "pillar", "base"),
|
|
],
|
|
"prod": [str(prod_env_pillar_tree_root_dir)],
|
|
},
|
|
}
|
|
)
|
|
|
|
factory = salt_factories.salt_master_daemon(
|
|
"syndic_master",
|
|
order_masters=True,
|
|
defaults=config_defaults,
|
|
overrides=config_overrides,
|
|
extra_cli_arguments_after_first_start_failure=["--log-level=info"],
|
|
)
|
|
return factory
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def salt_syndic_factory(salt_factories, salt_syndic_master_factory):
|
|
config_defaults = {"master": None, "minion": None, "syndic": None}
|
|
with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, "syndic")) as rfh:
|
|
opts = yaml.deserialize(rfh.read())
|
|
|
|
opts["hosts.file"] = os.path.join(RUNTIME_VARS.TMP, "hosts")
|
|
opts["aliases.file"] = os.path.join(RUNTIME_VARS.TMP, "aliases")
|
|
opts["transport"] = salt_syndic_master_factory.config["transport"]
|
|
config_defaults["syndic"] = opts
|
|
config_overrides = {"log_level_logfile": "quiet"}
|
|
factory = salt_syndic_master_factory.salt_syndic_daemon(
|
|
"syndic",
|
|
defaults=config_defaults,
|
|
overrides=config_overrides,
|
|
extra_cli_arguments_after_first_start_failure=["--log-level=info"],
|
|
)
|
|
return factory
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def salt_master_factory(
|
|
salt_factories,
|
|
salt_syndic_master_factory,
|
|
base_env_state_tree_root_dir,
|
|
base_env_pillar_tree_root_dir,
|
|
prod_env_state_tree_root_dir,
|
|
prod_env_pillar_tree_root_dir,
|
|
ext_pillar_file_tree_root_dir,
|
|
salt_api_account_factory,
|
|
):
|
|
root_dir = salt_factories.get_root_dir_for_daemon("master")
|
|
conf_dir = root_dir / "conf"
|
|
conf_dir.mkdir(exist_ok=True)
|
|
|
|
with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, "master")) as rfh:
|
|
config_defaults = yaml.deserialize(rfh.read())
|
|
|
|
tests_known_hosts_file = str(root_dir / "salt_ssh_known_hosts")
|
|
with salt.utils.files.fopen(tests_known_hosts_file, "w") as known_hosts:
|
|
known_hosts.write("")
|
|
|
|
config_defaults["root_dir"] = str(root_dir)
|
|
config_defaults["known_hosts_file"] = tests_known_hosts_file
|
|
config_defaults["syndic_master"] = "localhost"
|
|
config_defaults["transport"] = salt_syndic_master_factory.config["transport"]
|
|
|
|
config_overrides = {
|
|
"log_level_logfile": "quiet",
|
|
"fips_mode": FIPS_TESTRUN,
|
|
"publish_signing_algorithm": (
|
|
"PKCS1v15-SHA224" if FIPS_TESTRUN else "PKCS1v15-SHA1"
|
|
),
|
|
}
|
|
ext_pillar = []
|
|
if salt.utils.platform.is_windows():
|
|
ext_pillar.append(
|
|
{"cmd_yaml": "type {}".format(os.path.join(RUNTIME_VARS.FILES, "ext.yaml"))}
|
|
)
|
|
else:
|
|
ext_pillar.append(
|
|
{"cmd_yaml": "cat {}".format(os.path.join(RUNTIME_VARS.FILES, "ext.yaml"))}
|
|
)
|
|
ext_pillar.append(
|
|
{
|
|
"file_tree": {
|
|
"root_dir": str(ext_pillar_file_tree_root_dir),
|
|
"follow_dir_links": False,
|
|
"keep_newline": True,
|
|
}
|
|
}
|
|
)
|
|
config_overrides["pillar_opts"] = True
|
|
config_overrides["external_auth"] = {
|
|
"auto": {
|
|
salt_api_account_factory.username: [
|
|
"@wheel",
|
|
"@runner",
|
|
"test.*",
|
|
"grains.*",
|
|
],
|
|
}
|
|
}
|
|
|
|
# We need to copy the extension modules into the new master root_dir or
|
|
# it will be prefixed by it
|
|
extension_modules_path = str(root_dir / "extension_modules")
|
|
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 = str(root_dir / "autosign_file")
|
|
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)
|
|
|
|
config_overrides.update(
|
|
{
|
|
"ext_pillar": ext_pillar,
|
|
"extension_modules": extension_modules_path,
|
|
"file_roots": {
|
|
"base": [
|
|
str(base_env_state_tree_root_dir),
|
|
os.path.join(RUNTIME_VARS.FILES, "file", "base"),
|
|
],
|
|
# Alternate root to test __env__ choices
|
|
"prod": [
|
|
str(prod_env_state_tree_root_dir),
|
|
os.path.join(RUNTIME_VARS.FILES, "file", "prod"),
|
|
],
|
|
},
|
|
"pillar_roots": {
|
|
"base": [
|
|
str(base_env_pillar_tree_root_dir),
|
|
os.path.join(RUNTIME_VARS.FILES, "pillar", "base"),
|
|
],
|
|
"prod": [str(prod_env_pillar_tree_root_dir)],
|
|
},
|
|
}
|
|
)
|
|
|
|
# Let's copy over the test cloud config files and directories into the running master config directory
|
|
for entry in os.listdir(RUNTIME_VARS.CONF_DIR):
|
|
if not entry.startswith("cloud"):
|
|
continue
|
|
source = os.path.join(RUNTIME_VARS.CONF_DIR, entry)
|
|
dest = str(conf_dir / entry)
|
|
if os.path.isdir(source):
|
|
shutil.copytree(source, dest)
|
|
else:
|
|
shutil.copyfile(source, dest)
|
|
|
|
factory = salt_syndic_master_factory.salt_master_daemon(
|
|
"master",
|
|
defaults=config_defaults,
|
|
overrides=config_overrides,
|
|
extra_cli_arguments_after_first_start_failure=["--log-level=info"],
|
|
)
|
|
return factory
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def salt_minion_factory(salt_master_factory):
|
|
with salt.utils.files.fopen(os.path.join(RUNTIME_VARS.CONF_DIR, "minion")) as rfh:
|
|
config_defaults = yaml.deserialize(rfh.read())
|
|
config_defaults["hosts.file"] = os.path.join(RUNTIME_VARS.TMP, "hosts")
|
|
config_defaults["aliases.file"] = os.path.join(RUNTIME_VARS.TMP, "aliases")
|
|
config_defaults["transport"] = salt_master_factory.config["transport"]
|
|
|
|
config_overrides = {
|
|
"log_level_logfile": "quiet",
|
|
"file_roots": salt_master_factory.config["file_roots"].copy(),
|
|
"pillar_roots": salt_master_factory.config["pillar_roots"].copy(),
|
|
"fips_mode": FIPS_TESTRUN,
|
|
"encryption_algorithm": "OAEP-SHA224" if FIPS_TESTRUN else "OAEP-SHA1",
|
|
"signing_algorithm": "PKCS1v15-SHA224" if FIPS_TESTRUN else "PKCS1v15-SHA1",
|
|
}
|
|
|
|
virtualenv_binary = get_virtualenv_binary_path()
|
|
if virtualenv_binary:
|
|
config_overrides["venv_bin"] = virtualenv_binary
|
|
factory = salt_master_factory.salt_minion_daemon(
|
|
"minion",
|
|
defaults=config_defaults,
|
|
overrides=config_overrides,
|
|
extra_cli_arguments_after_first_start_failure=["--log-level=info"],
|
|
)
|
|
factory.after_terminate(
|
|
pytest.helpers.remove_stale_minion_key, salt_master_factory, factory.id
|
|
)
|
|
return factory
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def salt_sub_minion_factory(salt_master_factory):
|
|
with salt.utils.files.fopen(
|
|
os.path.join(RUNTIME_VARS.CONF_DIR, "sub_minion")
|
|
) as rfh:
|
|
config_defaults = yaml.deserialize(rfh.read())
|
|
config_defaults["hosts.file"] = os.path.join(RUNTIME_VARS.TMP, "hosts")
|
|
config_defaults["aliases.file"] = os.path.join(RUNTIME_VARS.TMP, "aliases")
|
|
config_defaults["transport"] = salt_master_factory.config["transport"]
|
|
|
|
config_overrides = {
|
|
"log_level_logfile": "quiet",
|
|
"file_roots": salt_master_factory.config["file_roots"].copy(),
|
|
"pillar_roots": salt_master_factory.config["pillar_roots"].copy(),
|
|
"fips_mode": FIPS_TESTRUN,
|
|
"encryption_algorithm": "OAEP-SHA224" if FIPS_TESTRUN else "OAEP-SHA1",
|
|
"signing_algorithm": "PKCS1v15-SHA224" if FIPS_TESTRUN else "PKCS1v15-SHA1",
|
|
}
|
|
|
|
virtualenv_binary = get_virtualenv_binary_path()
|
|
if virtualenv_binary:
|
|
config_overrides["venv_bin"] = virtualenv_binary
|
|
factory = salt_master_factory.salt_minion_daemon(
|
|
"sub_minion",
|
|
defaults=config_defaults,
|
|
overrides=config_overrides,
|
|
extra_cli_arguments_after_first_start_failure=["--log-level=info"],
|
|
)
|
|
factory.after_terminate(
|
|
pytest.helpers.remove_stale_minion_key, salt_master_factory, factory.id
|
|
)
|
|
return factory
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def salt_cli(salt_master_factory):
|
|
return salt_master_factory.salt_cli()
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def salt_cp_cli(salt_master_factory):
|
|
return salt_master_factory.salt_cp_cli()
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def salt_key_cli(salt_master_factory):
|
|
return salt_master_factory.salt_key_cli()
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def salt_run_cli(salt_master_factory):
|
|
return salt_master_factory.salt_run_cli()
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def salt_call_cli(salt_minion_factory):
|
|
return salt_minion_factory.salt_call_cli()
|
|
|
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
|
def bridge_pytest_and_runtests(
|
|
salt_factories,
|
|
salt_syndic_master_factory,
|
|
salt_syndic_factory,
|
|
salt_master_factory,
|
|
salt_minion_factory,
|
|
salt_sub_minion_factory,
|
|
sshd_config_dir,
|
|
):
|
|
# Make sure unittest2 uses the pytest generated configuration
|
|
RUNTIME_VARS.RUNTIME_CONFIGS["master"] = freeze(salt_master_factory.config)
|
|
RUNTIME_VARS.RUNTIME_CONFIGS["minion"] = freeze(salt_minion_factory.config)
|
|
RUNTIME_VARS.RUNTIME_CONFIGS["sub_minion"] = freeze(salt_sub_minion_factory.config)
|
|
RUNTIME_VARS.RUNTIME_CONFIGS["syndic_master"] = freeze(
|
|
salt_syndic_master_factory.config
|
|
)
|
|
RUNTIME_VARS.RUNTIME_CONFIGS["syndic"] = freeze(salt_syndic_factory.config)
|
|
RUNTIME_VARS.RUNTIME_CONFIGS["client_config"] = freeze(
|
|
salt.config.client_config(salt_master_factory.config["conf_file"])
|
|
)
|
|
|
|
# Make sure unittest2 classes know their paths
|
|
RUNTIME_VARS.TMP_ROOT_DIR = str(salt_factories.root_dir.resolve())
|
|
RUNTIME_VARS.TMP_CONF_DIR = os.path.dirname(salt_master_factory.config["conf_file"])
|
|
RUNTIME_VARS.TMP_MINION_CONF_DIR = os.path.dirname(
|
|
salt_minion_factory.config["conf_file"]
|
|
)
|
|
RUNTIME_VARS.TMP_SUB_MINION_CONF_DIR = os.path.dirname(
|
|
salt_sub_minion_factory.config["conf_file"]
|
|
)
|
|
RUNTIME_VARS.TMP_SYNDIC_MASTER_CONF_DIR = os.path.dirname(
|
|
salt_syndic_master_factory.config["conf_file"]
|
|
)
|
|
RUNTIME_VARS.TMP_SYNDIC_MINION_CONF_DIR = os.path.dirname(
|
|
salt_syndic_factory.config["conf_file"]
|
|
)
|
|
RUNTIME_VARS.TMP_SSH_CONF_DIR = str(sshd_config_dir)
|
|
with reap_stray_processes():
|
|
yield
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def sshd_config_dir(salt_factories):
|
|
config_dir = salt_factories.get_root_dir_for_daemon("sshd")
|
|
yield config_dir
|
|
shutil.rmtree(str(config_dir), ignore_errors=True)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def sshd_server(salt_factories, sshd_config_dir, salt_master, grains):
|
|
sshd_config_dict = {
|
|
"Protocol": "2",
|
|
# Turn strict modes off so that we can operate in /tmp
|
|
"StrictModes": "no",
|
|
# Logging
|
|
"SyslogFacility": "AUTH",
|
|
"LogLevel": "INFO",
|
|
# Authentication:
|
|
"LoginGraceTime": "120",
|
|
"PermitRootLogin": "without-password",
|
|
"PubkeyAuthentication": "yes",
|
|
# Don't read the user's ~/.rhosts and ~/.shosts files
|
|
"IgnoreRhosts": "yes",
|
|
"HostbasedAuthentication": "no",
|
|
# To enable empty passwords, change to yes (NOT RECOMMENDED)
|
|
"PermitEmptyPasswords": "no",
|
|
# Change to yes to enable challenge-response passwords (beware issues with
|
|
# some PAM modules and threads)
|
|
"ChallengeResponseAuthentication": "no",
|
|
# Change to no to disable tunnelled clear text passwords
|
|
"PasswordAuthentication": "no",
|
|
"X11Forwarding": "no",
|
|
"X11DisplayOffset": "10",
|
|
"PrintMotd": "no",
|
|
"PrintLastLog": "yes",
|
|
"TCPKeepAlive": "yes",
|
|
"AcceptEnv": "LANG LC_*",
|
|
"UsePAM": "yes",
|
|
}
|
|
sftp_server_paths = [
|
|
# Common
|
|
"/usr/lib/openssh/sftp-server",
|
|
# CentOS Stream 9
|
|
"/usr/libexec/openssh/sftp-server",
|
|
# Arch Linux
|
|
"/usr/lib/ssh/sftp-server",
|
|
# Photon OS 5
|
|
"/usr/libexec/sftp-server",
|
|
]
|
|
sftp_server_path = None
|
|
for path in sftp_server_paths:
|
|
if os.path.exists(path):
|
|
sftp_server_path = path
|
|
if sftp_server_path is None:
|
|
pytest.fail(f"Failed to find 'sftp-server'. Searched: {sftp_server_paths}")
|
|
else:
|
|
sshd_config_dict["Subsystem"] = f"sftp {sftp_server_path}"
|
|
factory = salt_factories.get_sshd_daemon(
|
|
sshd_config_dict=sshd_config_dict,
|
|
config_dir=sshd_config_dir,
|
|
)
|
|
with factory.started():
|
|
yield factory
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def known_hosts_file(sshd_server, salt_master, salt_factories):
|
|
with pytest.helpers.temp_file(
|
|
"ssh-known-hosts",
|
|
"\n".join(sshd_server.get_host_keys()),
|
|
salt_factories.tmp_root_dir,
|
|
) as known_hosts_file, pytest.helpers.temp_file(
|
|
"master.d/ssh-known-hosts.conf",
|
|
f"known_hosts_file: {known_hosts_file}",
|
|
salt_master.config_dir,
|
|
):
|
|
yield known_hosts_file
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def salt_ssh_roster_file(sshd_server, salt_master, known_hosts_file):
|
|
roster_contents = """
|
|
localhost:
|
|
host: 127.0.0.1
|
|
port: {}
|
|
user: {}
|
|
mine_functions:
|
|
test.arg: ['itworked']
|
|
""".format(
|
|
sshd_server.listen_port, RUNTIME_VARS.RUNNING_TESTS_USER
|
|
)
|
|
if salt.utils.platform.is_darwin():
|
|
roster_contents += " set_path: $PATH:/usr/local/bin/\n"
|
|
|
|
with pytest.helpers.temp_file(
|
|
"roster", roster_contents, salt_master.config_dir
|
|
) as roster_file:
|
|
yield roster_file
|
|
|
|
|
|
# <---- Salt Factories -----------------------------------------------------------------------------------------------
|
|
|
|
|
|
# ----- From Filenames Test Selection ------------------------------------------------------------------------------->
|
|
def _match_to_test_file(match):
|
|
parts = match.split(".")
|
|
test_module_path = TESTS_DIR.joinpath(*parts)
|
|
if test_module_path.exists():
|
|
return test_module_path
|
|
parts[-1] += ".py"
|
|
return TESTS_DIR.joinpath(*parts).relative_to(CODE_DIR)
|
|
|
|
|
|
def from_filenames_collection_modifyitems(config, items):
|
|
from_filenames = config.getoption("--from-filenames")
|
|
if not from_filenames:
|
|
# Don't do anything
|
|
return
|
|
|
|
terminal_reporter = config.pluginmanager.getplugin("terminalreporter")
|
|
terminal_reporter.ensure_newline()
|
|
terminal_reporter.section(
|
|
"From Filenames(--from-filenames) Test Selection", sep=">"
|
|
)
|
|
errors = []
|
|
test_module_selections = []
|
|
changed_files_selections = []
|
|
from_filenames_paths = set()
|
|
for path in [path.strip() for path in from_filenames.split(",")]:
|
|
# Make sure that, no matter what kind of path we're passed, Windows or Posix path,
|
|
# we resolve it to the platform slash separator
|
|
properly_slashed_path = pathlib.Path(
|
|
path.replace("\\", os.sep).replace("/", os.sep)
|
|
)
|
|
if not properly_slashed_path.exists():
|
|
errors.append(f"{properly_slashed_path}: Does not exist")
|
|
continue
|
|
if (
|
|
properly_slashed_path.name == "testrun-changed-files.txt"
|
|
or properly_slashed_path.is_absolute()
|
|
):
|
|
# In this case, this path is considered to be a file containing a line separated list
|
|
# of files to consider
|
|
contents = properly_slashed_path.read_text(encoding="utf-8")
|
|
for sep in ("\r\n", "\\r\\n", "\\n"):
|
|
contents = contents.replace(sep, "\n")
|
|
for line in contents.split("\n"):
|
|
line_path = pathlib.Path(
|
|
line.strip().replace("\\", os.sep).replace("/", os.sep)
|
|
)
|
|
if not line_path.exists():
|
|
errors.append(
|
|
"{}: Does not exist. Source {}".format(
|
|
line_path, properly_slashed_path
|
|
)
|
|
)
|
|
continue
|
|
changed_files_selections.append(
|
|
f"{line_path}: Source {properly_slashed_path}"
|
|
)
|
|
from_filenames_paths.add(line_path)
|
|
continue
|
|
changed_files_selections.append(
|
|
f"{properly_slashed_path}: Source --from-filenames"
|
|
)
|
|
from_filenames_paths.add(properly_slashed_path)
|
|
|
|
# Let's start collecting test modules
|
|
test_module_paths = set()
|
|
|
|
filename_map = yaml.deserialize((TESTS_DIR / "filename_map.yml").read_text())
|
|
# Let's add the match all rule
|
|
for rule, matches in filename_map.items():
|
|
if rule == "*":
|
|
for match in matches:
|
|
test_module_paths.add(_match_to_test_file(match))
|
|
break
|
|
|
|
# Let's now go through the list of files gathered
|
|
for path in from_filenames_paths:
|
|
if path.as_posix().startswith("tests/"):
|
|
if path.name == "conftest.py":
|
|
# This is not a test module, but consider any test_*.py files in child directories
|
|
for match in path.parent.rglob("test_*.py"):
|
|
test_module_selections.append(
|
|
"{}: Source '{}/test_*.py' recursive glob match because '{}' was modified".format(
|
|
match, path.parent, path
|
|
)
|
|
)
|
|
test_module_paths.add(match)
|
|
continue
|
|
# Tests in the listing don't require additional matching and will be added to the
|
|
# list of tests to run
|
|
test_module_selections.append(f"{path}: Source --from-filenames")
|
|
test_module_paths.add(path)
|
|
continue
|
|
if path.name == "setup.py" or path.as_posix().startswith("salt/"):
|
|
if path.name == "__init__.py":
|
|
# No direct matching
|
|
continue
|
|
|
|
# Let's try a direct match between the passed file and possible test modules
|
|
glob_patterns = (
|
|
# salt/version.py ->
|
|
# tests/unit/test_version.py
|
|
# tests/pytests/unit/test_version.py
|
|
f"**/test_{path.name}",
|
|
# salt/modules/grains.py ->
|
|
# tests/pytests/integration/modules/grains/tests_*.py
|
|
# salt/modules/saltutil.py ->
|
|
# tests/pytests/integration/modules/saltutil/test_*.py
|
|
f"**/{path.stem}/test_*.py",
|
|
# salt/modules/config.py ->
|
|
# tests/unit/modules/test_config.py
|
|
# tests/integration/modules/test_config.py
|
|
# tests/pytests/unit/modules/test_config.py
|
|
# tests/pytests/integration/modules/test_config.py
|
|
f"**/{path.parent.name}/test_{path.name}",
|
|
)
|
|
for pattern in glob_patterns:
|
|
for match in TESTS_DIR.rglob(pattern):
|
|
relative_path = match.relative_to(CODE_DIR)
|
|
test_module_selections.append(
|
|
"{}: Source '{}' glob pattern match".format(
|
|
relative_path, pattern
|
|
)
|
|
)
|
|
test_module_paths.add(relative_path)
|
|
|
|
# Do we have an entry in tests/filename_map.yml
|
|
for rule, matches in filename_map.items():
|
|
if rule == "*":
|
|
continue
|
|
elif "|" in rule:
|
|
# This is regex
|
|
if re.match(rule, path.as_posix()):
|
|
for match in matches:
|
|
test_module_paths.add(_match_to_test_file(match))
|
|
test_module_selections.append(
|
|
"{}: Source '{}' regex match from 'tests/filename_map.yml'".format(
|
|
match, rule
|
|
)
|
|
)
|
|
elif "*" in rule or "\\" in rule:
|
|
# Glob matching
|
|
for filerule in CODE_DIR.glob(rule):
|
|
if not filerule.exists():
|
|
continue
|
|
filerule = filerule.relative_to(CODE_DIR)
|
|
if filerule != path:
|
|
continue
|
|
for match in matches:
|
|
match_path = _match_to_test_file(match)
|
|
test_module_selections.append(
|
|
"{}: Source '{}' file rule from 'tests/filename_map.yml'".format(
|
|
match_path, filerule
|
|
)
|
|
)
|
|
test_module_paths.add(match_path)
|
|
else:
|
|
if path.as_posix() != rule:
|
|
continue
|
|
# Direct file paths as rules
|
|
filerule = pathlib.Path(rule)
|
|
if not filerule.exists():
|
|
continue
|
|
for match in matches:
|
|
match_path = _match_to_test_file(match)
|
|
test_module_selections.append(
|
|
"{}: Source '{}' direct file rule from 'tests/filename_map.yml'".format(
|
|
match_path, filerule
|
|
)
|
|
)
|
|
test_module_paths.add(match_path)
|
|
continue
|
|
else:
|
|
errors.append(f"{path}: Don't know what to do with this path")
|
|
|
|
if errors:
|
|
terminal_reporter.write("Errors:\n", bold=True)
|
|
for error in errors:
|
|
terminal_reporter.write(f" * {error}\n")
|
|
if changed_files_selections:
|
|
terminal_reporter.write("Changed files collected:\n", bold=True)
|
|
for selection in changed_files_selections:
|
|
terminal_reporter.write(f" * {selection}\n")
|
|
if test_module_selections:
|
|
terminal_reporter.write("Selected test modules:\n", bold=True)
|
|
for selection in test_module_selections:
|
|
terminal_reporter.write(f" * {selection}\n")
|
|
terminal_reporter.section(
|
|
"From Filenames(--from-filenames) Test Selection", sep="<"
|
|
)
|
|
terminal_reporter.ensure_newline()
|
|
|
|
selected = []
|
|
deselected = []
|
|
for item in items:
|
|
itempath = pathlib.Path(str(item.fspath)).resolve().relative_to(CODE_DIR)
|
|
if itempath in test_module_paths:
|
|
selected.append(item)
|
|
else:
|
|
deselected.append(item)
|
|
|
|
items[:] = selected
|
|
if deselected:
|
|
config.hook.pytest_deselected(items=deselected)
|
|
|
|
|
|
# <---- From Filenames Test Selection --------------------------------------------------------------------------------
|
|
|
|
|
|
# ----- Custom Fixtures --------------------------------------------------------------------------------------------->
|
|
@pytest.fixture(scope="session")
|
|
def sminion():
|
|
return create_sminion()
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def grains(sminion):
|
|
return sminion.opts["grains"].copy()
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def _grains_for_marker():
|
|
return create_sminion().opts["grains"]
|
|
|
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
|
def _disable_salt_logging():
|
|
# This fixture is used to set logging to a configuration that salt expects,
|
|
# however, no logging is actually configured since pytest's logging will be
|
|
# logging what we need.
|
|
logging_config = {
|
|
# Undocumented, on purpose, at least for now, options.
|
|
"configure_ext_handlers": False,
|
|
"configure_file_logger": False,
|
|
"configure_console_logger": False,
|
|
"configure_granular_levels": False,
|
|
}
|
|
salt._logging.set_logging_options_dict(logging_config)
|
|
# Run the test suite
|
|
yield
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def salt_api_account_factory():
|
|
return TestAccount(username="saltdev_api", password="saltdev")
|
|
|
|
|
|
# <---- Custom Fixtures ----------------------------------------------------------------------------------------------
|