mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00

Updates salt_factories_default_root_dir to use $RUNNER_TEMP if running in CI and using Windows. This is setup by the Github Runner and should be on the same disk as the checked out repo. Without doing that, the salt_factories_default_root_dir will be in the user's temp dir which is on a different disk. This causes the fileserver to throw an error as it tries to combine file_roots from the repo and pytest-salt-factories.
1707 lines
63 KiB
Python
1707 lines
63 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():
|
|
if sys.version_info >= (3, 8):
|
|
shutil.copytree(str(child), str(destpath), dirs_exist_ok=True)
|
|
else:
|
|
if destpath.exists():
|
|
shutil.rmtree(str(destpath), ignore_errors=True)
|
|
shutil.copytree(str(child), str(destpath))
|
|
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 ----------------------------------------------------------------------------------------------
|