From bdb44db4c01a6c5e709ba5e012ba6c460a54d43d Mon Sep 17 00:00:00 2001 From: Marcel Telka <marcel@telka.sk> Date: Sun, 24 Mar 2024 21:54:14 +0100 Subject: [PATCH] python/pytest-salt-factories: update to 1.0.1 --- /dev/null | 70 ----------------------- components/python/pytest-salt-factories/manifests/sample-manifest.p5m | 16 +++- components/python/pytest-salt-factories/pkg5 | 10 ++- components/python/pytest-salt-factories/python-integrate-project.conf | 4 + components/python/pytest-salt-factories/pytest-salt-factories-PYVER.p5m | 14 +++- components/python/pytest-salt-factories/Makefile | 13 ++-- 6 files changed, 37 insertions(+), 90 deletions(-) diff --git a/components/python/pytest-salt-factories/Makefile b/components/python/pytest-salt-factories/Makefile index 6586af7..59cf8f7 100644 --- a/components/python/pytest-salt-factories/Makefile +++ b/components/python/pytest-salt-factories/Makefile @@ -19,17 +19,16 @@ include ../../../make-rules/shared-macros.mk COMPONENT_NAME = pytest-salt-factories -HUMAN_VERSION = 0.912.2 +HUMAN_VERSION = 1.0.1 COMPONENT_SUMMARY = pytest-salt-factories - Pytest Salt Plugin COMPONENT_PROJECT_URL = https://github.com/saltstack/pytest-salt-factories -COMPONENT_ARCHIVE_URL = \ - https://files.pythonhosted.org/packages/67/98/87d7a4f2a14681ee10e7032c8be0871e966037d650a4c8dfd7b6b75a0d40/pytest-salt-factories-0.912.2.tar.gz COMPONENT_ARCHIVE_HASH = \ - sha256:214480495fd20b239360703a608b7d81e9396fba2d71cc34080ab844989082b6 + sha256:daf267a4810d01c91f5e9ae09cd084c4732d9c8a0cad7e207264a2b78c3199c5 COMPONENT_LICENSE = Apache-2.0 COMPONENT_LICENSE_FILE = LICENSE -TEST_STYLE = setup.py +# https://github.com/saltstack/pytest-system-statistics/issues/4 +TEST_STYLE = none include $(WS_MAKE_RULES)/common.mk @@ -39,8 +38,10 @@ PYTHON_REQUIRED_PACKAGES += library/python/psutil PYTHON_REQUIRED_PACKAGES += library/python/pytest PYTHON_REQUIRED_PACKAGES += library/python/pytest-helpers-namespace +PYTHON_REQUIRED_PACKAGES += library/python/pytest-shell-utilities PYTHON_REQUIRED_PACKAGES += library/python/pytest-skip-markers -PYTHON_REQUIRED_PACKAGES += library/python/pytest-tempdir +PYTHON_REQUIRED_PACKAGES += library/python/pytest-system-statistics +PYTHON_REQUIRED_PACKAGES += library/python/pyyaml PYTHON_REQUIRED_PACKAGES += library/python/pyzmq PYTHON_REQUIRED_PACKAGES += library/python/setuptools PYTHON_REQUIRED_PACKAGES += library/python/setuptools-declarative-requirements diff --git a/components/python/pytest-salt-factories/manifests/sample-manifest.p5m b/components/python/pytest-salt-factories/manifests/sample-manifest.p5m index 72904a8..5093634 100644 --- a/components/python/pytest-salt-factories/manifests/sample-manifest.p5m +++ b/components/python/pytest-salt-factories/manifests/sample-manifest.p5m @@ -10,7 +10,7 @@ # # -# Copyright 2023 <contributor> +# Copyright 2024 <contributor> # set name=pkg.fmri value=pkg:/$(COMPONENT_FMRI)-$(PYV)@$(IPS_COMPONENT_VERSION),$(BUILD_VERSION) @@ -59,18 +59,22 @@ file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/plugins/log_server.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/plugins/markers.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/plugins/sysinfo.py -file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/plugins/sysstats.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/__init__.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/cli_scripts.py +file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/coverage/sitecustomize.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/functional.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/loader.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/markers.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/ports.py +file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/ports.pyi file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/processes.py +file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/processes.pyi file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/saltext/__init__.py -file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/socket.py +file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/saltext/engines/__init__.py +file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/saltext/engines/pytest_engine.py +file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/saltext/log_handlers/__init__.py +file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/saltext/log_handlers/pytest_log_handler.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/tempfiles.py -file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/time.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/virtualenv.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/version.py @@ -84,7 +88,9 @@ depend type=require fmri=pkg:/library/python/psutil-$(PYV) depend type=require fmri=pkg:/library/python/pytest-$(PYV) depend type=require fmri=pkg:/library/python/pytest-helpers-namespace-$(PYV) +depend type=require fmri=pkg:/library/python/pytest-shell-utilities-$(PYV) depend type=require fmri=pkg:/library/python/pytest-skip-markers-$(PYV) -depend type=require fmri=pkg:/library/python/pytest-tempdir-$(PYV) +depend type=require fmri=pkg:/library/python/pytest-system-statistics-$(PYV) +depend type=require fmri=pkg:/library/python/pyyaml-$(PYV) depend type=require fmri=pkg:/library/python/pyzmq-$(PYV) depend type=require fmri=pkg:/library/python/virtualenv-$(PYV) diff --git a/components/python/pytest-salt-factories/patches/01-sdist-incomplete.patch b/components/python/pytest-salt-factories/patches/01-sdist-incomplete.patch deleted file mode 100644 index b79d198..0000000 --- a/components/python/pytest-salt-factories/patches/01-sdist-incomplete.patch +++ /dev/null @@ -1,9088 +0,0 @@ -https://github.com/saltstack/pytest-salt-factories/issues/158 - ---- pytest-salt-factories-0.912.2.orig/requirements/base.txt -+++ pytest-salt-factories-0.912.2/requirements/base.txt -@@ -0,0 +1,9 @@ -+pytest>=6.0.0 -+attrs>=19.2.0 -+pytest-tempdir>=2019.9.16 -+pytest-helpers-namespace>=2021.4.29 -+pytest-skip-markers>=1.1.2 -+psutil -+pyzmq -+msgpack -+virtualenv ---- pytest-salt-factories-0.912.2.orig/requirements/build.txt -+++ pytest-salt-factories-0.912.2/requirements/build.txt -@@ -0,0 +1,2 @@ -+twine -+build>=0.7.0 ---- pytest-salt-factories-0.912.2.orig/requirements/changelog.txt -+++ pytest-salt-factories-0.912.2/requirements/changelog.txt -@@ -0,0 +1 @@ -+towncrier==21.9.0rc1 ---- pytest-salt-factories-0.912.2.orig/requirements/docs.txt -+++ pytest-salt-factories-0.912.2/requirements/docs.txt -@@ -0,0 +1,9 @@ -+-r base.txt -+-r tests.txt -+-r changelog.txt -+furo -+sphinx -+sphinx-copybutton -+sphinx-prompt -+sphinxcontrib-spelling -+sphinxcontrib-towncrier >= 0.2.1a0 ---- pytest-salt-factories-0.912.2.orig/requirements/lint.txt -+++ pytest-salt-factories-0.912.2/requirements/lint.txt -@@ -0,0 +1,7 @@ -+-r base.txt -+-r tests.txt -+pylint==2.7.4 -+saltpylint==2020.9.28 -+pyenchant -+black; python_version >= '3.7' -+reorder-python-imports; python_version >= '3.7' ---- pytest-salt-factories-0.912.2.orig/requirements/tests.txt -+++ pytest-salt-factories-0.912.2/requirements/tests.txt -@@ -0,0 +1,5 @@ -+-r base.txt -+docker -+pytest-subtests -+pyfakefs==4.4.0; python_version == '3.5' -+pyfakefs; python_version > '3.5' ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/__init__.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/__init__.py -@@ -0,0 +1,58 @@ -+import pathlib -+import re -+import sys -+ -+try: -+ from .version import __version__ -+except ImportError: # pragma: no cover -+ __version__ = "0.0.0.not-installed" -+ try: -+ from importlib.metadata import version, PackageNotFoundError -+ -+ try: -+ __version__ = version("pytest-salt-factories") -+ except PackageNotFoundError: -+ # package is not installed -+ pass -+ except ImportError: -+ try: -+ from importlib_metadata import version, PackageNotFoundError -+ -+ try: -+ __version__ = version("pytest-salt-factories") -+ except PackageNotFoundError: -+ # package is not installed -+ pass -+ except ImportError: -+ try: -+ from pkg_resources import get_distribution, DistributionNotFound -+ -+ try: -+ __version__ = get_distribution("pytest-salt-factories").version -+ except DistributionNotFound: -+ # package is not installed -+ pass -+ except ImportError: -+ # pkg resources isn't even available?! -+ pass -+ -+ -+# Define __version_info__ attribute -+VERSION_INFO_REGEX = re.compile( -+ r"(?P<major>[\d]+)\.(?P<minor>[\d]+)\.(?P<patch>[\d]+)" -+ r"(?:\.dev(?P<commits>[\d]+)\+g(?P<sha>[a-z0-9]+)\.d(?P<date>[\d]+))?" -+) -+try: -+ __version_info__ = tuple( -+ int(p) if p.isdigit() else p for p in VERSION_INFO_REGEX.match(__version__).groups() if p -+ ) -+except AttributeError: # pragma: no cover -+ __version_info__ = (-1, -1, -1) -+finally: -+ del VERSION_INFO_REGEX -+ -+ -+# Define some constants -+CODE_ROOT_DIR = pathlib.Path(__file__).resolve().parent -+IS_WINDOWS = sys.platform.startswith("win") -+IS_DARWIN = IS_OSX = sys.platform.startswith("darwin") ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/__main__.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/__main__.py -@@ -0,0 +1,27 @@ -+""" -+The ``salt-factories`` CLI script is meant to be used to get an absolute path to the directory containing -+``sitecustomize.py`` so that it can be injected into ``PYTHONPATH`` when running tests to track subprocesses -+code coverage. -+""" -+import argparse -+import sys -+ -+import saltfactories -+ -+ -+def main(): -+ parser = argparse.ArgumentParser(description="PyTest Salt Factories") -+ parser.add_argument( -+ "--coverage", -+ action="store_true", -+ help="Prints the path to where the sitecustomize.py is to trigger coverage tracking on sub-processes.", -+ ) -+ options = parser.parse_args() -+ if options.coverage: -+ print(str(saltfactories.CODE_ROOT_DIR / "utils" / "coverage"), file=sys.stdout, flush=True) -+ parser.exit(status=0) -+ parser.exit(status=1, message=parser.format_usage()) -+ -+ -+if __name__ == "__main__": -+ main() ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/bases.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/bases.py -@@ -0,0 +1,1597 @@ -+""" -+.. -+ PYTEST_DONT_REWRITE -+ -+ -+Factories base classes -+""" -+import atexit -+import contextlib -+import json -+import logging -+import os -+import pprint -+import subprocess -+import sys -+import tempfile -+ -+import attr -+import psutil -+import pytest -+import salt.utils.files -+import salt.utils.path -+import salt.utils.verify -+import salt.utils.yaml -+from pytestskipmarkers.utils import platform -+from pytestskipmarkers.utils import ports -+from salt.utils.immutabletypes import freeze -+ -+from saltfactories.exceptions import FactoryNotRunning -+from saltfactories.exceptions import FactoryNotStarted -+from saltfactories.exceptions import FactoryTimeout -+from saltfactories.utils import format_callback_to_string -+from saltfactories.utils import running_username -+from saltfactories.utils import time -+from saltfactories.utils.processes import ProcessResult -+from saltfactories.utils.processes import ShellResult -+from saltfactories.utils.processes import terminate_process -+from saltfactories.utils.processes import terminate_process_list -+ -+ -+log = logging.getLogger(__name__) -+ -+ -+@attr.s(kw_only=True) -+class Factory: -+ """ -+ Base factory class -+ -+ :keyword str display_name: -+ Human readable name for the factory -+ :keyword dict environ: -+ A dictionary of ``key``, ``value`` pairs to add to the environment. -+ :keyword str cwd: -+ The path to the current working directory -+ """ -+ -+ display_name = attr.ib(default=None) -+ cwd = attr.ib(default=None) -+ environ = attr.ib(repr=False, default=None) -+ -+ def __attrs_post_init__(self): -+ if self.environ is None: -+ self.environ = os.environ.copy() -+ if self.cwd is None: -+ self.cwd = os.getcwd() -+ -+ def get_display_name(self): -+ """ -+ Returns a human readable name for the factory -+ """ -+ if self.display_name: -+ return "{}({})".format(self.__class__.__name__, self.display_name) -+ return self.__class__.__name__ -+ -+ -+@attr.s(kw_only=True) -+class SubprocessImpl: -+ """ -+ Subprocess interaction implementation -+ -+ :keyword ~saltfactories.bases.Factory factory: -+ The factory instance, either :py:class:`~saltfactories.bases.Factory` or -+ a sub-class of it. -+ """ -+ -+ factory = attr.ib() -+ -+ _terminal = attr.ib(repr=False, init=False, default=None) -+ _terminal_stdout = attr.ib(repr=False, init=False, default=None) -+ _terminal_stderr = attr.ib(repr=False, init=False, default=None) -+ _terminal_result = attr.ib(repr=False, init=False, default=None) -+ _terminal_timeout = attr.ib(repr=False, init=False, default=None) -+ _children = attr.ib(repr=False, init=False, default=attr.Factory(list)) -+ -+ def cmdline(self, *args): -+ """ -+ Construct a list of arguments to use when starting the subprocess -+ -+ :param str args: -+ Additional arguments to use when starting the subprocess -+ -+ By default, this method will just call it's factory's ``cmdline()`` -+ method, but can be overridden, as is the case of -+ :py:class:`saltfactories.base.SystemdSaltDaemonImpl`. -+ """ -+ return self.factory.cmdline(*args) -+ -+ def init_terminal(self, cmdline, env=None): -+ """ -+ Instantiate a terminal with the passed command line(``cmdline``) and return it. -+ -+ Additionally, it sets a reference to it in ``self._terminal`` and also collects -+ an initial listing of child processes which will be used when terminating the -+ terminal -+ -+ :param list,tuple cmdline: -+ List of strings to pass as ``args`` to :py:class:`~subprocess.Popen` -+ :keyword dict environ: -+ A dictionary of ``key``, ``value`` pairs to add to the -+ py:attr:`saltfactories.base.Factory.environ`. -+ """ -+ environ = self.factory.environ.copy() -+ if env is not None: -+ environ.update(env) -+ self._terminal_stdout = tempfile.SpooledTemporaryFile(512000, buffering=0) -+ self._terminal_stderr = tempfile.SpooledTemporaryFile(512000, buffering=0) -+ if platform.is_windows(): -+ # Windows does not support closing FDs -+ close_fds = False -+ elif platform.is_freebsd() and sys.version_info < (3, 9): -+ # Closing FDs in FreeBSD before Py3.9 can be slow -+ # https://bugs.python.org/issue38061 -+ close_fds = False -+ else: -+ close_fds = True -+ self._terminal = subprocess.Popen( -+ cmdline, -+ stdout=self._terminal_stdout, -+ stderr=self._terminal_stderr, -+ shell=False, -+ cwd=self.factory.cwd, -+ universal_newlines=True, -+ close_fds=close_fds, -+ env=environ, -+ bufsize=0, -+ ) -+ # Reset the previous _terminal_result if set -+ self._terminal_result = None -+ try: -+ # Check if the process starts properly -+ self._terminal.wait(timeout=0.05) -+ # If TimeoutExpired is not raised, it means the process failed to start -+ except subprocess.TimeoutExpired: -+ # We're good -+ # Collect any child processes, though, this early there likely is none -+ with contextlib.suppress(psutil.NoSuchProcess): -+ for child in psutil.Process(self._terminal.pid).children( -+ recursive=True -+ ): # pragma: no cover -+ if child not in self._children: -+ self._children.append(child) -+ atexit.register(self.terminate) -+ return self._terminal -+ -+ def is_running(self): -+ """ -+ :return: Returns true if the sub-process is alive -+ :rtype: bool -+ """ -+ if not self._terminal: -+ return False -+ return self._terminal.poll() is None -+ -+ def terminate(self): -+ """ -+ Terminate the started daemon -+ -+ :rtype: ~saltfactories.utils.processes.ProcessResult -+ """ -+ return self._terminate() -+ -+ def _terminate(self): -+ """ -+ This method actually terminates the started daemon -+ """ -+ if self._terminal is None: -+ return self._terminal_result -+ atexit.unregister(self.terminate) -+ log.info("Stopping %s", self.factory) -+ # Collect any child processes information before terminating the process -+ with contextlib.suppress(psutil.NoSuchProcess): -+ for child in psutil.Process(self._terminal.pid).children(recursive=True): -+ if child not in self._children: -+ self._children.append(child) -+ -+ with self._terminal: -+ if self.factory.slow_stop: -+ self._terminal.terminate() -+ else: -+ self._terminal.kill() -+ try: -+ # Allow the process to exit by itself in case slow_stop is True -+ self._terminal.wait(10) -+ except subprocess.TimeoutExpired: # pragma: no cover -+ # The process failed to stop, no worries, we'll make sure it exit along with it's -+ # child processes bellow -+ pass -+ # Lets log and kill any child processes left behind, including the main subprocess -+ # if it failed to properly stop -+ terminate_process( -+ pid=self._terminal.pid, -+ kill_children=True, -+ children=self._children, -+ slow_stop=self.factory.slow_stop, -+ ) -+ # Wait for the process to terminate, to avoid zombies. -+ self._terminal.wait() -+ # poll the terminal so the right returncode is set on the popen object -+ self._terminal.poll() -+ # This call shouldn't really be necessary -+ self._terminal.communicate() -+ -+ self._terminal_stdout.flush() -+ self._terminal_stdout.seek(0) -+ if sys.version_info < (3, 6): # pragma: no cover -+ stdout = self._terminal._translate_newlines( -+ self._terminal_stdout.read(), __salt_system_encoding__ -+ ) -+ else: -+ stdout = self._terminal._translate_newlines( -+ self._terminal_stdout.read(), __salt_system_encoding__, sys.stdout.errors -+ ) -+ self._terminal_stdout.close() -+ -+ self._terminal_stderr.flush() -+ self._terminal_stderr.seek(0) -+ if sys.version_info < (3, 6): # pragma: no cover -+ stderr = self._terminal._translate_newlines( -+ self._terminal_stderr.read(), __salt_system_encoding__ -+ ) -+ else: -+ stderr = self._terminal._translate_newlines( -+ self._terminal_stderr.read(), __salt_system_encoding__, sys.stderr.errors -+ ) -+ self._terminal_stderr.close() -+ try: -+ self._terminal_result = ProcessResult( -+ self._terminal.returncode, stdout, stderr, cmdline=self._terminal.args -+ ) -+ log.info("%s %s", self.factory.__class__.__name__, self._terminal_result) -+ return self._terminal_result -+ finally: -+ self._terminal = None -+ self._terminal_stdout = None -+ self._terminal_stderr = None -+ self._terminal_timeout = None -+ self._children = [] -+ -+ @property -+ def pid(self): -+ """ -+ The pid of the running process. None if not running. -+ """ -+ if not self._terminal: # pragma: no cover -+ return -+ return self._terminal.pid -+ -+ def run(self, *args, **kwargs): -+ """ -+ Run the given command synchronously -+ """ -+ cmdline = self.cmdline(*args, **kwargs) -+ log.info("%s is running %r in CWD: %s ...", self.factory, cmdline, self.factory.cwd) -+ return self.init_terminal(cmdline) -+ -+ -+@attr.s(kw_only=True) -+class Subprocess(Factory): -+ """ -+ Base CLI script/binary class -+ -+ :keyword str script_name: -+ This is the string containing the name of the binary to call on the subprocess, either the -+ full path to it, or the basename. In case of the basename, the directory containing the -+ basename must be in your ``$PATH`` variable. -+ :keyword list,tuple base_script_args: -+ An list or tuple iterable of the base arguments to use when building the command line to -+ launch the process -+ :keyword bool slow_stop: -+ Whether to terminate the processes by sending a :py:attr:`SIGTERM` signal or by calling -+ :py:meth:`~subprocess.Popen.terminate` on the sub-process. -+ When code coverage is enabled, one will want `slow_stop` set to `True` so that coverage data -+ can be written down to disk. -+ -+ Please look at :py:class:`~saltfactories.bases.Factory` for the additional supported keyword -+ arguments documentation. -+ """ -+ -+ script_name = attr.ib() -+ base_script_args = attr.ib(default=attr.Factory(list)) -+ slow_stop = attr.ib(default=True) -+ -+ impl = attr.ib(repr=False, init=False) -+ -+ @impl.default -+ def _set_impl_default(self): -+ impl_class = self._get_impl_class() -+ return impl_class(factory=self) -+ -+ def _get_impl_class(self): -+ return SubprocessImpl -+ -+ def get_display_name(self): -+ """ -+ Returns a human readable name for the factory -+ """ -+ return self.display_name or self.script_name -+ -+ def get_script_path(self): -+ """ -+ Returns the path to the script to run -+ """ -+ if os.path.isabs(self.script_name): -+ script_path = self.script_name -+ else: -+ script_path = salt.utils.path.which(self.script_name) -+ if not script_path or not os.path.exists(script_path): -+ pytest.fail("The CLI script {!r} does not exist".format(script_path)) -+ return script_path -+ -+ def get_base_script_args(self): -+ """ -+ Returns any additional arguments to pass to the CLI script -+ """ -+ return list(self.base_script_args) -+ -+ def get_script_args(self): # pylint: disable=no-self-use -+ """ -+ Returns any additional arguments to pass to the CLI script -+ """ -+ return [] -+ -+ def cmdline(self, *args): -+ """ -+ Construct a list of arguments to use when starting the subprocess -+ -+ :param str args: -+ Additional arguments to use when starting the subprocess -+ -+ :rtype: list -+ """ -+ return ( -+ [self.get_script_path()] -+ + self.get_base_script_args() -+ + self.get_script_args() -+ + list(args) -+ ) -+ -+ def is_running(self): -+ """ -+ Returns true if the sub-process is alive -+ """ -+ return self.impl.is_running() -+ -+ def terminate(self): -+ """ -+ Terminate the started daemon -+ """ -+ return self.impl.terminate() -+ -+ @property -+ def pid(self): -+ """ -+ The pid of the running process. None if not running. -+ """ -+ return self.impl.pid -+ -+ -+@attr.s(kw_only=True) -+class Process(Subprocess): -+ """ -+ Base process factory -+ -+ :keyword int timeout: -+ The default maximum amount of seconds that a script should run. -+ This value can be overridden when calling :py:meth:`~saltfactories.bases.Process.run` through -+ the ``_timeout`` keyword argument, and, in that case, the timeout value applied would be that -+ of ``_timeout`` instead of ``self.timeout``. -+ -+ Please look at :py:class:`~saltfactories.bases.Subprocess` for the additional supported keyword -+ arguments documentation. -+ """ -+ -+ timeout = attr.ib() -+ -+ @timeout.default -+ def _set_timeout(self): -+ if not sys.platform.startswith(("win", "darwin")): -+ return 60 -+ # Windows and macOS are just slower. -+ return 120 -+ -+ def run(self, *args, _timeout=None, **kwargs): -+ """ -+ Run the given command synchronously -+ -+ :keyword list,tuple args: -+ The list of arguments to pass to :py:meth:`~saltfactories.bases.Process.cmdline` -+ to construct the command to run -+ :keyword int _timeout: -+ The timeout value for this particular ``run()`` call. If this value is not ``None``, -+ it will be used instead of :py:attr:`~saltfactories.bases.Process.timeout`, -+ the default timeout. -+ """ -+ start_time = time.time() -+ # Build the cmdline to pass to the terminal -+ # We set the _terminal_timeout attribute while calling cmdline in case it needs -+ # access to that information to build the command line -+ self.impl._terminal_timeout = _timeout or self.timeout -+ timmed_out = False -+ try: -+ self.impl.run(*args, **kwargs) -+ self.impl._terminal.communicate(timeout=self.impl._terminal_timeout) -+ except subprocess.TimeoutExpired: -+ timmed_out = True -+ -+ result = self.terminate() -+ cmdline = result.cmdline -+ exitcode = result.exitcode -+ if timmed_out: -+ raise FactoryTimeout( -+ "{} Failed to run: {}; Error: Timed out after {:.2f} seconds!".format( -+ self, cmdline, time.time() - start_time -+ ), -+ stdout=result.stdout, -+ stderr=result.stderr, -+ cmdline=cmdline, -+ exitcode=exitcode, -+ ) -+ stdout, stderr, json_out = self.process_output( -+ result.stdout, result.stderr, cmdline=cmdline -+ ) -+ log.info( -+ "%s completed %r in CWD: %s after %.2f seconds", -+ self, -+ cmdline, -+ self.cwd, -+ time.time() - start_time, -+ ) -+ return ShellResult(exitcode, stdout, stderr, json=json_out, cmdline=cmdline) -+ -+ def process_output(self, stdout, stderr, cmdline=None): -+ """ -+ Process the output. When possible JSON is loaded from the output. -+ -+ :return: -+ Returns a tuple in the form of ``(stdout, stderr, loaded_json)`` -+ :rtype: tuple -+ """ -+ if stdout: -+ try: -+ json_out = json.loads(stdout) -+ except ValueError: -+ log.debug("%s failed to load JSON from the following output:\n%r", self, stdout) -+ json_out = None -+ else: -+ json_out = None -+ return stdout, stderr, json_out -+ -+ -+@attr.s(kw_only=True, slots=True, frozen=True) -+class StartDaemonCallArguments: -+ """ -+ This class holds the arguments and keyword arguments used to start a daemon. -+ -+ It's used when restarting the daemon so that the same call is used. -+ -+ :keyword list,tuple args: -+ List of arguments -+ :keyword dict kwargs: -+ Dictionary of keyword arguments -+ """ -+ -+ args = attr.ib() -+ kwargs = attr.ib() -+ -+ -+@attr.s(kw_only=True) -+class DaemonImpl(SubprocessImpl): -+ """ -+ Daemon subprocess interaction implementation -+ -+ Please look at :py:class:`~saltfactories.bases.SubprocessImpl` for the additional supported keyword -+ arguments documentation. -+ """ -+ -+ _before_start_callbacks = attr.ib(repr=False, hash=False, default=attr.Factory(list)) -+ _after_start_callbacks = attr.ib(repr=False, hash=False, default=attr.Factory(list)) -+ _before_terminate_callbacks = attr.ib(repr=False, hash=False, default=attr.Factory(list)) -+ _after_terminate_callbacks = attr.ib(repr=False, hash=False, default=attr.Factory(list)) -+ _start_args_and_kwargs = attr.ib(init=False, repr=False, hash=False) -+ -+ def before_start(self, callback, *args, **kwargs): -+ """ -+ Register a function callback to run before the daemon starts -+ -+ :param ~collections.abc.Callable callback: -+ The function to call back -+ :keyword args: -+ The arguments to pass to the callback -+ :keyword kwargs: -+ The keyword arguments to pass to the callback -+ """ -+ self._before_start_callbacks.append((callback, args, kwargs)) -+ -+ def after_start(self, callback, *args, **kwargs): -+ """ -+ Register a function callback to run after the daemon starts -+ -+ :param ~collections.abc.Callable callback: -+ The function to call back -+ :keyword args: -+ The arguments to pass to the callback -+ :keyword kwargs: -+ The keyword arguments to pass to the callback -+ """ -+ self._after_start_callbacks.append((callback, args, kwargs)) -+ -+ def before_terminate(self, callback, *args, **kwargs): -+ """ -+ Register a function callback to run before the daemon terminates -+ -+ :param ~collections.abc.Callable callback: -+ The function to call back -+ :keyword args: -+ The arguments to pass to the callback -+ :keyword kwargs: -+ The keyword arguments to pass to the callback -+ """ -+ self._before_terminate_callbacks.append((callback, args, kwargs)) -+ -+ def after_terminate(self, callback, *args, **kwargs): -+ """ -+ Register a function callback to run after the daemon terminates -+ -+ :param ~collections.abc.Callable callback: -+ The function to call back -+ :keyword args: -+ The arguments to pass to the callback -+ :keyword kwargs: -+ The keyword arguments to pass to the callback -+ """ -+ self._after_terminate_callbacks.append((callback, args, kwargs)) -+ -+ def start(self, *extra_cli_arguments, max_start_attempts=None, start_timeout=None): -+ """ -+ Start the daemon -+ -+ :keyword tuple, extra_cli_arguments: -+ Extra arguments to pass to the CLI that starts the daemon -+ :keyword int max_start_attempts: -+ Maximum number of attempts to try and start the daemon in case of failures -+ :keyword int start_timeout: -+ The maximum number of seconds to wait before considering that the daemon did not start -+ """ -+ if self.is_running(): # pragma: no cover -+ log.warning("%s is already running.", self) -+ return True -+ self._start_args_and_kwargs = StartDaemonCallArguments( -+ args=extra_cli_arguments, -+ kwargs={"max_start_attempts": max_start_attempts, "start_timeout": start_timeout}, -+ ) -+ process_running = False -+ start_time = time.time() -+ start_attempts = max_start_attempts or self.factory.max_start_attempts -+ current_attempt = 0 -+ run_arguments = list(extra_cli_arguments) -+ while True: -+ if process_running: -+ break -+ current_attempt += 1 -+ if current_attempt > start_attempts: -+ break -+ log.info( -+ "Starting %s. Attempt: %d of %d", self.factory, current_attempt, start_attempts -+ ) -+ for callback, args, kwargs in self._before_start_callbacks: -+ try: -+ callback(*args, **kwargs) -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ log.info( -+ "Exception raised when running %s: %s", -+ format_callback_to_string(callback, args, kwargs), -+ exc, -+ exc_info=True, -+ ) -+ current_start_time = time.time() -+ start_running_timeout = current_start_time + ( -+ start_timeout or self.factory.start_timeout -+ ) -+ if current_attempt > 1 and self.factory.extra_cli_arguments_after_first_start_failure: -+ run_arguments = list(extra_cli_arguments) + list( -+ self.factory.extra_cli_arguments_after_first_start_failure -+ ) -+ self.run(*run_arguments) -+ if not self.is_running(): # pragma: no cover -+ # A little breathe time to allow the process to start if not started already -+ time.sleep(0.5) -+ while time.time() <= start_running_timeout: -+ if not self.is_running(): -+ log.warning("%s is no longer running", self.factory) -+ self.terminate() -+ break -+ try: -+ if ( -+ self.factory.run_start_checks(current_start_time, start_running_timeout) -+ is False -+ ): -+ time.sleep(1) -+ continue -+ except FactoryNotStarted: -+ self.terminate() -+ break -+ log.info( -+ "The %s factory is running after %d attempts. Took %1.2f seconds", -+ self.factory, -+ current_attempt, -+ time.time() - start_time, -+ ) -+ process_running = True -+ break -+ else: -+ # The factory failed to confirm it's running status -+ self.terminate() -+ if process_running: -+ for callback, args, kwargs in self._after_start_callbacks: -+ try: -+ callback(*args, **kwargs) -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ log.info( -+ "Exception raised when running %s: %s", -+ format_callback_to_string(callback, args, kwargs), -+ exc, -+ exc_info=True, -+ ) -+ return process_running -+ result = self.terminate() -+ raise FactoryNotStarted( -+ "The {} factory has failed to confirm running status after {} attempts, which " -+ "took {:.2f} seconds".format( -+ self.factory, -+ current_attempt - 1, -+ time.time() - start_time, -+ ), -+ stdout=result.stdout, -+ stderr=result.stderr, -+ exitcode=result.exitcode, -+ cmdline=result.cmdline, -+ ) -+ -+ def terminate(self): -+ """ -+ Terminate the daemon -+ """ -+ if self._terminal_result is not None: -+ # This factory has already been terminated -+ return self._terminal_result -+ for callback, args, kwargs in self._before_terminate_callbacks: -+ try: -+ callback(*args, **kwargs) -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ log.info( -+ "Exception raised when running %s: %s", -+ format_callback_to_string(callback, args, kwargs), -+ exc, -+ exc_info=True, -+ ) -+ try: -+ return super().terminate() -+ finally: -+ for callback, args, kwargs in self._after_terminate_callbacks: -+ try: -+ callback(*args, **kwargs) -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ log.info( -+ "Exception raised when running %s: %s", -+ format_callback_to_string(callback, args, kwargs), -+ exc, -+ exc_info=True, -+ ) -+ -+ def get_start_arguments(self): -+ """ -+ Return the arguments and keyword arguments used when starting the daemon: -+ -+ :rtype: StartDaemonCallArguments -+ """ -+ return self._start_args_and_kwargs -+ -+ -+@attr.s(kw_only=True) -+class Daemon(Subprocess): -+ """ -+ Base daemon factory -+ -+ :keyword list check_ports: -+ List of ports to try and connect to while confirming that the daemon is up and running -+ :keyword tuple, extra_cli_arguments_after_first_start_failure: -+ Extra arguments to pass to the CLI that starts the daemon after the first failure -+ :keyword int max_start_attempts: -+ Maximum number of attempts to try and start the daemon in case of failures -+ :keyword int start_timeout: -+ The maximum number of seconds to wait before considering that the daemon did not start -+ -+ Please look at :py:class:`~saltfactories.bases.Subprocess` for the additional supported keyword -+ arguments documentation. -+ """ -+ -+ check_ports = attr.ib(default=None) -+ factories_manager = attr.ib(repr=False, hash=False, default=None) -+ start_timeout = attr.ib(repr=False) -+ max_start_attempts = attr.ib(repr=False, default=3) -+ extra_cli_arguments_after_first_start_failure = attr.ib(hash=False, default=attr.Factory(list)) -+ listen_ports = attr.ib(init=False, repr=False, hash=False, default=attr.Factory(list)) -+ -+ def _get_impl_class(self): -+ return DaemonImpl -+ -+ def __attrs_post_init__(self): -+ super().__attrs_post_init__() -+ if self.check_ports and not isinstance(self.check_ports, (list, tuple)): -+ self.check_ports = [self.check_ports] -+ if self.check_ports: -+ self.listen_ports.extend(self.check_ports) -+ -+ self.after_start(self._add_factory_to_stats_processes) -+ self.after_terminate(self._terminate_processes_matching_listen_ports) -+ self.after_terminate(self._remove_factory_from_stats_processes) -+ -+ def before_start(self, callback, *args, **kwargs): -+ """ -+ Register a function callback to run before the daemon starts -+ -+ :param ~collections.abc.Callable callback: -+ The function to call back -+ :keyword args: -+ The arguments to pass to the callback -+ :keyword kwargs: -+ The keyword arguments to pass to the callback -+ """ -+ self.impl._before_start_callbacks.append((callback, args, kwargs)) -+ -+ def after_start(self, callback, *args, **kwargs): -+ """ -+ Register a function callback to run after the daemon starts -+ -+ :param ~collections.abc.Callable callback: -+ The function to call back -+ :keyword args: -+ The arguments to pass to the callback -+ :keyword kwargs: -+ The keyword arguments to pass to the callback -+ """ -+ self.impl._after_start_callbacks.append((callback, args, kwargs)) -+ -+ def before_terminate(self, callback, *args, **kwargs): -+ """ -+ Register a function callback to run before the daemon terminates -+ -+ :param ~collections.abc.Callable callback: -+ The function to call back -+ :keyword args: -+ The arguments to pass to the callback -+ :keyword kwargs: -+ The keyword arguments to pass to the callback -+ """ -+ self.impl._before_terminate_callbacks.append((callback, args, kwargs)) -+ -+ def after_terminate(self, callback, *args, **kwargs): -+ """ -+ Register a function callback to run after the daemon terminates -+ -+ :param ~collections.abc.Callable callback: -+ The function to call back -+ :keyword args: -+ The arguments to pass to the callback -+ :keyword kwargs: -+ The keyword arguments to pass to the callback -+ """ -+ self.impl._after_terminate_callbacks.append((callback, args, kwargs)) -+ -+ def get_check_ports(self): -+ """ -+ Return a list of ports to check against to ensure the daemon is running -+ """ -+ return self.check_ports or [] -+ -+ def start(self, *extra_cli_arguments, max_start_attempts=None, start_timeout=None): -+ """ -+ Start the daemon -+ """ -+ return self.impl.start( -+ *extra_cli_arguments, max_start_attempts=max_start_attempts, start_timeout=start_timeout -+ ) -+ -+ def started(self, *extra_cli_arguments, max_start_attempts=None, start_timeout=None): -+ """ -+ Start the daemon and return it's instance so it can be used as a context manager -+ """ -+ self.start( -+ *extra_cli_arguments, max_start_attempts=max_start_attempts, start_timeout=start_timeout -+ ) -+ return self -+ -+ @contextlib.contextmanager -+ def stopped( -+ self, -+ before_stop_callback=None, -+ after_stop_callback=None, -+ before_start_callback=None, -+ after_start_callback=None, -+ ): -+ """ -+ :keyword ~collections.abc.Callable before_stop_callback: -+ A callable to run before stopping the daemon. The callback must accept one argument, -+ the daemon instance. -+ :keyword ~collections.abc.Callable after_stop_callback: -+ A callable to run after stopping the daemon. The callback must accept one argument, -+ the daemon instance. -+ :keyword ~collections.abc.Callable before_start_callback: -+ A callable to run before starting the daemon. The callback must accept one argument, -+ the daemon instance. -+ :keyword ~collections.abc.Callable after_start_callback: -+ A callable to run after starting the daemon. The callback must accept one argument, -+ the daemon instance. -+ -+ This context manager will stop the factory while the context is in place, it re-starts it once out of -+ context. -+ -+ For example: -+ -+ .. code-block:: python -+ -+ assert factory.is_running() is True -+ -+ with factory.stopped(): -+ assert factory.is_running() is False -+ -+ assert factory.is_running() is True -+ """ -+ if not self.is_running(): -+ raise FactoryNotRunning("{} is not running ".format(self)) -+ start_arguments = self.impl.get_start_arguments() -+ try: -+ if before_stop_callback: -+ try: -+ before_stop_callback(self) -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ log.info( -+ "Exception raised when running %s: %s", -+ format_callback_to_string(before_stop_callback), -+ exc, -+ exc_info=True, -+ ) -+ self.terminate() -+ if after_stop_callback: -+ try: -+ after_stop_callback(self) -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ log.info( -+ "Exception raised when running %s: %s", -+ format_callback_to_string(after_stop_callback), -+ exc, -+ exc_info=True, -+ ) -+ yield -+ except Exception: # pragma: no cover pylint: disable=broad-except,try-except-raise -+ raise -+ else: -+ if before_start_callback: -+ try: -+ before_start_callback(self) -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ log.info( -+ "Exception raised when running %s: %s", -+ format_callback_to_string(before_start_callback), -+ exc, -+ exc_info=True, -+ ) -+ _started = self.started(*start_arguments.args, **start_arguments.kwargs) -+ if _started: -+ if after_start_callback: -+ try: -+ after_start_callback(self) -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ log.info( -+ "Exception raised when running %s: %s", -+ format_callback_to_string(after_start_callback), -+ exc, -+ exc_info=True, -+ ) -+ return _started -+ -+ def run_start_checks(self, started_at, timeout_at): -+ """ -+ Run checks to confirm that the daemon has started -+ """ -+ log.debug("%s is running start checks", self) -+ check_ports = set(self.get_check_ports()) -+ if not check_ports: -+ log.debug("No ports to check connection to for %s", self) -+ return True -+ log.debug("Listening ports to check for %s: %s", self, set(self.get_check_ports())) -+ checks_start_time = time.time() -+ while time.time() <= timeout_at: -+ if not self.is_running(): -+ raise FactoryNotStarted("{} is no longer running".format(self)) -+ if not check_ports: -+ break -+ check_ports -= ports.get_connectable_ports(check_ports) -+ if check_ports: -+ time.sleep(1.5) -+ else: -+ log.error( -+ "Failed to check ports after %1.2f seconds for %s. Remaining ports to check: %s", -+ time.time() - checks_start_time, -+ self, -+ check_ports, -+ ) -+ return False -+ log.debug("All listening ports checked for %s: %s", self, set(self.get_check_ports())) -+ return True -+ -+ def _add_factory_to_stats_processes(self): -+ if self.factories_manager and self.factories_manager.stats_processes is not None: -+ display_name = self.get_display_name() -+ self.factories_manager.stats_processes.add(display_name, self.pid) -+ -+ def _remove_factory_from_stats_processes(self): -+ if self.factories_manager and self.factories_manager.stats_processes is not None: -+ display_name = self.get_display_name() -+ self.factories_manager.stats_processes.remove(display_name) -+ -+ def _terminate_processes_matching_listen_ports(self): -+ if not self.listen_ports: -+ return -+ # If any processes were not terminated and are listening on the ports -+ # we have set on listen_ports, terminate those processes. -+ found_processes = [] -+ for process in psutil.process_iter(["connections"]): -+ try: -+ for connection in process.connections(): -+ if connection.status != psutil.CONN_LISTEN: -+ # We only care about listening services -+ continue -+ if connection.laddr.port in self.check_ports: -+ found_processes.append(process) -+ # We already found one connection, no need to check the others -+ break -+ except psutil.AccessDenied: -+ # We've been denied access to this process connections. Carry on. -+ continue -+ if found_processes: -+ log.debug( -+ "The following processes were found listening on ports %s: %s", -+ ", ".join([str(port) for port in self.listen_ports]), -+ found_processes, -+ ) -+ terminate_process_list(found_processes, kill=True, slow_stop=False) -+ else: -+ log.debug( -+ "No astray processes were found listening on ports: %s", -+ ", ".join([str(port) for port in self.listen_ports]), -+ ) -+ -+ def __enter__(self): -+ if not self.is_running(): -+ raise RuntimeError( -+ "Factory not yet started. Perhaps you're after something like:\n\n" -+ "with {}.started() as factory:\n" -+ " yield factory".format(self.__class__.__name__) -+ ) -+ return self -+ -+ def __exit__(self, *_): -+ self.terminate() -+ -+ -+@attr.s(kw_only=True) -+class Salt: -+ """ -+ Base factory for salt cli's and daemon's -+ -+ :param dict config: -+ The Salt config dictionary -+ :param str python_executable: -+ The path to the python executable to use -+ :param bool system_install: -+ If true, the daemons and CLI's are run against a system installed salt setup, ie, the default -+ salt system paths apply. -+ """ -+ -+ id = attr.ib(default=None, init=False) -+ config = attr.ib(repr=False) -+ config_dir = attr.ib(init=False, default=None) -+ config_file = attr.ib(init=False, default=None) -+ python_executable = attr.ib(default=None) -+ system_install = attr.ib(repr=False, default=False) -+ display_name = attr.ib(init=False, default=None) -+ -+ def __attrs_post_init__(self): -+ if self.python_executable is None and self.system_install is False: -+ self.python_executable = sys.executable -+ # We really do not want buffered output -+ self.environ.setdefault("PYTHONUNBUFFERED", "1") -+ # Don't write .pyc files or create them in __pycache__ directories -+ self.environ.setdefault("PYTHONDONTWRITEBYTECODE", "1") -+ self.config_file = self.config["conf_file"] -+ self.config_dir = os.path.dirname(self.config_file) -+ self.id = self.config["id"] -+ self.config = freeze(self.config) -+ -+ def get_display_name(self): -+ """ -+ Returns a human readable name for the factory -+ """ -+ if self.display_name is None: -+ self.display_name = "{}(id={!r})".format(self.__class__.__name__, self.id) -+ return super().get_display_name() -+ -+ -+@attr.s(kw_only=True) -+class SaltCliImpl(SubprocessImpl): -+ """ -+ Salt CLI's subprocess interaction implementation -+ -+ Please look at :py:class:`~saltfactories.bases.SubprocessImpl` for the additional supported keyword -+ arguments documentation. -+ """ -+ -+ def cmdline(self, *args, minion_tgt=None, **kwargs): # pylint: disable=arguments-differ -+ """ -+ Construct a list of arguments to use when starting the subprocess -+ -+ :param str args: -+ Additional arguments to use when starting the subprocess -+ :keyword str minion_tgt: -+ The minion ID to target -+ :keyword kwargs: -+ Additional keyword arguments will be converted into ``key=value`` pairs to be consumed by the salt CLI's -+ """ -+ return self.factory.cmdline(*args, minion_tgt=minion_tgt, **kwargs) -+ -+ -+@attr.s(kw_only=True) -+class SaltCli(Salt, Process): -+ """ -+ Base factory for salt cli's -+ -+ :param bool hard_crash: -+ Pass ``--hard-crash`` to Salt's CLI's -+ -+ Please look at :py:class:`~saltfactories.bases.Salt` and -+ :py:class:`~saltfactories.bases.Process` for the additional supported keyword -+ arguments documentation. -+ """ -+ -+ hard_crash = attr.ib(repr=False, default=False) -+ # Override the following to default to non-mandatory and to None -+ display_name = attr.ib(init=False, default=None) -+ _minion_tgt = attr.ib(repr=False, init=False, default=None) -+ merge_json_output = attr.ib(repr=False, default=True) -+ -+ __cli_timeout_supported__ = attr.ib(repr=False, init=False, default=False) -+ __cli_log_level_supported__ = attr.ib(repr=False, init=False, default=True) -+ __cli_output_supported__ = attr.ib(repr=False, init=False, default=True) -+ __json_output__ = attr.ib(repr=False, init=False, default=False) -+ __merge_json_output__ = attr.ib(repr=False, init=False, default=True) -+ -+ def _get_impl_class(self): -+ return SaltCliImpl -+ -+ def __attrs_post_init__(self): -+ Process.__attrs_post_init__(self) -+ Salt.__attrs_post_init__(self) -+ -+ def get_script_args(self): -+ """ -+ Returns any additional arguments to pass to the CLI script -+ """ -+ if not self.hard_crash: -+ return super().get_script_args() -+ return ["--hard-crash"] -+ -+ def get_minion_tgt(self, minion_tgt=None): -+ return minion_tgt -+ -+ def cmdline( -+ self, *args, minion_tgt=None, merge_json_output=None, **kwargs -+ ): # pylint: disable=arguments-differ -+ """ -+ Construct a list of arguments to use when starting the subprocess -+ -+ :param str args: -+ Additional arguments to use when starting the subprocess -+ :keyword str minion_tgt: -+ The minion ID to target -+ :keyword bool merge_json_output: -+ The default behavior of salt outputters is to print one line per minion return, which makes -+ parsing the whole output as JSON impossible when targeting multiple minions. If this value -+ is ``True``, an attempt is made to merge each JSON line into a single dictionary. -+ :keyword kwargs: -+ Additional keyword arguments will be converted into ``key=value`` pairs to be consumed by the salt CLI's -+ """ -+ log.debug( -+ "Building cmdline. Minion target: %s; Input args: %s; Input kwargs: %s;", -+ minion_tgt, -+ args, -+ kwargs, -+ ) -+ minion_tgt = self._minion_tgt = self.get_minion_tgt(minion_tgt=minion_tgt) -+ if merge_json_output is None: -+ self.__merge_json_output__ = self.merge_json_output -+ else: -+ self.__merge_json_output__ = merge_json_output -+ cmdline = [] -+ -+ # Convert all passed in arguments to strings -+ args = [str(arg) for arg in args] -+ -+ # Handle the config directory flag -+ for arg in args: -+ if arg.startswith("--config-dir="): -+ break -+ if arg in ("-c", "--config-dir"): -+ break -+ else: -+ cmdline.append("--config-dir={}".format(self.config_dir)) -+ -+ # Handle the timeout CLI flag, if supported -+ if self.__cli_timeout_supported__: -+ salt_cli_timeout_next = False -+ for arg in args: -+ if arg.startswith("--timeout="): -+ # Let's actually change the _terminal_timeout value which is used to -+ # calculate when the run() method should actually timeout -+ try: -+ salt_cli_timeout = int(arg.split("--timeout=")[-1]) -+ except ValueError: -+ # Not a number? Let salt do it's error handling -+ break -+ if salt_cli_timeout >= self.impl._terminal_timeout: -+ self.impl._terminal_timeout = int(salt_cli_timeout) + 10 -+ break -+ if salt_cli_timeout_next: -+ try: -+ salt_cli_timeout = int(arg) -+ except ValueError: -+ # Not a number? Let salt do it's error handling -+ break -+ if salt_cli_timeout >= self.impl._terminal_timeout: -+ self.impl._terminal_timeout = int(salt_cli_timeout) + 10 -+ break -+ if arg == "-t" or arg.startswith("--timeout"): -+ salt_cli_timeout_next = True -+ continue -+ else: -+ # Pass the default timeout to salt and increase the internal timeout by 10 seconds to -+ # allow salt to exit cleanly. -+ salt_cli_timeout = self.impl._terminal_timeout -+ if salt_cli_timeout: -+ self.impl._terminal_timeout = salt_cli_timeout + 10 -+ # Add it to the salt command CLI flags -+ cmdline.append("--timeout={}".format(salt_cli_timeout)) -+ -+ # Handle the output flag -+ if self.__cli_output_supported__: -+ for idx, arg in enumerate(args): -+ if arg in ("--out", "--output"): -+ self.__json_output__ = args[idx + 1] == "json" -+ break -+ if arg.startswith(("--out=", "--output=")): -+ self.__json_output__ = arg.split("=")[-1].strip() == "json" -+ break -+ else: -+ # No output was passed, the default output is JSON -+ cmdline.append("--out=json") -+ self.__json_output__ = True -+ if self.__json_output__: -+ for arg in args: -+ if arg in ("--out-indent", "--output-indent"): -+ break -+ if arg.startswith(("--out-indent=", "--output-indent=")): -+ break -+ else: -+ # Default to one line per output -+ cmdline.append("--out-indent=0") -+ -+ if self.__cli_log_level_supported__: -+ # Handle the logging flag -+ for arg in args: -+ if arg in ("-l", "--log-level"): -+ break -+ if arg.startswith("--log-level="): -+ break -+ else: -+ # Default to being almost quiet on console output -+ cmdline.append("--log-level=critical") -+ -+ if minion_tgt: -+ cmdline.append(minion_tgt) -+ -+ # Add the remaining args -+ cmdline.extend(args) -+ -+ # Keyword arguments get passed as KEY=VALUE pairs to the CLI -+ for key in kwargs: -+ value = kwargs[key] -+ if not isinstance(value, str): -+ value = json.dumps(value) -+ cmdline.append("{}={}".format(key, value)) -+ cmdline = super().cmdline(*cmdline) -+ if self.python_executable: -+ if cmdline[0] != self.python_executable: -+ cmdline.insert(0, self.python_executable) -+ log.debug("Built cmdline: %s", cmdline) -+ return cmdline -+ -+ def process_output(self, stdout, stderr, cmdline=None): -+ """ -+ Process the output. When possible JSON is loaded from the output. -+ -+ :return: -+ Returns a tuple in the form of ``(stdout, stderr, loaded_json)`` -+ :rtype: tuple -+ """ -+ json_out = None -+ if stdout and self.__json_output__: -+ try: -+ json_out = json.loads(stdout) -+ except ValueError: -+ if self.__merge_json_output__: -+ try: -+ json_out = json.loads(stdout.replace("}\n{", ", ")) -+ except ValueError: -+ pass -+ if json_out is None: -+ log.debug("%s failed to load JSON from the following output:\n%r", self, stdout) -+ if ( -+ self.__cli_output_supported__ -+ and json_out -+ and isinstance(json_out, str) -+ and self.__json_output__ -+ ): -+ # Sometimes the parsed JSON is just a string, for example: -+ # OUTPUT: '"The salt master could not be contacted. Is master running?"\n' -+ # LOADED JSON: 'The salt master could not be contacted. Is master running?' -+ # -+ # In this case, we assign the loaded JSON to stdout and reset json_out -+ stdout = json_out -+ json_out = None -+ if ( -+ self.__cli_output_supported__ -+ and json_out -+ and self._minion_tgt -+ and self._minion_tgt != "*" -+ ): -+ try: -+ json_out = json_out[self._minion_tgt] -+ except KeyError: # pragma: no cover -+ pass -+ return stdout, stderr, json_out -+ -+ -+@attr.s(kw_only=True) -+class SystemdSaltDaemonImpl(DaemonImpl): -+ """ -+ Daemon systemd interaction implementation -+ -+ Please look at :py:class:`~saltfactories.bases.DaemonImpl` for the additional supported keyword -+ arguments documentation. -+ """ -+ -+ _process = attr.ib(init=False, repr=False, default=None) -+ _service_name = attr.ib(init=False, repr=False, default=None) -+ -+ def cmdline(self, *args): -+ """ -+ Construct a list of arguments to use when starting the subprocess -+ -+ :param str args: -+ Additional arguments to use when starting the subprocess -+ -+ """ -+ if args: # pragma: no cover -+ log.debug( -+ "%s.run() is ignoring the passed in arguments: %r", self.__class__.__name__, args -+ ) -+ return ("systemctl", "start", self.get_service_name()) -+ -+ def get_service_name(self): -+ """ -+ Return the systemd service name -+ """ -+ if self._service_name is None: -+ script_path = self.factory.get_script_path() -+ if os.path.isabs(script_path): -+ script_path = os.path.basename(script_path) -+ self._service_name = script_path -+ return self._service_name -+ -+ def is_running(self): -+ """ -+ Returns true if the sub-process is alive -+ """ -+ if self._process is None: -+ ret = self.internal_run("systemctl", "show", "-p", "MainPID", self.get_service_name()) -+ _, mainpid = ret.stdout.split("=") -+ if mainpid == "0": -+ return False -+ self._process = psutil.Process(int(mainpid)) -+ return self._process.is_running() -+ -+ def internal_run(self, *args, **kwargs): -+ """ -+ Run the given command synchronously -+ """ -+ result = subprocess.run( -+ args, -+ stdout=subprocess.PIPE, -+ stderr=subprocess.PIPE, -+ universal_newlines=True, -+ check=False, -+ **kwargs -+ ) -+ stdout = result.stdout.strip() -+ stderr = result.stderr.strip() -+ process_result = ProcessResult(result.returncode, stdout, stderr, cmdline=result.args) -+ log.info("%s %s", self.factory.__class__.__name__, process_result) -+ return process_result -+ -+ def start(self, *extra_cli_arguments, max_start_attempts=None, start_timeout=None): -+ started = super().start( -+ *extra_cli_arguments, max_start_attempts=max_start_attempts, start_timeout=start_timeout -+ ) -+ atexit.register(self.terminate) -+ return started -+ -+ def _terminate(self): -+ """ -+ This method actually terminates the started daemon -+ """ -+ if self._process is None: # pragma: no cover -+ return self._terminal_result -+ atexit.unregister(self.terminate) -+ log.info("Stopping %s", self.factory) -+ # Collect any child processes information before terminating the process -+ with contextlib.suppress(psutil.NoSuchProcess): -+ for child in psutil.Process(self.pid).children(recursive=True): -+ if child not in self._children: -+ self._children.append(child) -+ -+ pid = self.pid -+ cmdline = self._process.cmdline() -+ self.internal_run("systemctl", "stop", self.get_service_name()) -+ if self._process.is_running(): # pragma: no cover -+ try: -+ self._process.wait() -+ except psutil.TimeoutExpired: -+ self._process.terminate() -+ try: -+ self._process.wait() -+ except psutil.TimeoutExpired: -+ pass -+ exitcode = self._process.wait() or 0 -+ -+ self._process = None -+ # Lets log and kill any child processes left behind, including the main subprocess -+ # if it failed to properly stop -+ terminate_process( -+ pid=pid, -+ kill_children=True, -+ children=self._children, -+ slow_stop=self.factory.slow_stop, -+ ) -+ -+ self._terminal_stdout.close() -+ self._terminal_stderr.close() -+ stdout = "" -+ ret = self.internal_run("journalctl", "--no-pager", "-u", self.get_service_name()) -+ stderr = ret.stdout -+ try: -+ self._terminal_result = ProcessResult(exitcode, stdout, stderr, cmdline=cmdline) -+ log.info("%s %s", self.factory.__class__.__name__, self._terminal_result) -+ return self._terminal_result -+ finally: -+ self._terminal = None -+ self._terminal_stdout = None -+ self._terminal_stderr = None -+ self._terminal_timeout = None -+ self._children = [] -+ -+ @property -+ def pid(self): -+ if self.is_running(): -+ return self._process.pid -+ -+ -+@attr.s(kw_only=True) -+class SaltDaemon(Salt, Daemon): -+ """ -+ Base factory for salt daemon's -+ -+ Please look at :py:class:`~saltfactories.bases.Salt` and -+ :py:class:`~saltfactories.bases.Daemon` for the additional supported keyword -+ arguments documentation. -+ """ -+ -+ display_name = attr.ib(init=False, default=None) -+ event_listener = attr.ib(repr=False, default=None) -+ started_at = attr.ib(repr=False, default=None) -+ -+ def __attrs_post_init__(self): -+ Daemon.__attrs_post_init__(self) -+ Salt.__attrs_post_init__(self) -+ -+ if self.system_install is True and self.extra_cli_arguments_after_first_start_failure: -+ raise pytest.UsageError( -+ "You cannot pass `extra_cli_arguments_after_first_start_failure` to a salt " -+ "system installation setup." -+ ) -+ elif self.system_install is False: -+ for arg in self.extra_cli_arguments_after_first_start_failure: -+ if arg in ("-l", "--log-level"): -+ break -+ if arg.startswith("--log-level="): -+ break -+ else: -+ self.extra_cli_arguments_after_first_start_failure.append("--log-level=debug") -+ -+ def _get_impl_class(self): -+ if self.system_install: -+ return SystemdSaltDaemonImpl -+ return super()._get_impl_class() -+ -+ @classmethod -+ def configure( -+ cls, -+ factories_manager, -+ daemon_id, -+ root_dir=None, -+ defaults=None, -+ overrides=None, -+ **configure_kwargs -+ ): -+ """ -+ Configure the salt daemon -+ """ -+ return cls._configure( -+ factories_manager, -+ daemon_id, -+ root_dir=root_dir, -+ defaults=defaults, -+ overrides=overrides, -+ **configure_kwargs -+ ) -+ -+ @classmethod -+ def _configure( -+ cls, -+ factories_manager, -+ daemon_id, -+ root_dir=None, -+ defaults=None, -+ overrides=None, -+ ): -+ raise NotImplementedError -+ -+ @classmethod -+ def verify_config(cls, config): -+ salt.utils.verify.verify_env( -+ cls._get_verify_config_entries(config), -+ running_username(), -+ pki_dir=config.get("pki_dir") or "", -+ root_dir=config["root_dir"], -+ ) -+ -+ @classmethod -+ def _get_verify_config_entries(cls, config): -+ raise NotImplementedError -+ -+ @classmethod -+ def write_config(cls, config): -+ """ -+ Write the configuration to file -+ """ -+ config_file = config.pop("conf_file") -+ log.debug( -+ "Writing to configuration file %s. Configuration:\n%s", -+ config_file, -+ pprint.pformat(config), -+ ) -+ -+ # Write down the computed configuration into the config file -+ with salt.utils.files.fopen(config_file, "w") as wfh: -+ salt.utils.yaml.safe_dump(config, wfh, default_flow_style=False) -+ loaded_config = cls.load_config(config_file, config) -+ cls.verify_config(loaded_config) -+ return loaded_config -+ -+ @classmethod -+ def load_config(cls, config_file, config): -+ """ -+ Should return the configuration as the daemon would have loaded after -+ parsing the CLI -+ """ -+ raise NotImplementedError -+ -+ def get_check_events(self): -+ """ -+ Return a list of tuples in the form of `(master_id, event_tag)` check against to ensure the daemon is running -+ """ -+ raise NotImplementedError -+ -+ def cmdline(self, *args): -+ """ -+ Construct a list of arguments to use when starting the subprocess -+ -+ :param str args: -+ Additional arguments to use when starting the subprocess -+ -+ """ -+ _args = [] -+ # Handle the config directory flag -+ for arg in args: -+ if not isinstance(arg, str): -+ continue -+ if arg.startswith("--config-dir="): -+ break -+ if arg in ("-c", "--config-dir"): -+ break -+ else: -+ _args.append("--config-dir={}".format(self.config_dir)) -+ # Handle the logging flag -+ for arg in args: -+ if not isinstance(arg, str): -+ continue -+ if arg in ("-l", "--log-level"): -+ break -+ if arg.startswith("--log-level="): -+ break -+ else: -+ # Default to being almost quiet on console output -+ _args.append("--log-level=critical") -+ cmdline = super().cmdline(*(_args + list(args))) -+ if self.python_executable: -+ if cmdline[0] != self.python_executable: -+ cmdline.insert(0, self.python_executable) -+ return cmdline -+ -+ def run_start_checks(self, started_at, timeout_at): -+ """ -+ Run checks to confirm that the daemon has started -+ """ -+ if not super().run_start_checks(started_at, timeout_at): -+ return False -+ if not self.event_listener: # pragma: no cover -+ log.debug("The 'event_listener' attribute is not set. Not checking events...") -+ return True -+ -+ check_events = set(self.get_check_events()) -+ if not check_events: -+ log.debug("No events to listen to for %s", self) -+ return True -+ log.debug("Events to check for %s: %s", self, set(self.get_check_events())) -+ checks_start_time = time.time() -+ while time.time() <= timeout_at: -+ if not self.is_running(): -+ raise FactoryNotStarted("{} is no longer running".format(self)) -+ if not check_events: -+ break -+ check_events -= { -+ (event.daemon_id, event.tag) -+ for event in self.event_listener.get_events(check_events, after_time=started_at) -+ } -+ if check_events: -+ time.sleep(1.5) -+ else: -+ log.error( -+ "Failed to check events after %1.2f seconds for %s. Remaining events to check: %s", -+ time.time() - checks_start_time, -+ self, -+ check_events, -+ ) -+ return False -+ log.debug("All events checked for %s: %s", self, set(self.get_check_events())) -+ return True ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/cli/__init__.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/cli/__init__.py -@@ -0,0 +1,12 @@ -+""" -+CLI -+=== -+""" -+from . import call -+from . import cloud -+from . import cp -+from . import key -+from . import run -+from . import salt -+from . import spm -+from . import ssh ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/cli/call.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/cli/call.py -@@ -0,0 +1,23 @@ -+""" -+``salt-call`` CLI factory -+""" -+import attr -+ -+from saltfactories.bases import SaltCli -+ -+ -+@attr.s(kw_only=True, slots=True) -+class SaltCall(SaltCli): -+ """ -+ salt-call CLI factory -+ """ -+ -+ __cli_timeout_supported__ = attr.ib(repr=False, init=False, default=True) -+ -+ def get_minion_tgt(self, minion_tgt=None): -+ return None -+ -+ def process_output(self, stdout, stderr, cmdline=None): -+ # Under salt-call, the minion target is always "local" -+ self._minion_tgt = "local" -+ return super().process_output(stdout, stderr, cmdline=cmdline) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/cli/cloud.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/cli/cloud.py -@@ -0,0 +1,103 @@ -+""" -+.. -+ PYTEST_DONT_REWRITE -+ -+ -+``salt-cloud`` CLI factory -+""" -+import logging -+import pathlib -+import pprint -+import urllib.parse -+ -+import attr -+import salt.config -+import salt.utils.dictupdate -+import salt.utils.files -+import salt.utils.yaml -+ -+from saltfactories.bases import SaltCli -+from saltfactories.utils import running_username -+ -+log = logging.getLogger(__name__) -+ -+ -+@attr.s(kw_only=True, slots=True) -+class SaltCloud(SaltCli): -+ """ -+ salt-cloud CLI factory -+ """ -+ -+ @staticmethod -+ def default_config(root_dir, master_id, defaults=None, overrides=None): -+ if defaults is None: -+ defaults = {} -+ -+ conf_dir = root_dir / "conf" -+ conf_dir.mkdir(parents=True, exist_ok=True) -+ for confd in ("cloud.conf.d", "cloud.providers.d", "cloud.profiles.d"): -+ dpath = conf_dir / confd -+ dpath.mkdir(exist_ok=True) -+ -+ conf_file = str(conf_dir / "cloud") -+ -+ _defaults = { -+ "conf_file": conf_file, -+ "root_dir": str(root_dir), -+ "log_file": "logs/cloud.log", -+ "log_level_logfile": "debug", -+ "pytest-cloud": { -+ "master-id": master_id, -+ "log": {"prefix": "{{cli_name}}({})".format(master_id)}, -+ }, -+ } -+ # Merge in the initial default options with the internal _defaults -+ salt.utils.dictupdate.update(defaults, _defaults, merge_lists=True) -+ -+ if overrides: -+ # Merge in the default options with the master_overrides -+ salt.utils.dictupdate.update(defaults, overrides, merge_lists=True) -+ -+ return defaults -+ -+ @classmethod -+ def configure( -+ cls, -+ factories_manager, -+ daemon_id, -+ root_dir=None, -+ defaults=None, -+ overrides=None, -+ **configure_kwargs -+ ): -+ return cls.default_config(root_dir, daemon_id, defaults=defaults, overrides=overrides) -+ -+ @classmethod -+ def verify_config(cls, config): -+ prepend_root_dirs = [] -+ for config_key in ("log_file",): -+ if urllib.parse.urlparse(config.get(config_key, "")).scheme == "": -+ prepend_root_dirs.append(config_key) -+ if prepend_root_dirs: -+ salt.config.prepend_root_dir(config, prepend_root_dirs) -+ salt.utils.verify.verify_env( -+ [str(pathlib.Path(config["log_file"]).parent)], -+ running_username(), -+ pki_dir=config.get("pki_dir") or "", -+ root_dir=config["root_dir"], -+ ) -+ -+ @classmethod -+ def write_config(cls, config): -+ cls.verify_config(config) -+ config_file = config.pop("conf_file") -+ log.debug( -+ "Writing to configuration file %s. Configuration:\n%s", -+ config_file, -+ pprint.pformat(config), -+ ) -+ -+ # Write down the computed configuration into the config file -+ with salt.utils.files.fopen(config_file, "w") as wfh: -+ salt.utils.yaml.safe_dump(config, wfh, default_flow_style=False) -+ return salt.config.cloud_config(config_file) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/cli/cp.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/cli/cp.py -@@ -0,0 +1,20 @@ -+""" -+``salt-cp`` CLI factory -+""" -+import attr -+ -+from saltfactories.bases import SaltCli -+ -+ -+@attr.s(kw_only=True, slots=True) -+class SaltCp(SaltCli): -+ """ -+ salt-cp CLI factory -+ """ -+ -+ __cli_timeout_supported__ = attr.ib(repr=False, init=False, default=True) -+ -+ def process_output(self, stdout, stderr, cmdline=None): -+ if "No minions matched the target. No command was sent, no jid was assigned.\n" in stdout: -+ stdout = stdout.split("\n", 1)[1:][0] -+ return super().process_output(stdout, stderr, cmdline=cmdline) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/cli/key.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/cli/key.py -@@ -0,0 +1,45 @@ -+""" -+.. -+ PYTEST_DONT_REWRITE -+ -+ -+``salt-key`` CLI factory -+""" -+import re -+ -+import attr -+from salt.utils.parsers import SaltKeyOptionParser -+ -+from saltfactories.bases import SaltCli -+ -+try: -+ SALT_KEY_LOG_LEVEL_SUPPORTED = SaltKeyOptionParser._skip_console_logging_config_ is False -+except AttributeError: # pragma: no cover -+ # New logging is in place -+ SALT_KEY_LOG_LEVEL_SUPPORTED = True -+ -+ -+@attr.s(kw_only=True, slots=True) -+class SaltKey(SaltCli): -+ """ -+ salt-key CLI factory -+ """ -+ -+ _output_replace_re = attr.ib( -+ init=False, -+ repr=False, -+ default=re.compile(r"((The following keys are going to be.*:|Key for minion.*)\n)"), -+ ) -+ # As of Neon, salt-key still does not support --log-level -+ # Only when we get the new logging merged in will we get that, so remove that CLI flag -+ __cli_log_level_supported__ = attr.ib( -+ repr=False, init=False, default=SALT_KEY_LOG_LEVEL_SUPPORTED -+ ) -+ -+ def get_minion_tgt(self, minion_tgt=None): -+ return None -+ -+ def process_output(self, stdout, stderr, cmdline=None): -+ # salt-key print()s to stdout regardless of output chosen -+ stdout = self._output_replace_re.sub("", stdout) -+ return super().process_output(stdout, stderr, cmdline=cmdline) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/cli/run.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/cli/run.py -@@ -0,0 +1,23 @@ -+""" -+``salt-run`` CLI factory -+""" -+import attr -+ -+from saltfactories.bases import SaltCli -+ -+ -+@attr.s(kw_only=True, slots=True) -+class SaltRun(SaltCli): -+ """ -+ salt-run CLI factory -+ """ -+ -+ __cli_timeout_supported__ = attr.ib(repr=False, init=False, default=True) -+ -+ def get_minion_tgt(self, minion_tgt=None): -+ return None -+ -+ def process_output(self, stdout, stderr, cmdline=None): -+ if "No minions matched the target. No command was sent, no jid was assigned.\n" in stdout: -+ stdout = stdout.split("\n", 1)[1:][0] -+ return super().process_output(stdout, stderr, cmdline=cmdline) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/cli/salt.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/cli/salt.py -@@ -0,0 +1,31 @@ -+""" -+``salt`` CLI factory -+""" -+import attr -+import pytest -+ -+from saltfactories.bases import SaltCli -+ -+ -+@attr.s(kw_only=True, slots=True) -+class Salt(SaltCli): -+ """ -+ salt CLI factory -+ """ -+ -+ __cli_timeout_supported__ = attr.ib(repr=False, init=False, default=True) -+ -+ def cmdline(self, *args, minion_tgt=None, **kwargs): # pylint: disable=arguments-differ -+ skip_raise_exception_args = {"-V", "--version", "--versions-report", "--help"} -+ if minion_tgt is None and not set(args).intersection(skip_raise_exception_args): -+ raise pytest.UsageError( -+ "The `minion_tgt` keyword argument is mandatory for the salt CLI factory" -+ ) -+ return super().cmdline(*args, minion_tgt=minion_tgt, **kwargs) -+ -+ def process_output(self, stdout, stderr, cmdline=None): -+ if "No minions matched the target. No command was sent, no jid was assigned.\n" in stdout: -+ stdout = stdout.split("\n", 1)[1:][0] -+ if cmdline and "--show-jid" in cmdline and stdout.startswith("jid: "): -+ stdout = stdout.split("\n", 1)[-1].strip() -+ return super().process_output(stdout, stderr, cmdline=cmdline) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/cli/spm.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/cli/spm.py -@@ -0,0 +1,18 @@ -+""" -+``spm`` CLI factory -+""" -+import attr -+ -+from saltfactories.bases import SaltCli -+ -+ -+@attr.s(kw_only=True, slots=True) -+class Spm(SaltCli): -+ """ -+ ``spm`` CLI factory -+ """ -+ -+ __cli_output_supported__ = attr.ib(repr=False, init=False, default=False) -+ -+ def get_minion_tgt(self, minion_tgt=None): -+ return None ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/cli/ssh.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/cli/ssh.py -@@ -0,0 +1,38 @@ -+""" -+salt-ssh CLI factory -+""" -+import attr -+ -+from saltfactories.bases import SaltCli -+ -+ -+@attr.s(kw_only=True, slots=True) -+class SaltSsh(SaltCli): -+ """ -+ salt CLI factory -+ """ -+ -+ roster_file = attr.ib(default=None) -+ client_key = attr.ib(default=None) -+ target_host = attr.ib(default=None) -+ ssh_user = attr.ib(default=None) -+ -+ def __attrs_post_init__(self): -+ super().__attrs_post_init__() -+ if self.target_host is None: -+ self.target_host = "127.0.0.1" -+ -+ def get_script_args(self): -+ script_args = super().get_script_args() -+ if self.roster_file: -+ script_args.append("--roster-file={}".format(self.roster_file)) -+ if self.client_key: -+ script_args.append("--priv={}".format(self.client_key)) -+ if self.ssh_user: -+ script_args.append("--user={}".format(self.ssh_user)) -+ return script_args -+ -+ def get_minion_tgt(self, minion_tgt=None): -+ if minion_tgt is None and self.target_host: -+ minion_tgt = self.target_host -+ return minion_tgt ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/client.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/client.py -@@ -0,0 +1,103 @@ -+""" -+.. -+ PYTEST_DONT_REWRITE -+ -+Salt Client in-process implementation -+""" -+import logging -+import re -+ -+import attr -+import pytest -+import salt.client -+ -+ -+log = logging.getLogger(__name__) -+ -+ -+@attr.s(kw_only=True, slots=True) -+class LocalClient: -+ """ -+ Wrapper class around Salt's local client -+ """ -+ -+ STATE_FUNCTION_RUNNING_RE = re.compile( -+ r"""The function (?:"|')(?P<state_func>.*)(?:"|') is running as PID """ -+ r"(?P<pid>[\d]+) and was started at (?P<date>.*) with jid (?P<jid>[\d]+)" -+ ) -+ -+ master_config = attr.ib(repr=False) -+ functions_known_to_return_none = attr.ib(repr=False) -+ __client = attr.ib(init=False, repr=False) -+ -+ @functions_known_to_return_none.default -+ def _set_functions_known_to_return_none(self): -+ return ( -+ "data.get", -+ "file.chown", -+ "file.chgrp", -+ "pkg.refresh_db", -+ "ssh.recv_known_host_entries", -+ "time.sleep", -+ ) -+ -+ @__client.default -+ def _set_client(self): -+ return salt.client.get_local_client(mopts=self.master_config) -+ -+ def run(self, function, *args, minion_tgt="minion", timeout=300, **kwargs): -+ """ -+ Run a single salt function and condition the return down to match the -+ behavior of the raw function call -+ """ -+ if "f_arg" in kwargs: -+ kwargs["arg"] = kwargs.pop("f_arg") -+ if "f_timeout" in kwargs: -+ kwargs["timeout"] = kwargs.pop("f_timeout") -+ ret = self.__client.cmd(minion_tgt, function, args, timeout=timeout, kwarg=kwargs) -+ if minion_tgt not in ret: -+ pytest.fail( -+ "WARNING(SHOULD NOT HAPPEN #1935): Failed to get a reply " -+ "from the minion '{}'. Command output: {}".format(minion_tgt, ret) -+ ) -+ elif ret[minion_tgt] is None and function not in self.__functions_known_to_return_none: -+ pytest.fail( -+ "WARNING(SHOULD NOT HAPPEN #1935): Failed to get '{}' from " -+ "the minion '{}'. Command output: {}".format(function, minion_tgt, ret) -+ ) -+ -+ # Try to match stalled state functions -+ ret[minion_tgt] = self._check_state_return(ret[minion_tgt]) -+ -+ return ret[minion_tgt] -+ -+ def _check_state_return(self, ret): -+ if isinstance(ret, dict): -+ # This is the supposed return format for state calls -+ return ret -+ -+ if isinstance(ret, list): -+ jids = [] -+ # These are usually errors -+ for item in ret[:]: -+ if not isinstance(item, str): -+ # We don't know how to handle this -+ continue -+ match = self.STATE_FUNCTION_RUNNING_RE.match(item) -+ if not match: -+ # We don't know how to handle this -+ continue -+ jid = match.group("jid") -+ if jid in jids: -+ continue -+ -+ jids.append(jid) -+ job_data = self.run("saltutil.find_job", jid) -+ job_kill = self.run("saltutil.kill_job", jid) -+ -+ msg = ( -+ "A running state.single was found causing a state lock. " -+ "Job details: '{}' Killing Job Returned: '{}'".format(job_data, job_kill) -+ ) -+ ret.append("[TEST SUITE ENFORCED]{}[/TEST SUITE ENFORCED]".format(msg)) -+ return ret ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/daemons/__init__.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/daemons/__init__.py -@@ -0,0 +1,11 @@ -+""" -+Daemons -+======= -+""" -+from . import api -+from . import container -+from . import master -+from . import minion -+from . import proxy -+from . import sshd -+from . import syndic ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/daemons/api.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/daemons/api.py -@@ -0,0 +1,57 @@ -+""" -+.. -+ PYTEST_DONT_REWRITE -+ -+ -+Salt API Factory -+""" -+import attr -+import pytest -+ -+from saltfactories.bases import SaltDaemon -+ -+ -+@attr.s(kw_only=True, slots=True) -+class SaltApi(SaltDaemon): -+ def __attrs_post_init__(self): -+ if "rest_cherrypy" in self.config: -+ self.check_ports = [self.config["rest_cherrypy"]["port"]] -+ elif "rest_tornado" in self.config: -+ self.check_ports = [self.config["rest_tornado"]["port"]] -+ else: -+ raise pytest.UsageError( -+ "The salt-master configuration for this salt-api instance does not seem to have " -+ "any api properly configured." -+ ) -+ super().__attrs_post_init__() -+ -+ @classmethod -+ def _configure( -+ cls, -+ factories_manager, -+ daemon_id, -+ root_dir=None, -+ defaults=None, -+ overrides=None, -+ ): -+ raise pytest.UsageError( -+ "The salt-api daemon is not configurable. It uses the salt-master config that " -+ "it's attached to." -+ ) -+ -+ @classmethod -+ def _get_verify_config_entries(cls, config): -+ return [] -+ -+ @classmethod -+ def load_config(cls, config_file, config): -+ raise pytest.UsageError( -+ "The salt-api daemon does not have it's own config file. It uses the salt-master config that " -+ "it's attached to." -+ ) -+ -+ def get_check_events(self): -+ """ -+ Return a list of tuples in the form of `(master_id, event_tag)` check against to ensure the daemon is running -+ """ -+ return [] ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/daemons/container.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/daemons/container.py -@@ -0,0 +1,579 @@ -+""" -+.. -+ PYTEST_DONT_REWRITE -+ -+ -+Container based factories -+""" -+import atexit -+import logging -+import os -+ -+import attr -+ -+from saltfactories import bases -+from saltfactories import CODE_ROOT_DIR -+from saltfactories.daemons import minion -+from saltfactories.exceptions import FactoryNotStarted -+from saltfactories.utils import format_callback_to_string -+from saltfactories.utils import ports -+from saltfactories.utils import random_string -+from saltfactories.utils import time -+from saltfactories.utils.processes import ProcessResult -+ -+try: -+ import docker -+ from docker.errors import APIError -+ -+ HAS_DOCKER = True -+except ImportError: # pragma: no cover -+ HAS_DOCKER = False -+ -+ class APIError(Exception): -+ pass -+ -+ -+try: -+ from requests.exceptions import ConnectionError as RequestsConnectionError -+ -+ HAS_REQUESTS = True -+except ImportError: # pragma: no cover -+ HAS_REQUESTS = False -+ -+ class RequestsConnectionError(ConnectionError): -+ pass -+ -+ -+try: -+ import pywintypes -+ -+ PyWinTypesError = pywintypes.error # pragma: no cover -+except ImportError: -+ -+ class PyWinTypesError(Exception): -+ pass -+ -+ -+log = logging.getLogger(__name__) -+ -+ -+@attr.s(kw_only=True) -+class Container(bases.Factory): -+ image = attr.ib() -+ name = attr.ib(default=None) -+ check_ports = attr.ib(default=None) -+ docker_client = attr.ib(repr=False, default=None) -+ container_run_kwargs = attr.ib(repr=False, default=attr.Factory(dict)) -+ container = attr.ib(init=False, default=None, repr=False) -+ start_timeout = attr.ib(repr=False, default=30) -+ max_start_attempts = attr.ib(repr=False, default=3) -+ _before_start_callbacks = attr.ib(repr=False, hash=False, default=attr.Factory(list)) -+ _before_terminate_callbacks = attr.ib(repr=False, hash=False, default=attr.Factory(list)) -+ _after_start_callbacks = attr.ib(repr=False, hash=False, default=attr.Factory(list)) -+ _after_terminate_callbacks = attr.ib(repr=False, hash=False, default=attr.Factory(list)) -+ _terminate_result = attr.ib(repr=False, hash=False, init=False, default=None) -+ -+ def __attrs_post_init__(self): -+ super().__attrs_post_init__() -+ if self.name is None: -+ self.name = random_string("factories-") -+ if self.docker_client is None: -+ if not HAS_DOCKER: -+ raise RuntimeError("The docker python library was not found installed") -+ if not HAS_REQUESTS: -+ raise RuntimeError("The requests python library was not found installed") -+ self.docker_client = docker.from_env() -+ -+ def before_start(self, callback, *args, **kwargs): -+ """ -+ Register a function callback to run before the container starts -+ -+ :param ~collections.abc.Callable callback: -+ The function to call back -+ :keyword args: -+ The arguments to pass to the callback -+ :keyword kwargs: -+ The keyword arguments to pass to the callback -+ """ -+ self._before_start_callbacks.append((callback, args, kwargs)) -+ -+ def after_start(self, callback, *args, **kwargs): -+ """ -+ Register a function callback to run after the container starts -+ -+ :param ~collections.abc.Callable callback: -+ The function to call back -+ :keyword args: -+ The arguments to pass to the callback -+ :keyword kwargs: -+ The keyword arguments to pass to the callback -+ """ -+ self._after_start_callbacks.append((callback, args, kwargs)) -+ -+ def before_terminate(self, callback, *args, **kwargs): -+ """ -+ Register a function callback to run before the container terminates -+ -+ :param ~collections.abc.Callable callback: -+ The function to call back -+ :keyword args: -+ The arguments to pass to the callback -+ :keyword kwargs: -+ The keyword arguments to pass to the callback -+ """ -+ self._before_terminate_callbacks.append((callback, args, kwargs)) -+ -+ def after_terminate(self, callback, *args, **kwargs): -+ """ -+ Register a function callback to run after the container terminates -+ -+ :param ~collections.abc.Callable callback: -+ The function to call back -+ :keyword args: -+ The arguments to pass to the callback -+ :keyword kwargs: -+ The keyword arguments to pass to the callback -+ """ -+ self._after_terminate_callbacks.append((callback, args, kwargs)) -+ -+ def start(self, *command, max_start_attempts=None, start_timeout=None): -+ if self.is_running(): -+ log.warning("%s is already running.", self) -+ return True -+ connectable = Container.client_connectable(self.docker_client) -+ if connectable is not True: -+ self.terminate() -+ raise RuntimeError(connectable) -+ self._terminate_result = None -+ atexit.register(self.terminate) -+ factory_started = False -+ for callback, args, kwargs in self._before_start_callbacks: -+ try: -+ callback(*args, **kwargs) -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ log.info( -+ "Exception raised when running %s: %s", -+ format_callback_to_string(callback, args, kwargs), -+ exc, -+ exc_info=True, -+ ) -+ -+ start_time = time.time() -+ start_attempts = max_start_attempts or self.max_start_attempts -+ current_attempt = 0 -+ while current_attempt <= start_attempts: -+ current_attempt += 1 -+ if factory_started: -+ break -+ log.info("Starting %s. Attempt: %d of %d", self, current_attempt, start_attempts) -+ current_start_time = time.time() -+ start_running_timeout = current_start_time + (start_timeout or self.start_timeout) -+ -+ # Start the container -+ self.container = self.docker_client.containers.run( -+ self.image, -+ name=self.name, -+ detach=True, -+ stdin_open=True, -+ command=list(command) or None, -+ **self.container_run_kwargs -+ ) -+ while time.time() <= start_running_timeout: -+ # Don't know why, but if self.container wasn't previously in a running -+ # state, and now it is, we have to re-set the self.container attribute -+ # so that it gives valid status information -+ self.container = self.docker_client.containers.get(self.name) -+ if self.container.status != "running": -+ time.sleep(0.25) -+ continue -+ -+ self.container = self.docker_client.containers.get(self.name) -+ logs = self.container.logs(stdout=True, stderr=True, stream=False) -+ if isinstance(logs, bytes): -+ stdout = logs.decode() -+ stderr = None -+ else: -+ stdout = logs[0].decode() -+ stderr = logs[1].decode() -+ if stdout and stderr: -+ log.info("Running Container Logs:\n%s\n%s", stdout, stderr) -+ elif stdout: -+ log.info("Running Container Logs:\n%s", stdout) -+ -+ # If we reached this far it means that we got the running status above, and -+ # now that the container has started, run start checks -+ try: -+ if ( -+ self.run_container_start_checks(current_start_time, start_running_timeout) -+ is False -+ ): -+ time.sleep(0.5) -+ continue -+ except FactoryNotStarted: -+ self.terminate() -+ break -+ log.info( -+ "The %s factory is running after %d attempts. Took %1.2f seconds", -+ self, -+ current_attempt, -+ time.time() - start_time, -+ ) -+ factory_started = True -+ break -+ else: -+ # We reached start_running_timeout, re-try -+ try: -+ self.container.remove(force=True) -+ self.container.wait() -+ except docker.errors.NotFound: -+ pass -+ self.container = None -+ else: -+ # The factory failed to confirm it's running status -+ self.terminate() -+ if factory_started: -+ for callback, args, kwargs in self._after_start_callbacks: -+ try: -+ callback(*args, **kwargs) -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ log.info( -+ "Exception raised when running %s: %s", -+ format_callback_to_string(callback, args, kwargs), -+ exc, -+ exc_info=True, -+ ) -+ # TODO: Add containers to the processes stats?! -+ # if self.factories_manager and self.factories_manager.stats_processes is not None: -+ # self.factories_manager.stats_processes[self.get_display_name()] = psutil.Process( -+ # self.pid -+ # ) -+ return factory_started -+ result = self.terminate() -+ raise FactoryNotStarted( -+ "The {} factory has failed to confirm running status after {} attempts, which " -+ "took {:.2f} seconds({:.2f} seconds each)".format( -+ self, -+ current_attempt - 1, -+ time.time() - start_time, -+ start_timeout or self.start_timeout, -+ ), -+ stdout=result.stdout, -+ stderr=result.stderr, -+ exitcode=result.exitcode, -+ ) -+ -+ def started(self, *command, max_start_attempts=None, start_timeout=None): -+ """ -+ Start the container and return it's instance so it can be used as a context manager -+ """ -+ self.start(*command, max_start_attempts=max_start_attempts, start_timeout=start_timeout) -+ return self -+ -+ def terminate(self): -+ if self._terminate_result is not None: -+ # The factory is already terminated -+ return self._terminate_result -+ atexit.unregister(self.terminate) -+ for callback, args, kwargs in self._before_terminate_callbacks: -+ try: -+ callback(*args, **kwargs) -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ log.info( -+ "Exception raised when running %s: %s", -+ format_callback_to_string(callback, args, kwargs), -+ exc, -+ exc_info=True, -+ ) -+ stdout = stderr = None -+ try: -+ if self.container is not None: -+ container = self.docker_client.containers.get(self.name) -+ logs = container.logs(stdout=True, stderr=True, stream=False) -+ if isinstance(logs, bytes): -+ stdout = logs.decode() -+ else: -+ stdout = logs[0].decode() -+ stderr = logs[1].decode() -+ if stdout and stderr: -+ log.info("Stopped Container Logs:\n%s\n%s", stdout, stderr) -+ elif stdout: -+ log.info("Stopped Container Logs:\n%s", stdout) -+ if container.status == "running": -+ container.remove(force=True) -+ container.wait() -+ self.container = None -+ except docker.errors.NotFound: -+ pass -+ finally: -+ for callback, args, kwargs in self._after_terminate_callbacks: -+ try: -+ callback(*args, **kwargs) -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ log.info( -+ "Exception raised when running %s: %s", -+ format_callback_to_string(callback, args, kwargs), -+ exc, -+ exc_info=True, -+ ) -+ self._terminate_result = ProcessResult(exitcode=0, stdout=stdout, stderr=stderr) -+ return self._terminate_result -+ -+ def get_check_ports(self): -+ """ -+ Return a list of ports to check against to ensure the daemon is running -+ """ -+ return self.check_ports or [] -+ -+ def is_running(self): -+ if self.container is None: -+ return False -+ -+ self.container = self.docker_client.containers.get(self.name) -+ return self.container.status == "running" -+ -+ def run(self, *cmd, **kwargs): -+ if len(cmd) == 1: -+ cmd = cmd[0] -+ log.info("%s is running %r ...", self, cmd) -+ # We force dmux to True so that we always get back both stdout and stderr -+ container = self.docker_client.containers.get(self.name) -+ ret = container.exec_run(cmd, demux=True, **kwargs) -+ exitcode = ret.exit_code -+ stdout = stderr = None -+ if ret.output: -+ stdout, stderr = ret.output -+ if stdout is not None: -+ stdout = stdout.decode() -+ if stderr is not None: -+ stderr = stderr.decode() -+ return ProcessResult(exitcode=exitcode, stdout=stdout, stderr=stderr, cmdline=cmd) -+ -+ @staticmethod -+ def client_connectable(docker_client): -+ try: -+ if not docker_client.ping(): -+ return "The docker client failed to get a ping response from the docker daemon" -+ return True -+ except (APIError, RequestsConnectionError, PyWinTypesError) as exc: -+ return "The docker client failed to ping the docker server: {}".format(exc) -+ -+ def run_container_start_checks(self, started_at, timeout_at): -+ checks_start_time = time.time() -+ while time.time() <= timeout_at: -+ if not self.is_running(): -+ raise FactoryNotStarted("{} is no longer running".format(self)) -+ if self._container_start_checks(): -+ break -+ else: -+ log.error( -+ "Failed to run container start checks after %1.2f seconds", -+ time.time() - checks_start_time, -+ ) -+ return False -+ check_ports = set(self.get_check_ports()) -+ if not check_ports: -+ return True -+ while time.time() <= timeout_at: -+ if not self.is_running(): -+ raise FactoryNotStarted("{} is no longer running".format(self)) -+ if not check_ports: -+ break -+ check_ports -= ports.get_connectable_ports(check_ports) -+ if check_ports: -+ time.sleep(0.5) -+ else: -+ log.error("Failed to check ports after %1.2f seconds", time.time() - checks_start_time) -+ return False -+ return True -+ -+ def _container_start_checks(self): -+ return True -+ -+ def __enter__(self): -+ if not self.is_running(): -+ raise RuntimeError( -+ "Factory not yet started. Perhaps you're after something like:\n\n" -+ "with {}.started() as factory:\n" -+ " yield factory".format(self.__class__.__name__) -+ ) -+ return self -+ -+ def __exit__(self, *_): -+ self.terminate() -+ -+ -+@attr.s(kw_only=True) -+class SaltDaemon(bases.SaltDaemon, Container): -+ def __attrs_post_init__(self): -+ self.daemon_started = self.daemon_starting = False -+ if self.python_executable is None: -+ # Default to whatever is the default python in the container -+ self.python_executable = "python" -+ bases.SaltDaemon.__attrs_post_init__(self) -+ Container.__attrs_post_init__(self) -+ # There are some volumes which NEED to exist on the container so -+ # that configs are in the right place and also our custom salt -+ # plugins along with the custom scripts to start the daemons. -+ root_dir = os.path.dirname(self.config["root_dir"]) -+ config_dir = str(self.config_dir) -+ scripts_dir = str(self.factories_manager.scripts_dir) -+ volumes = { -+ root_dir: {"bind": root_dir, "mode": "z"}, -+ scripts_dir: {"bind": scripts_dir, "mode": "z"}, -+ config_dir: {"bind": self.config_dir, "mode": "z"}, -+ str(CODE_ROOT_DIR): {"bind": str(CODE_ROOT_DIR), "mode": "z"}, -+ } -+ if "volumes" not in self.container_run_kwargs: -+ self.container_run_kwargs["volumes"] = {} -+ self.container_run_kwargs["volumes"].update(volumes) -+ self.container_run_kwargs.setdefault("hostname", self.name) -+ self.container_run_kwargs.setdefault("auto_remove", True) -+ -+ def cmdline(self, *args): -+ return ["docker", "exec", "-i", self.name] + super().cmdline(*args) -+ -+ def start(self, *extra_cli_arguments, max_start_attempts=None, start_timeout=None): -+ # Start the container -+ Container.start(self, max_start_attempts=max_start_attempts, start_timeout=start_timeout) -+ self.daemon_starting = True -+ # Now that the container is up, let's start the daemon -+ self.daemon_started = bases.SaltDaemon.start( -+ self, -+ *extra_cli_arguments, -+ max_start_attempts=max_start_attempts, -+ start_timeout=start_timeout -+ ) -+ return self.daemon_started -+ -+ def terminate(self): -+ self.daemon_started = self.daemon_starting = False -+ ret = bases.SaltDaemon.terminate(self) -+ Container.terminate(self) -+ return ret -+ -+ def is_running(self): -+ running = Container.is_running(self) -+ if running is False: -+ return running -+ if self.daemon_starting or self.daemon_started: -+ return bases.SaltDaemon.is_running(self) -+ return running -+ -+ def get_check_ports(self): -+ """ -+ Return a list of ports to check against to ensure the daemon is running -+ """ -+ return Container.get_check_ports(self) + bases.SaltDaemon.get_check_ports(self) -+ -+ def before_start( -+ self, callback, *args, on_container=False, **kwargs -+ ): # pylint: disable=arguments-differ -+ """ -+ Register a function callback to run before the daemon starts -+ -+ :param ~collections.abc.Callable callback: -+ The function to call back -+ :keyword bool on_container: -+ If true, the callback will be registered on the container and not the daemon. -+ :keyword args: -+ The arguments to pass to the callback -+ :keyword kwargs: -+ The keyword arguments to pass to the callback -+ """ -+ if on_container: -+ Container.before_start(self, callback, *args, **kwargs) -+ else: -+ bases.SaltDaemon.before_start(self, callback, *args, **kwargs) -+ -+ def after_start( -+ self, callback, *args, on_container=False, **kwargs -+ ): # pylint: disable=arguments-differ -+ """ -+ Register a function callback to run after the daemon starts -+ -+ :param ~collections.abc.Callable callback: -+ The function to call back -+ :keyword bool on_container: -+ If true, the callback will be registered on the container and not the daemon. -+ :keyword args: -+ The arguments to pass to the callback -+ :keyword kwargs: -+ The keyword arguments to pass to the callback -+ """ -+ if on_container: -+ Container.after_start(self, callback, *args, **kwargs) -+ else: -+ bases.SaltDaemon.after_start(self, callback, *args, **kwargs) -+ -+ def before_terminate( -+ self, callback, *args, on_container=False, **kwargs -+ ): # pylint: disable=arguments-differ -+ """ -+ Register a function callback to run before the daemon terminates -+ -+ :param ~collections.abc.Callable callback: -+ The function to call back -+ :keyword bool on_container: -+ If true, the callback will be registered on the container and not the daemon. -+ :keyword args: -+ The arguments to pass to the callback -+ :keyword kwargs: -+ The keyword arguments to pass to the callback -+ """ -+ if on_container: -+ Container.before_terminate(self, callback, *args, **kwargs) -+ else: -+ bases.SaltDaemon.before_terminate(self, callback, *args, **kwargs) -+ -+ def after_terminate( -+ self, callback, *args, on_container=False, **kwargs -+ ): # pylint: disable=arguments-differ -+ """ -+ Register a function callback to run after the daemon terminates -+ -+ :param ~collections.abc.Callable callback: -+ The function to call back -+ :keyword bool on_container: -+ If true, the callback will be registered on the container and not the daemon. -+ :keyword args: -+ The arguments to pass to the callback -+ :keyword kwargs: -+ The keyword arguments to pass to the callback -+ """ -+ if on_container: -+ Container.after_terminate(self, callback, *args, **kwargs) -+ else: -+ bases.SaltDaemon.after_terminate(self, callback, *args, **kwargs) -+ -+ def started(self, *extra_cli_arguments, max_start_attempts=None, start_timeout=None): -+ """ -+ Start the daemon and return it's instance so it can be used as a context manager -+ """ -+ return bases.SaltDaemon.started( -+ self, -+ *extra_cli_arguments, -+ max_start_attempts=max_start_attempts, -+ start_timeout=start_timeout -+ ) -+ -+ def get_check_events(self): -+ """ -+ Return a list of tuples in the form of `(master_id, event_tag)` check against to ensure the daemon is running -+ """ -+ raise NotImplementedError -+ -+ -+@attr.s(kw_only=True, slots=True) -+class SaltMinion(SaltDaemon, minion.SaltMinion): -+ """ -+ Salt minion daemon implementation running in a docker container -+ """ -+ -+ def get_check_events(self): -+ """ -+ Return a list of tuples in the form of `(master_id, event_tag)` check against to ensure the daemon is running -+ """ -+ return minion.SaltMinion.get_check_events(self) -+ -+ def run_start_checks(self, started_at, timeout_at): -+ return minion.SaltMinion.run_start_checks(self, started_at, timeout_at) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/daemons/master.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/daemons/master.py -@@ -0,0 +1,522 @@ -+""" -+.. -+ PYTEST_DONT_REWRITE -+ -+ -+Salt Master Factory -+""" -+import copy -+import pathlib -+import shutil -+from functools import partial -+ -+import attr -+import salt.config -+import salt.utils.dictupdate -+ -+from saltfactories import cli -+from saltfactories import client -+from saltfactories.bases import SaltDaemon -+from saltfactories.utils import cli_scripts -+from saltfactories.utils import ports -+from saltfactories.utils import running_username -+from saltfactories.utils.tempfiles import SaltPillarTree -+from saltfactories.utils.tempfiles import SaltStateTree -+ -+ -+@attr.s(kw_only=True, slots=True) -+class SaltMaster(SaltDaemon): -+ on_auth_event_callback = attr.ib(repr=False, default=None) -+ -+ state_tree = attr.ib(init=False, hash=False, repr=False) -+ pillar_tree = attr.ib(init=False, hash=False, repr=False) -+ -+ def __attrs_post_init__(self): -+ super().__attrs_post_init__() -+ if self.config.get("open_mode", False) is False: -+ # If the master is not configured to be in open mode, register an auth event callback -+ # If we were passed an auth event callback, it needs to get this master as the first -+ # argument -+ if self.on_auth_event_callback: -+ auth_event_callback = partial(self.on_auth_event_callback, self) -+ else: -+ auth_event_callback = self._on_auth_event -+ self.before_start( -+ self.event_listener.register_auth_event_handler, self.id, auth_event_callback -+ ) -+ self.after_terminate(self.event_listener.unregister_auth_event_handler, self.id) -+ -+ @state_tree.default -+ def __setup_state_tree(self): -+ if "file_roots" in self.config: -+ return SaltStateTree(envs=copy.deepcopy(self.config.get("file_roots") or {})) -+ -+ @pillar_tree.default -+ def __setup_pillar_tree(self): -+ if "pillar_roots" in self.config: -+ return SaltPillarTree(envs=copy.deepcopy(self.config.get("pillar_roots") or {})) -+ -+ @classmethod -+ def default_config( -+ cls, -+ root_dir, -+ master_id, -+ defaults=None, -+ overrides=None, -+ order_masters=False, -+ master_of_masters=None, -+ system_install=False, -+ ): -+ if defaults is None: -+ defaults = {} -+ -+ if overrides is None: -+ overrides = {} -+ else: -+ overrides = overrides.copy() -+ master_of_masters_id = None -+ if master_of_masters: -+ master_of_masters_id = master_of_masters.id -+ overrides["syndic_master"] = master_of_masters.config["interface"] -+ overrides["syndic_master_port"] = master_of_masters.config["ret_port"] -+ # Match transport if not set -+ defaults.setdefault("transport", master_of_masters.config["transport"]) -+ -+ if system_install is True: -+ -+ conf_dir = root_dir / "etc" / "salt" -+ conf_dir.mkdir(parents=True, exist_ok=True) -+ conf_file = str(conf_dir / "master") -+ pki_dir = conf_dir / "pki" / "master" -+ -+ logs_dir = root_dir / "var" / "log" / "salt" -+ logs_dir.mkdir(parents=True, exist_ok=True) -+ -+ pidfile_dir = root_dir / "var" / "run" -+ -+ state_tree_root = root_dir / "srv" / "salt" -+ state_tree_root.mkdir(parents=True, exist_ok=True) -+ pillar_tree_root = root_dir / "srv" / "pillar" -+ pillar_tree_root.mkdir(parents=True, exist_ok=True) -+ -+ _defaults = { -+ "id": master_id, -+ "conf_file": conf_file, -+ "root_dir": str(root_dir), -+ "interface": "127.0.0.1", -+ "publish_port": salt.config.DEFAULT_MASTER_OPTS["publish_port"], -+ "ret_port": salt.config.DEFAULT_MASTER_OPTS["ret_port"], -+ "tcp_master_pub_port": salt.config.DEFAULT_MASTER_OPTS["tcp_master_pub_port"], -+ "tcp_master_pull_port": salt.config.DEFAULT_MASTER_OPTS["tcp_master_pull_port"], -+ "tcp_master_publish_pull": salt.config.DEFAULT_MASTER_OPTS[ -+ "tcp_master_publish_pull" -+ ], -+ "tcp_master_workers": salt.config.DEFAULT_MASTER_OPTS["tcp_master_workers"], -+ "api_pidfile": str(pidfile_dir / "api.pid"), -+ "pki_dir": str(pki_dir), -+ "fileserver_backend": ["roots"], -+ "log_file": str(logs_dir / "master.log"), -+ "log_level_logfile": "debug", -+ "api_logfile": str(logs_dir / "api.log"), -+ "key_logfile": str(logs_dir / "key.log"), -+ "log_fmt_console": "%(asctime)s,%(msecs)03.0f [%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", -+ "log_fmt_logfile": "[%(asctime)s,%(msecs)03.0f][%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", -+ "file_roots": { -+ "base": [str(state_tree_root)], -+ }, -+ "pillar_roots": { -+ "base": [str(pillar_tree_root)], -+ }, -+ "order_masters": order_masters, -+ "max_open_files": 10240, -+ "pytest-master": { -+ "master-id": master_of_masters_id, -+ "log": {"prefix": "{}(id={!r})".format(cls.__name__, master_id)}, -+ }, -+ } -+ else: -+ conf_dir = root_dir / "conf" -+ conf_dir.mkdir(parents=True, exist_ok=True) -+ conf_file = str(conf_dir / "master") -+ state_tree_root = root_dir / "state-tree" -+ state_tree_root.mkdir(exist_ok=True) -+ state_tree_root_base = state_tree_root / "base" -+ state_tree_root_base.mkdir(exist_ok=True) -+ state_tree_root_prod = state_tree_root / "prod" -+ state_tree_root_prod.mkdir(exist_ok=True) -+ pillar_tree_root = root_dir / "pillar-tree" -+ pillar_tree_root.mkdir(exist_ok=True) -+ pillar_tree_root_base = pillar_tree_root / "base" -+ pillar_tree_root_base.mkdir(exist_ok=True) -+ pillar_tree_root_prod = pillar_tree_root / "prod" -+ pillar_tree_root_prod.mkdir(exist_ok=True) -+ -+ _defaults = { -+ "id": master_id, -+ "conf_file": conf_file, -+ "root_dir": str(root_dir), -+ "interface": "127.0.0.1", -+ "publish_port": ports.get_unused_localhost_port(), -+ "ret_port": ports.get_unused_localhost_port(), -+ "tcp_master_pub_port": ports.get_unused_localhost_port(), -+ "tcp_master_pull_port": ports.get_unused_localhost_port(), -+ "tcp_master_publish_pull": ports.get_unused_localhost_port(), -+ "tcp_master_workers": ports.get_unused_localhost_port(), -+ "pidfile": "run/master.pid", -+ "api_pidfile": "run/api.pid", -+ "pki_dir": "pki", -+ "cachedir": "cache", -+ "sock_dir": "run/master", -+ "fileserver_list_cache_time": 0, -+ "fileserver_backend": ["roots"], -+ "pillar_opts": False, -+ "peer": {".*": ["test.*"]}, -+ "log_file": "logs/master.log", -+ "log_level_logfile": "debug", -+ "api_logfile": "logs/api.log", -+ "key_logfile": "logs/key.log", -+ "token_dir": "tokens", -+ "token_file": str(root_dir / "ksfjhdgiuebfgnkefvsikhfjdgvkjahcsidk"), -+ "file_buffer_size": 8192, -+ "log_fmt_console": "%(asctime)s,%(msecs)03.0f [%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", -+ "log_fmt_logfile": "[%(asctime)s,%(msecs)03.0f][%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", -+ "file_roots": { -+ "base": [str(state_tree_root_base)], -+ "prod": [str(state_tree_root_prod)], -+ }, -+ "pillar_roots": { -+ "base": [str(pillar_tree_root_base)], -+ "prod": [str(pillar_tree_root_prod)], -+ }, -+ "order_masters": order_masters, -+ "max_open_files": 10240, -+ "enable_legacy_startup_events": False, -+ "pytest-master": { -+ "master-id": master_of_masters_id, -+ "log": {"prefix": "{}(id={!r})".format(cls.__name__, master_id)}, -+ }, -+ } -+ # Merge in the initial default options with the internal _defaults -+ salt.utils.dictupdate.update(defaults, _defaults, merge_lists=True) -+ -+ if overrides: -+ # Merge in the default options with the master_overrides -+ salt.utils.dictupdate.update(defaults, overrides, merge_lists=True) -+ -+ return defaults -+ -+ @classmethod -+ def _configure( # pylint: disable=arguments-differ -+ cls, -+ factories_manager, -+ daemon_id, -+ master_of_masters=None, -+ root_dir=None, -+ defaults=None, -+ overrides=None, -+ order_masters=False, -+ ): -+ return cls.default_config( -+ root_dir, -+ daemon_id, -+ master_of_masters=master_of_masters, -+ defaults=defaults, -+ overrides=overrides, -+ order_masters=order_masters, -+ system_install=factories_manager.system_install, -+ ) -+ -+ @classmethod -+ def _get_verify_config_entries(cls, config): -+ # verify env to make sure all required directories are created and have the -+ # right permissions -+ pki_dir = pathlib.Path(config["pki_dir"]) -+ verify_env_entries = [ -+ str(pki_dir / "minions"), -+ str(pki_dir / "minions_pre"), -+ str(pki_dir / "minions_rejected"), -+ str(pki_dir / "accepted"), -+ str(pki_dir / "rejected"), -+ str(pki_dir / "pending"), -+ str(pathlib.Path(config["log_file"]).parent), -+ str(pathlib.Path(config["cachedir"]) / "proc"), -+ str(pathlib.Path(config["cachedir"]) / "jobs"), -+ # config['extension_modules'], -+ config["sock_dir"], -+ ] -+ verify_env_entries.extend(config["file_roots"]["base"]) -+ if "prod" in config["file_roots"]: -+ verify_env_entries.extend(config["file_roots"]["prod"]) -+ verify_env_entries.extend(config["pillar_roots"]["base"]) -+ if "prod" in config["pillar_roots"]: -+ verify_env_entries.extend(config["pillar_roots"]["prod"]) -+ return verify_env_entries -+ -+ @classmethod -+ def load_config(cls, config_file, config): -+ return salt.config.master_config(config_file) -+ -+ def _on_auth_event(self, payload): -+ if self.config["open_mode"]: -+ return -+ minion_id = payload["id"] -+ keystate = payload["act"] -+ salt_key_cli = self.salt_key_cli() -+ if keystate == "pend": -+ ret = salt_key_cli.run("--yes", "--accept", minion_id) -+ assert ret.exitcode == 0 -+ -+ def get_check_events(self): -+ """ -+ Return a list of tuples in the form of `(master_id, event_tag)` check against to ensure the daemon is running -+ """ -+ yield self.config["id"], "salt/master/{id}/start".format(**self.config) -+ -+ # The following methods just route the calls to the right method in the factories manager -+ # while making sure the CLI tools are referring to this daemon -+ def salt_master_daemon(self, master_id, **kwargs): -+ """ -+ This method will configure a master under a master-of-masters. -+ -+ Please see the documentation in :py:class:`~saltfactories.manager.FactoriesManager.salt_master_daemon` -+ """ -+ return self.factories_manager.salt_master_daemon( -+ master_id, master_of_masters=self, **kwargs -+ ) -+ -+ def salt_minion_daemon(self, minion_id, **kwargs): -+ """ -+ Please see the documentation in :py:class:`~saltfactories.manager.FactoriesManager.configure_salt_minion` -+ """ -+ return self.factories_manager.salt_minion_daemon(minion_id, master=self, **kwargs) -+ -+ def salt_proxy_minion_daemon(self, minion_id, **kwargs): -+ """ -+ Please see the documentation in :py:class:`~saltfactories.manager.FactoriesManager.salt_proxy_minion_daemon` -+ """ -+ return self.factories_manager.salt_proxy_minion_daemon(minion_id, master=self, **kwargs) -+ -+ def salt_api_daemon(self, **kwargs): -+ """ -+ Please see the documentation in :py:class:`~saltfactories.manager.FactoriesManager.salt_api_daemon` -+ """ -+ return self.factories_manager.salt_api_daemon(self, **kwargs) -+ -+ def salt_syndic_daemon(self, syndic_id, **kwargs): -+ """ -+ Please see the documentation in :py:class:`~saltfactories.manager.FactoriesManager.salt_syndic_daemon` -+ """ -+ return self.factories_manager.salt_syndic_daemon( -+ syndic_id, master_of_masters=self, **kwargs -+ ) -+ -+ def salt_cloud_cli( -+ self, -+ defaults=None, -+ overrides=None, -+ factory_class=cli.cloud.SaltCloud, -+ **factory_class_kwargs -+ ): -+ """ -+ Return a salt-cloud CLI instance -+ -+ Args: -+ defaults(dict): -+ A dictionary of default configuration to use when configuring the minion -+ overrides(dict): -+ A dictionary of configuration overrides to use when configuring the minion -+ -+ Returns: -+ :py:class:`~saltfactories.cli.cloud.SaltCloud`: -+ The salt-cloud CLI script process class instance -+ """ -+ -+ root_dir = pathlib.Path(self.config["root_dir"]) -+ -+ config = factory_class.configure( -+ self, -+ self.id, -+ root_dir=root_dir, -+ defaults=defaults, -+ overrides=overrides, -+ ) -+ self.factories_manager.final_cloud_config_tweaks(config) -+ config = factory_class.write_config(config) -+ -+ if self.system_install is False: -+ script_path = cli_scripts.generate_script( -+ self.factories_manager.scripts_dir, -+ "salt-cloud", -+ code_dir=self.factories_manager.code_dir, -+ inject_coverage=self.factories_manager.inject_coverage, -+ inject_sitecustomize=self.factories_manager.inject_sitecustomize, -+ ) -+ else: -+ script_path = shutil.which("salt-cloud") -+ return factory_class( -+ script_name=script_path, -+ config=config, -+ system_install=self.factories_manager.system_install, -+ **factory_class_kwargs -+ ) -+ -+ def salt_cli(self, factory_class=cli.salt.Salt, **factory_class_kwargs): -+ """ -+ Return a `salt` CLI process for this master instance -+ """ -+ if self.system_install is False: -+ script_path = cli_scripts.generate_script( -+ self.factories_manager.scripts_dir, -+ "salt", -+ code_dir=self.factories_manager.code_dir, -+ inject_coverage=self.factories_manager.inject_coverage, -+ inject_sitecustomize=self.factories_manager.inject_sitecustomize, -+ ) -+ else: -+ script_path = shutil.which("salt") -+ return factory_class( -+ script_name=script_path, -+ config=self.config.copy(), -+ system_install=self.factories_manager.system_install, -+ **factory_class_kwargs -+ ) -+ -+ def salt_cp_cli(self, factory_class=cli.cp.SaltCp, **factory_class_kwargs): -+ """ -+ Return a `salt-cp` CLI process for this master instance -+ """ -+ if self.system_install is False: -+ script_path = cli_scripts.generate_script( -+ self.factories_manager.scripts_dir, -+ "salt-cp", -+ code_dir=self.factories_manager.code_dir, -+ inject_coverage=self.factories_manager.inject_coverage, -+ inject_sitecustomize=self.factories_manager.inject_sitecustomize, -+ ) -+ else: -+ script_path = shutil.which("salt-cp") -+ return factory_class( -+ script_name=script_path, -+ config=self.config.copy(), -+ system_install=self.factories_manager.system_install, -+ **factory_class_kwargs -+ ) -+ -+ def salt_key_cli(self, factory_class=cli.key.SaltKey, **factory_class_kwargs): -+ """ -+ Return a `salt-key` CLI process for this master instance -+ """ -+ if self.system_install is False: -+ script_path = cli_scripts.generate_script( -+ self.factories_manager.scripts_dir, -+ "salt-key", -+ code_dir=self.factories_manager.code_dir, -+ inject_coverage=self.factories_manager.inject_coverage, -+ inject_sitecustomize=self.factories_manager.inject_sitecustomize, -+ ) -+ else: -+ script_path = shutil.which("salt-key") -+ return factory_class( -+ script_name=script_path, -+ config=self.config.copy(), -+ system_install=self.factories_manager.system_install, -+ **factory_class_kwargs -+ ) -+ -+ def salt_run_cli(self, factory_class=cli.run.SaltRun, **factory_class_kwargs): -+ """ -+ Return a `salt-run` CLI process for this master instance -+ """ -+ if self.system_install is False: -+ script_path = cli_scripts.generate_script( -+ self.factories_manager.scripts_dir, -+ "salt-run", -+ code_dir=self.factories_manager.code_dir, -+ inject_coverage=self.factories_manager.inject_coverage, -+ inject_sitecustomize=self.factories_manager.inject_sitecustomize, -+ ) -+ else: -+ script_path = shutil.which("salt-run") -+ return factory_class( -+ script_name=script_path, -+ config=self.config.copy(), -+ system_install=self.factories_manager.system_install, -+ **factory_class_kwargs -+ ) -+ -+ def salt_spm_cli(self, factory_class=cli.spm.Spm, **factory_class_kwargs): -+ """ -+ Return a `spm` CLI process for this master instance -+ """ -+ if self.system_install is False: -+ script_path = cli_scripts.generate_script( -+ self.factories_manager.scripts_dir, -+ "spm", -+ code_dir=self.factories_manager.code_dir, -+ inject_coverage=self.factories_manager.inject_coverage, -+ inject_sitecustomize=self.factories_manager.inject_sitecustomize, -+ ) -+ else: -+ script_path = shutil.which("spm") -+ return factory_class( -+ script_name=script_path, -+ config=self.config.copy(), -+ system_install=self.factories_manager.system_install, -+ **factory_class_kwargs -+ ) -+ -+ def salt_ssh_cli( -+ self, -+ factory_class=cli.ssh.SaltSsh, -+ roster_file=None, -+ target_host=None, -+ client_key=None, -+ ssh_user=None, -+ **factory_class_kwargs -+ ): -+ """ -+ Return a `salt-ssh` CLI process for this master instance -+ -+ Args: -+ roster_file(str): -+ The roster file to use -+ target_host(str): -+ The target host address to connect to -+ client_key(str): -+ The path to the private ssh key to use to connect -+ ssh_user(str): -+ The remote username to connect as -+ """ -+ if self.system_install is False: -+ script_path = cli_scripts.generate_script( -+ self.factories_manager.scripts_dir, -+ "salt-ssh", -+ code_dir=self.factories_manager.code_dir, -+ inject_coverage=self.factories_manager.inject_coverage, -+ inject_sitecustomize=self.factories_manager.inject_sitecustomize, -+ ) -+ else: -+ script_path = shutil.which("salt-ssh") -+ return factory_class( -+ script_name=script_path, -+ config=self.config.copy(), -+ roster_file=roster_file, -+ target_host=target_host, -+ client_key=client_key, -+ ssh_user=ssh_user or running_username(), -+ system_install=self.factories_manager.system_install, -+ **factory_class_kwargs -+ ) -+ -+ def salt_client( -+ self, -+ functions_known_to_return_none=None, -+ factory_class=client.LocalClient, -+ ): -+ """ -+ Return a local salt client object -+ """ -+ return factory_class( -+ master_config=self.config.copy(), -+ functions_known_to_return_none=functions_known_to_return_none, -+ ) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/daemons/minion.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/daemons/minion.py -@@ -0,0 +1,255 @@ -+""" -+.. -+ PYTEST_DONT_REWRITE -+ -+ -+Salt Minion Factory -+""" -+import copy -+import logging -+import pathlib -+import shutil -+ -+import attr -+import salt.config -+import salt.utils.dictupdate -+from pytestskipmarkers.utils import platform -+from pytestskipmarkers.utils import ports -+ -+from saltfactories import cli -+from saltfactories.bases import SaltDaemon -+from saltfactories.utils import cli_scripts -+from saltfactories.utils.tempfiles import SaltPillarTree -+from saltfactories.utils.tempfiles import SaltStateTree -+ -+log = logging.getLogger(__name__) -+ -+ -+@attr.s(kw_only=True, slots=True) -+class SaltMinion(SaltDaemon): -+ -+ state_tree = attr.ib(init=False, hash=False, repr=False) -+ pillar_tree = attr.ib(init=False, hash=False, repr=False) -+ -+ @state_tree.default -+ def __setup_state_tree(self): -+ if "file_roots" in self.config: -+ return SaltStateTree(envs=copy.deepcopy(self.config.get("file_roots") or {})) -+ -+ @pillar_tree.default -+ def __setup_pillar_tree(self): -+ if "pillar_roots" in self.config: -+ return SaltPillarTree(envs=copy.deepcopy(self.config.get("pillar_roots") or {})) -+ -+ @classmethod -+ def default_config( -+ cls, -+ root_dir, -+ minion_id, -+ defaults=None, -+ overrides=None, -+ master=None, -+ system_install=False, -+ ): -+ if defaults is None: -+ defaults = {} -+ -+ master_id = master_port = None -+ if master is not None: -+ master_id = master.id -+ master_port = master.config["ret_port"] -+ # Match transport if not set -+ defaults.setdefault("transport", master.config["transport"]) -+ -+ if system_install is True: -+ -+ conf_dir = root_dir / "etc" / "salt" -+ conf_dir.mkdir(parents=True, exist_ok=True) -+ conf_file = str(conf_dir / "minion") -+ pki_dir = conf_dir / "pki" / "minion" -+ -+ logs_dir = root_dir / "var" / "log" / "salt" -+ logs_dir.mkdir(parents=True, exist_ok=True) -+ -+ state_tree_root = root_dir / "srv" / "salt" -+ state_tree_root.mkdir(parents=True, exist_ok=True) -+ pillar_tree_root = root_dir / "srv" / "pillar" -+ pillar_tree_root.mkdir(parents=True, exist_ok=True) -+ -+ _defaults = { -+ "id": master_id, -+ "conf_file": conf_file, -+ "root_dir": str(root_dir), -+ "interface": "127.0.0.1", -+ "master": "127.0.0.1", -+ "master_port": master_port or salt.config.DEFAULT_MINION_OPTS["master_port"], -+ "tcp_pub_port": salt.config.DEFAULT_MINION_OPTS["tcp_pub_port"], -+ "tcp_pull_port": salt.config.DEFAULT_MINION_OPTS["tcp_pull_port"], -+ "pki_dir": str(pki_dir), -+ "log_file": str(logs_dir / "minion.log"), -+ "log_level_logfile": "debug", -+ "log_fmt_console": "%(asctime)s,%(msecs)03.0f [%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", -+ "log_fmt_logfile": "[%(asctime)s,%(msecs)03.0f][%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", -+ "file_roots": { -+ "base": [str(state_tree_root)], -+ }, -+ "pillar_roots": { -+ "base": [str(pillar_tree_root)], -+ }, -+ "pytest-minion": { -+ "master-id": master_id, -+ "log": {"prefix": "{}(id={!r})".format(cls.__name__, minion_id)}, -+ }, -+ } -+ else: -+ conf_dir = root_dir / "conf" -+ conf_dir.mkdir(parents=True, exist_ok=True) -+ conf_file = str(conf_dir / "minion") -+ -+ state_tree_root = root_dir / "state-tree" -+ state_tree_root.mkdir(exist_ok=True) -+ state_tree_root_base = state_tree_root / "base" -+ state_tree_root_base.mkdir(exist_ok=True) -+ state_tree_root_prod = state_tree_root / "prod" -+ state_tree_root_prod.mkdir(exist_ok=True) -+ pillar_tree_root = root_dir / "pillar-tree" -+ pillar_tree_root.mkdir(exist_ok=True) -+ pillar_tree_root_base = pillar_tree_root / "base" -+ pillar_tree_root_base.mkdir(exist_ok=True) -+ pillar_tree_root_prod = pillar_tree_root / "prod" -+ pillar_tree_root_prod.mkdir(exist_ok=True) -+ -+ _defaults = { -+ "id": minion_id, -+ "conf_file": conf_file, -+ "root_dir": str(root_dir), -+ "interface": "127.0.0.1", -+ "master": "127.0.0.1", -+ "master_port": master_port or ports.get_unused_localhost_port(), -+ "tcp_pub_port": ports.get_unused_localhost_port(), -+ "tcp_pull_port": ports.get_unused_localhost_port(), -+ "pidfile": "run/minion.pid", -+ "pki_dir": "pki", -+ "cachedir": "cache", -+ "sock_dir": "run/minion", -+ "log_file": "logs/minion.log", -+ "log_level_logfile": "debug", -+ "loop_interval": 0.05, -+ "log_fmt_console": "%(asctime)s,%(msecs)03.0f [%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", -+ "log_fmt_logfile": "[%(asctime)s,%(msecs)03.0f][%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", -+ "file_roots": { -+ "base": [str(state_tree_root_base)], -+ "prod": [str(state_tree_root_prod)], -+ }, -+ "pillar_roots": { -+ "base": [str(pillar_tree_root_base)], -+ "prod": [str(pillar_tree_root_prod)], -+ }, -+ "enable_legacy_startup_events": False, -+ "acceptance_wait_time": 0.5, -+ "acceptance_wait_time_max": 5, -+ "pytest-minion": { -+ "master-id": master_id, -+ "log": {"prefix": "{}(id={!r})".format(cls.__name__, minion_id)}, -+ }, -+ } -+ # Merge in the initial default options with the internal _defaults -+ salt.utils.dictupdate.update(defaults, _defaults, merge_lists=True) -+ -+ if overrides: -+ # Merge in the default options with the minion_overrides -+ salt.utils.dictupdate.update(defaults, overrides, merge_lists=True) -+ -+ return defaults -+ -+ @classmethod -+ def _configure( # pylint: disable=arguments-differ -+ cls, -+ factories_manager, -+ daemon_id, -+ root_dir=None, -+ defaults=None, -+ overrides=None, -+ master=None, -+ ): -+ return cls.default_config( -+ root_dir, -+ daemon_id, -+ defaults=defaults, -+ overrides=overrides, -+ master=master, -+ system_install=factories_manager.system_install, -+ ) -+ -+ @classmethod -+ def _get_verify_config_entries(cls, config): -+ # verify env to make sure all required directories are created and have the -+ # right permissions -+ pki_dir = pathlib.Path(config["pki_dir"]) -+ verify_env_entries = [ -+ str(pki_dir / "minions"), -+ str(pki_dir / "minions_pre"), -+ str(pki_dir / "minions_rejected"), -+ str(pki_dir / "accepted"), -+ str(pki_dir / "rejected"), -+ str(pki_dir / "pending"), -+ str(pathlib.Path(config["log_file"]).parent), -+ str(pathlib.Path(config["cachedir"]) / "proc"), -+ # config['extension_modules'], -+ config["sock_dir"], -+ ] -+ verify_env_entries.extend(config["file_roots"]["base"]) -+ if "prod" in config["file_roots"]: -+ verify_env_entries.extend(config["file_roots"]["prod"]) -+ verify_env_entries.extend(config["pillar_roots"]["base"]) -+ if "prod" in config["pillar_roots"]: -+ verify_env_entries.extend(config["pillar_roots"]["prod"]) -+ return verify_env_entries -+ -+ @classmethod -+ def load_config(cls, config_file, config): -+ return salt.config.minion_config(config_file, minion_id=config["id"], cache_minion_id=True) -+ -+ def get_script_args(self): -+ args = super().get_script_args() -+ if platform.is_windows() is False: -+ args.append("--disable-keepalive") -+ return args -+ -+ def get_check_events(self): -+ """ -+ Return a list of tuples in the form of `(master_id, event_tag)` check against to ensure the daemon is running -+ """ -+ pytest_config = self.config["pytest-{}".format(self.config["__role"])] -+ if not pytest_config.get("master-id"): -+ log.warning( -+ "Will not be able to check for start events for %s since it's missing the 'master-id' key " -+ "in the 'pytest-%s' dictionary, or it's value is None.", -+ self, -+ self.config["__role"], -+ ) -+ else: -+ yield pytest_config["master-id"], "salt/{role}/{id}/start".format( -+ role=self.config["__role"], id=self.id -+ ) -+ -+ def salt_call_cli(self, factory_class=cli.call.SaltCall, **factory_class_kwargs): -+ """ -+ Return a `salt-call` CLI process for this minion instance -+ """ -+ if self.system_install is False: -+ script_path = cli_scripts.generate_script( -+ self.factories_manager.scripts_dir, -+ "salt-call", -+ code_dir=self.factories_manager.code_dir, -+ inject_coverage=self.factories_manager.inject_coverage, -+ inject_sitecustomize=self.factories_manager.inject_sitecustomize, -+ ) -+ else: -+ script_path = shutil.which("salt-call") -+ return factory_class( -+ script_name=script_path, -+ config=self.config.copy(), -+ system_install=self.factories_manager.system_install, -+ **factory_class_kwargs -+ ) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/daemons/proxy.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/daemons/proxy.py -@@ -0,0 +1,276 @@ -+""" -+.. -+ PYTEST_DONT_REWRITE -+ -+ -+Salt Proxy Minion Factory -+""" -+import copy -+import logging -+import pathlib -+import shutil -+ -+import attr -+import salt.config -+import salt.utils.dictupdate -+from pytestskipmarkers.utils import platform -+from pytestskipmarkers.utils import ports -+ -+from saltfactories import cli -+from saltfactories.bases import SaltDaemon -+from saltfactories.bases import SystemdSaltDaemonImpl -+from saltfactories.utils import cli_scripts -+from saltfactories.utils.tempfiles import SaltPillarTree -+from saltfactories.utils.tempfiles import SaltStateTree -+ -+log = logging.getLogger(__name__) -+ -+ -+class SystemdSaltProxyImpl(SystemdSaltDaemonImpl): -+ def get_service_name(self): -+ if self._service_name is None: -+ self._service_name = "{}@{}".format(super().get_service_name(), self.factory.id) -+ return self._service_name -+ -+ -+@attr.s(kw_only=True, slots=True) -+class SaltProxyMinion(SaltDaemon): -+ -+ include_proxyid_cli_flag = attr.ib(default=True, repr=False) -+ -+ state_tree = attr.ib(init=False, hash=False, repr=False) -+ pillar_tree = attr.ib(init=False, hash=False, repr=False) -+ -+ def _get_impl_class(self): -+ if self.system_install: -+ return SystemdSaltProxyImpl -+ return super()._get_impl_class() -+ -+ @state_tree.default -+ def __setup_state_tree(self): -+ if "file_roots" in self.config: -+ return SaltStateTree(envs=copy.deepcopy(self.config.get("file_roots") or {})) -+ -+ @pillar_tree.default -+ def __setup_pillar_tree(self): -+ if "pillar_roots" in self.config: -+ return SaltPillarTree(envs=copy.deepcopy(self.config.get("pillar_roots") or {})) -+ -+ @classmethod -+ def default_config( -+ cls, -+ root_dir, -+ proxy_minion_id, -+ defaults=None, -+ overrides=None, -+ master=None, -+ system_install=False, -+ ): -+ if defaults is None: -+ defaults = {} -+ -+ master_id = master_port = None -+ if master is not None: -+ master_id = master.id -+ master_port = master.config["ret_port"] -+ # Match transport if not set -+ defaults.setdefault("transport", master.config["transport"]) -+ -+ if system_install is True: -+ conf_dir = root_dir / "etc" / "salt" -+ conf_dir.mkdir(parents=True, exist_ok=True) -+ conf_file = str(conf_dir / "proxy") -+ pki_dir = conf_dir / "pki" / "minion" -+ -+ logs_dir = root_dir / "var" / "log" / "salt" -+ logs_dir.mkdir(parents=True, exist_ok=True) -+ -+ state_tree_root = root_dir / "srv" / "salt" -+ state_tree_root.mkdir(parents=True, exist_ok=True) -+ pillar_tree_root = root_dir / "srv" / "pillar" -+ pillar_tree_root.mkdir(parents=True, exist_ok=True) -+ -+ _defaults = { -+ "id": proxy_minion_id, -+ "conf_file": conf_file, -+ "root_dir": str(root_dir), -+ "interface": "127.0.0.1", -+ "master": "127.0.0.1", -+ "master_port": master_port or salt.config.DEFAULT_MINION_OPTS["master_port"], -+ "tcp_pub_port": salt.config.DEFAULT_MINION_OPTS["tcp_pub_port"], -+ "tcp_pull_port": salt.config.DEFAULT_MINION_OPTS["tcp_pull_port"], -+ "pki_dir": str(pki_dir), -+ "log_file": str(logs_dir / "proxy.log"), -+ "log_level_logfile": "debug", -+ "loop_interval": 0.05, -+ "log_fmt_console": "%(asctime)s,%(msecs)03.0f [%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", -+ "log_fmt_logfile": "[%(asctime)s,%(msecs)03.0f][%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", -+ "file_roots": { -+ "base": [str(state_tree_root)], -+ }, -+ "pillar_roots": { -+ "base": [str(pillar_tree_root)], -+ }, -+ "proxy": {"proxytype": "dummy"}, -+ "pytest-minion": { -+ "master-id": master_id, -+ "log": {"prefix": "{}(id={!r})".format(cls.__name__, proxy_minion_id)}, -+ }, -+ } -+ else: -+ conf_dir = root_dir / "conf" -+ conf_dir.mkdir(parents=True, exist_ok=True) -+ conf_file = str(conf_dir / "proxy") -+ -+ state_tree_root = root_dir / "state-tree" -+ state_tree_root.mkdir(exist_ok=True) -+ state_tree_root_base = state_tree_root / "base" -+ state_tree_root_base.mkdir(exist_ok=True) -+ state_tree_root_prod = state_tree_root / "prod" -+ state_tree_root_prod.mkdir(exist_ok=True) -+ pillar_tree_root = root_dir / "pillar-tree" -+ pillar_tree_root.mkdir(exist_ok=True) -+ pillar_tree_root_base = pillar_tree_root / "base" -+ pillar_tree_root_base.mkdir(exist_ok=True) -+ pillar_tree_root_prod = pillar_tree_root / "prod" -+ pillar_tree_root_prod.mkdir(exist_ok=True) -+ -+ _defaults = { -+ "id": proxy_minion_id, -+ "conf_file": conf_file, -+ "root_dir": str(root_dir), -+ "interface": "127.0.0.1", -+ "master": "127.0.0.1", -+ "master_port": master_port or ports.get_unused_localhost_port(), -+ "tcp_pub_port": ports.get_unused_localhost_port(), -+ "tcp_pull_port": ports.get_unused_localhost_port(), -+ "pidfile": "run/proxy.pid", -+ "pki_dir": "pki", -+ "cachedir": "cache", -+ "sock_dir": "run/proxy", -+ "log_file": "logs/proxy.log", -+ "log_level_logfile": "debug", -+ "loop_interval": 0.05, -+ "log_fmt_console": "%(asctime)s,%(msecs)03.0f [%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", -+ "log_fmt_logfile": "[%(asctime)s,%(msecs)03.0f][%(name)-17s:%(lineno)-4d][%(levelname)-8s][%(processName)18s(%(process)d)] %(message)s", -+ "file_roots": { -+ "base": [str(state_tree_root_base)], -+ "prod": [str(state_tree_root_prod)], -+ }, -+ "pillar_roots": { -+ "base": [str(pillar_tree_root_base)], -+ "prod": [str(pillar_tree_root_prod)], -+ }, -+ "proxy": {"proxytype": "dummy"}, -+ "enable_legacy_startup_events": False, -+ "acceptance_wait_time": 0.5, -+ "acceptance_wait_time_max": 5, -+ "pytest-minion": { -+ "master-id": master_id, -+ "log": {"prefix": "{}(id={!r})".format(cls.__name__, proxy_minion_id)}, -+ }, -+ } -+ # Merge in the initial default options with the internal _defaults -+ salt.utils.dictupdate.update(defaults, _defaults, merge_lists=True) -+ -+ if overrides: -+ # Merge in the default options with the proxy_overrides -+ salt.utils.dictupdate.update(defaults, overrides, merge_lists=True) -+ -+ return defaults -+ -+ @classmethod -+ def _configure( # pylint: disable=arguments-differ -+ cls, -+ factories_manager, -+ daemon_id, -+ root_dir=None, -+ defaults=None, -+ overrides=None, -+ master=None, -+ ): -+ return cls.default_config( -+ root_dir, -+ daemon_id, -+ defaults=defaults, -+ overrides=overrides, -+ master=master, -+ system_install=factories_manager.system_install, -+ ) -+ -+ @classmethod -+ def _get_verify_config_entries(cls, config): -+ # verify env to make sure all required directories are created and have the -+ # right permissions -+ verify_env_entries = [ -+ str(pathlib.Path(config["log_file"]).parent), -+ # config['extension_modules'], -+ config["sock_dir"], -+ ] -+ verify_env_entries.extend(config["file_roots"]["base"]) -+ if "prod" in config["file_roots"]: -+ verify_env_entries.extend(config["file_roots"]["prod"]) -+ verify_env_entries.extend(config["pillar_roots"]["base"]) -+ if "prod" in config["pillar_roots"]: -+ verify_env_entries.extend(config["pillar_roots"]["prod"]) -+ return verify_env_entries -+ -+ @classmethod -+ def load_config(cls, config_file, config): -+ return salt.config.proxy_config(config_file, minion_id=config["id"], cache_minion_id=True) -+ -+ def get_base_script_args(self): -+ script_args = super().get_base_script_args() -+ if platform.is_windows() is False: -+ script_args.append("--disable-keepalive") -+ return script_args -+ -+ def cmdline(self, *args): -+ if self.include_proxyid_cli_flag is False: -+ return super().cmdline(*args) -+ _args = [] -+ for arg in args: -+ if arg.startswith("--proxyid"): -+ break -+ else: -+ _args.append("--proxyid={}".format(self.id)) -+ return super().cmdline(*(_args + list(args))) -+ -+ def get_check_events(self): -+ """ -+ Return a list of tuples in the form of `(master_id, event_tag)` check against to ensure the daemon is running -+ """ -+ pytest_config = self.config["pytest-{}".format(self.config["__role"])] -+ if not pytest_config.get("master-id"): -+ log.warning( -+ "Will not be able to check for start events for %s since it's missing the 'master-id' key " -+ "in the 'pytest-%s' dictionary, or it's value is None.", -+ self, -+ self.config["__role"], -+ ) -+ else: -+ yield pytest_config["master-id"], "salt/{role}/{id}/start".format( -+ role=self.config["__role"], id=self.id -+ ) -+ -+ def salt_call_cli(self, factory_class=cli.call.SaltCall, **factory_class_kwargs): -+ """ -+ Return a `salt-call` CLI process for this minion instance -+ """ -+ if self.system_install is False: -+ script_path = cli_scripts.generate_script( -+ self.factories_manager.scripts_dir, -+ "salt-call", -+ code_dir=self.factories_manager.code_dir, -+ inject_coverage=self.factories_manager.inject_coverage, -+ inject_sitecustomize=self.factories_manager.inject_sitecustomize, -+ ) -+ else: -+ script_path = shutil.which("salt-call") -+ return factory_class( -+ script_name=script_path, -+ config=self.config.copy(), -+ base_script_args=["--proxyid={}".format(self.id)], -+ system_install=self.factories_manager.system_install, -+ **factory_class_kwargs -+ ) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/daemons/sshd.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/daemons/sshd.py -@@ -0,0 +1,204 @@ -+""" -+SSHD daemon factory implementation -+""" -+import logging -+import pathlib -+import shutil -+import subprocess -+from datetime import datetime -+ -+import attr -+from pytestskipmarkers.utils import platform -+from pytestskipmarkers.utils import ports -+ -+from saltfactories.bases import Daemon -+from saltfactories.exceptions import FactoryFailure -+from saltfactories.utils import running_username -+from saltfactories.utils import socket -+ -+log = logging.getLogger(__name__) -+ -+ -+@attr.s(kw_only=True, slots=True) -+class Sshd(Daemon): -+ config_dir = attr.ib() -+ listen_address = attr.ib(default=None) -+ listen_port = attr.ib(default=None) -+ authorized_keys = attr.ib(default=None) -+ sshd_config_dict = attr.ib(default=None, repr=False) -+ client_key = attr.ib(default=None, init=False, repr=False) -+ sshd_config = attr.ib(default=None, init=False) -+ -+ def __attrs_post_init__(self): -+ if self.authorized_keys is None: -+ self.authorized_keys = [] -+ if self.sshd_config_dict is None: -+ self.sshd_config_dict = {} -+ if self.listen_address is None: -+ self.listen_address = "127.0.0.1" -+ if self.listen_port is None: -+ self.listen_port = ports.get_unused_localhost_port() -+ self.check_ports = [self.listen_port] -+ if isinstance(self.config_dir, str): -+ self.config_dir = pathlib.Path(self.config_dir) -+ elif not isinstance(self.config_dir, pathlib.Path): -+ # A py local path? -+ self.config_dir = pathlib.Path(self.config_dir.strpath) -+ self.config_dir.chmod(0o0700) -+ authorized_keys_file = self.config_dir / "authorized_keys" -+ -+ # Let's generate the client key -+ self.client_key = self._generate_client_ecdsa_key() -+ with open("{}.pub".format(self.client_key)) as rfh: -+ pubkey = rfh.read().strip() -+ log.debug("SSH client pub key: %r", pubkey) -+ self.authorized_keys.append(pubkey) -+ -+ # Write the authorized pub keys to file -+ with open(str(authorized_keys_file), "w") as wfh: -+ wfh.write("\n".join(self.authorized_keys)) -+ -+ authorized_keys_file.chmod(0o0600) -+ -+ with open(str(authorized_keys_file)) as rfh: -+ log.debug("AuthorizedKeysFile contents:\n%s", rfh.read()) -+ -+ _default_config = { -+ "ListenAddress": self.listen_address, -+ "PermitRootLogin": "yes" if running_username() == "root" else "no", -+ "ChallengeResponseAuthentication": "no", -+ "PasswordAuthentication": "no", -+ "PubkeyAuthentication": "yes", -+ "PrintMotd": "no", -+ "PidFile": self.config_dir / "sshd.pid", -+ "AuthorizedKeysFile": authorized_keys_file, -+ } -+ if self.sshd_config_dict: -+ _default_config.update(self.sshd_config_dict) -+ self.sshd_config = _default_config -+ self._write_config() -+ super().__attrs_post_init__() -+ -+ def get_base_script_args(self): -+ """ -+ Returns any additional arguments to pass to the CLI script -+ """ -+ return ["-D", "-e", "-f", str(self.config_dir / "sshd_config"), "-p", str(self.listen_port)] -+ -+ def _write_config(self): -+ sshd_config_file = self.config_dir / "sshd_config" -+ if not sshd_config_file.exists(): -+ # Let's write a default config file -+ config_lines = [] -+ for key, value in self.sshd_config.items(): -+ if isinstance(value, list): -+ for item in value: -+ config_lines.append("{} {}\n".format(key, item)) -+ continue -+ config_lines.append("{} {}\n".format(key, value)) -+ -+ # Let's generate the host keys -+ if platform.is_fips_enabled() is False: -+ self._generate_server_dsa_key() -+ self._generate_server_ecdsa_key() -+ self._generate_server_ed25519_key() -+ for host_key in pathlib.Path(self.config_dir).glob("ssh_host_*_key"): -+ config_lines.append("HostKey {}\n".format(host_key)) -+ -+ with open(str(sshd_config_file), "w") as wfh: -+ wfh.write("".join(sorted(config_lines))) -+ sshd_config_file.chmod(0o0600) -+ with open(str(sshd_config_file)) as wfh: -+ log.debug( -+ "Wrote to configuration file %s. Configuration:\n%s", -+ sshd_config_file, -+ wfh.read(), -+ ) -+ -+ def _generate_client_ecdsa_key(self): -+ key_filename = "client_key" -+ key_path_prv = self.config_dir / key_filename -+ key_path_pub = self.config_dir / "{}.pub".format(key_filename) -+ if key_path_prv.exists() and key_path_pub.exists(): -+ return key_path_prv -+ self._ssh_keygen(key_filename, "ecdsa", "521") -+ for key_path in (key_path_prv, key_path_pub): -+ key_path.chmod(0o0400) -+ return key_path_prv -+ -+ def _generate_server_dsa_key(self): -+ key_filename = "ssh_host_dsa_key" -+ key_path_prv = self.config_dir / key_filename -+ key_path_pub = self.config_dir / "{}.pub".format(key_filename) -+ if key_path_prv.exists() and key_path_pub.exists(): -+ return key_path_prv -+ self._ssh_keygen(key_filename, "dsa", "1024") -+ for key_path in (key_path_prv, key_path_pub): -+ key_path.chmod(0o0400) -+ return key_path_prv -+ -+ def _generate_server_ecdsa_key(self): -+ key_filename = "ssh_host_ecdsa_key" -+ key_path_prv = self.config_dir / key_filename -+ key_path_pub = self.config_dir / "{}.pub".format(key_filename) -+ if key_path_prv.exists() and key_path_pub.exists(): -+ return key_path_prv -+ self._ssh_keygen(key_filename, "ecdsa", "521") -+ for key_path in (key_path_prv, key_path_pub): -+ key_path.chmod(0o0400) -+ return key_path_prv -+ -+ def _generate_server_ed25519_key(self): -+ key_filename = "ssh_host_ed25519_key" -+ key_path_prv = self.config_dir / key_filename -+ key_path_pub = self.config_dir / "{}.pub".format(key_filename) -+ if key_path_prv.exists() and key_path_pub.exists(): -+ return key_path_prv -+ self._ssh_keygen(key_filename, "ed25519", "521") -+ for key_path in (key_path_prv, key_path_pub): -+ key_path.chmod(0o0400) -+ return key_path_prv -+ -+ def _ssh_keygen(self, key_filename, key_type, bits, comment=None): -+ try: -+ ssh_keygen = self._ssh_keygen_path -+ except AttributeError: -+ ssh_keygen = self._ssh_keygen_path = shutil.which("ssh-keygen") -+ -+ if comment is None: -+ comment = "{user}@{host}-{date}".format( -+ user=running_username(), -+ host=socket.gethostname(), -+ date=datetime.utcnow().strftime("%Y-%m-%d"), -+ ) -+ -+ cmdline = [ -+ ssh_keygen, -+ "-t", -+ key_type, -+ "-b", -+ bits, -+ "-C", -+ comment, -+ "-f", -+ key_filename, -+ "-P", -+ "", -+ ] -+ try: -+ subprocess.run( -+ cmdline, -+ cwd=str(self.config_dir), -+ check=True, -+ universal_newlines=True, -+ stdout=subprocess.PIPE, -+ stderr=subprocess.PIPE, -+ ) -+ except subprocess.CalledProcessError as exc: -+ raise FactoryFailure( -+ "Failed to generate ssh key.", -+ cmdline=exc.args, -+ stdout=exc.stdout, -+ stderr=exc.stderr, -+ exitcode=exc.returncode, -+ ) from exc ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/daemons/syndic.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/daemons/syndic.py -@@ -0,0 +1,160 @@ -+""" -+.. -+ PYTEST_DONT_REWRITE -+ -+ -+Salt Syndic Factory -+""" -+import logging -+import pathlib -+ -+import attr -+import salt.config -+import salt.utils.dictupdate -+ -+from saltfactories.bases import SaltDaemon -+from saltfactories.utils import ports -+ -+log = logging.getLogger(__name__) -+ -+ -+@attr.s(kw_only=True, slots=True) -+class SaltSyndic(SaltDaemon): -+ -+ master = attr.ib(repr=False, hash=False) -+ minion = attr.ib(repr=False, hash=False) -+ -+ @classmethod -+ def default_config( -+ cls, -+ root_dir, -+ syndic_id, -+ defaults=None, -+ overrides=None, -+ master_of_masters=None, -+ system_install=False, -+ ): -+ if defaults is None: -+ defaults = {} -+ -+ if overrides is None: -+ overrides = {} -+ -+ master_of_masters_id = syndic_master_port = None -+ if master_of_masters: -+ master_of_masters_id = master_of_masters.id -+ syndic_master_port = master_of_masters.config["ret_port"] -+ # Match transport if not set -+ defaults.setdefault("transport", master_of_masters.config["transport"]) -+ -+ if system_install is True: -+ conf_dir = root_dir / "etc" / "salt" -+ conf_dir.mkdir(parents=True, exist_ok=True) -+ conf_d_dir = conf_dir / "master.d" -+ conf_d_dir.mkdir(exist_ok=True) -+ conf_file = str(conf_d_dir / "syndic.conf") -+ -+ pidfile_dir = root_dir / "var" / "run" -+ -+ logs_dir = root_dir / "var" / "log" / "salt" -+ logs_dir.mkdir(parents=True, exist_ok=True) -+ -+ _defaults = { -+ "id": syndic_id, -+ "master_id": syndic_id, -+ "conf_file": conf_file, -+ "root_dir": str(root_dir), -+ "syndic_master": "127.0.0.1", -+ "syndic_master_port": syndic_master_port -+ or salt.config.DEFAULT_MASTER_OPTS["ret_port"], -+ "syndic_pidfile": str(pidfile_dir / "syndic.pid"), -+ "syndic_log_file": str(logs_dir / "syndic.log"), -+ "syndic_log_level_logfile": "debug", -+ "pytest-syndic": { -+ "master-id": master_of_masters_id, -+ "log": {"prefix": "{}(id={!r})".format(cls.__name__, syndic_id)}, -+ }, -+ } -+ else: -+ conf_dir = root_dir / "conf" -+ conf_dir.mkdir(parents=True, exist_ok=True) -+ conf_d_dir = conf_dir / "master.d" -+ conf_d_dir.mkdir(exist_ok=True) -+ conf_file = str(conf_d_dir / "syndic.conf") -+ -+ _defaults = { -+ "id": syndic_id, -+ "master_id": syndic_id, -+ "conf_file": conf_file, -+ "root_dir": str(root_dir), -+ "syndic_master": "127.0.0.1", -+ "syndic_master_port": syndic_master_port or ports.get_unused_localhost_port(), -+ "syndic_pidfile": "run/syndic.pid", -+ "syndic_log_file": "logs/syndic.log", -+ "syndic_log_level_logfile": "debug", -+ "syndic_dir": "cache/syndics", -+ "enable_legacy_startup_events": False, -+ "pytest-syndic": { -+ "master-id": master_of_masters_id, -+ "log": {"prefix": "{}(id={!r})".format(cls.__name__, syndic_id)}, -+ }, -+ } -+ # Merge in the initial default options with the internal _defaults -+ salt.utils.dictupdate.update(defaults, _defaults, merge_lists=True) -+ -+ if overrides: -+ # Merge in the default options with the syndic_overrides -+ salt.utils.dictupdate.update(defaults, overrides, merge_lists=True) -+ return defaults -+ -+ @classmethod -+ def _configure( # pylint: disable=arguments-differ -+ cls, -+ factories_manager, -+ daemon_id, -+ root_dir=None, -+ defaults=None, -+ overrides=None, -+ master_of_masters=None, -+ ): -+ return cls.default_config( -+ root_dir, -+ daemon_id, -+ defaults=defaults, -+ overrides=overrides, -+ master_of_masters=master_of_masters, -+ system_install=factories_manager.system_install, -+ ) -+ -+ @classmethod -+ def _get_verify_config_entries(cls, config): -+ # verify env to make sure all required directories are created and have the -+ # right permissions -+ verify_env_entries = [ -+ str(pathlib.Path(config["syndic_log_file"]).parent), -+ ] -+ return verify_env_entries -+ -+ @classmethod -+ def load_config(cls, config_file, config): -+ conf_dir = pathlib.Path(config_file).parent.parent -+ master_config_file = str(conf_dir / "master") -+ minion_config_file = str(conf_dir / "minion") -+ return salt.config.syndic_config(master_config_file, minion_config_file) -+ -+ def get_check_events(self): -+ """ -+ Return a list of tuples in the form of `(master_id, event_tag)` check against to ensure the daemon is running -+ """ -+ pytest_config = self.config["pytest-{}".format(self.config["__role"])] -+ if not pytest_config.get("master-id"): -+ log.warning( -+ "Will not be able to check for start events for %s since it's missing the 'master-id' key " -+ "in the 'pytest-%s' dictionary, or it's value is None.", -+ self, -+ self.config["__role"], -+ ) -+ else: -+ yield pytest_config["master-id"], "salt/{role}/{id}/start".format( -+ role=self.config["__role"], id=self.id -+ ) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/exceptions.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/exceptions.py -@@ -0,0 +1,103 @@ -+""" -+PyTest Salt Factories related exceptions -+""" -+import traceback -+ -+ -+class SaltFactoriesException(Exception): -+ """ -+ Base exception for all pytest salt factories -+ """ -+ -+ -+class ProcessFailed(SaltFactoriesException): -+ """ -+ Exception raised when a sub-process fails -+ -+ :param str message: -+ The exception message -+ :keyword list,tuple cmdline: -+ The command line used to start the process -+ :keyword str stdout: -+ The ``stdout`` returned by the process -+ :keyword str stderr: -+ The ``stderr`` returned by the process -+ :keyword int exitcode: -+ The exitcode returned by the process -+ :keyword Exception exc: -+ The original exception raised -+ """ -+ -+ def __init__(self, message, cmdline=None, stdout=None, stderr=None, exitcode=None, exc=None): -+ super().__init__() -+ self.message = message -+ self.cmdline = cmdline -+ self.stdout = stdout -+ self.stderr = stderr -+ self.exitcode = exitcode -+ self.exc = exc -+ -+ def __str__(self): -+ message = self.message -+ append_new_line = False -+ if self.cmdline: -+ message += "\n Command Line: {}".format(self.cmdline) -+ append_new_line = True -+ if self.exitcode is not None: -+ append_new_line = True -+ message += "\n Exitcode: {}".format(self.exitcode) -+ if self.stdout or self.stderr: -+ append_new_line = True -+ message += "\n Process Output:" -+ if self.stdout: -+ message += "\n >>>>> STDOUT >>>>>\n{}\n <<<<< STDOUT <<<<<".format(self.stdout) -+ if self.stderr: -+ message += "\n >>>>> STDERR >>>>>\n{}\n <<<<< STDERR <<<<<".format(self.stderr) -+ if self.exc: -+ append_new_line = True -+ message += "\n{}".format("".join(traceback.format_exception(*self.exc)).rstrip()) -+ if append_new_line: -+ message += "\n" -+ return message -+ -+ -+class FactoryFailure(ProcessFailed): -+ """ -+ Exception raised when a sub-process fails on one of the factories -+ """ -+ -+ -+class FactoryNotStarted(FactoryFailure): -+ """ -+ Exception raised when a factory failed to start -+ -+ Please look at :py:class:`~saltfactories.exceptions.FactoryFailure` for the supported keyword -+ arguments documentation. -+ """ -+ -+ -+class FactoryNotRunning(FactoryFailure): -+ """ -+ Exception raised when trying to use a factory's `.stopped` context manager and the factory is not running -+ -+ Please look at :py:class:`~saltfactories.exceptions.FactoryFailure` for the supported keyword -+ arguments documentation. -+ """ -+ -+ -+class ProcessNotStarted(FactoryFailure): -+ """ -+ Exception raised when a process failed to start -+ -+ Please look at :py:class:`~saltfactories.exceptions.FactoryFailure` for the supported keyword -+ arguments documentation. -+ """ -+ -+ -+class FactoryTimeout(FactoryNotStarted): -+ """ -+ Exception raised when a process timed-out -+ -+ Please look at :py:class:`~saltfactories.exceptions.FactoryFailure` for the supported keyword -+ arguments documentation. -+ """ ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/manager.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/manager.py -@@ -0,0 +1,665 @@ -+""" -+.. -+ PYTEST_DONT_REWRITE -+ -+Salt Factories Manager -+""" -+import logging -+import pathlib -+import sys -+ -+import attr -+ -+from saltfactories import CODE_ROOT_DIR -+from saltfactories import daemons -+from saltfactories.bases import Salt -+from saltfactories.utils import cli_scripts -+from saltfactories.utils import running_username -+ -+log = logging.getLogger(__name__) -+ -+ -+@attr.s(kw_only=True, slots=True) -+class FactoriesManager: -+ """ -+ The :class:`FactoriesManager` is responsible for configuring and spawning Salt Daemons and -+ making sure that any salt CLI tools are "targeted" to the right daemon. -+ -+ It also keeps track of which daemons were started and adds their termination routines to PyTest's -+ request finalization routines. -+ -+ If process statistics are enabled, it also adds the started daemons to those statistics. -+ -+ :keyword pathlib.Path, str root_dir: -+ :keyword int log_server_port: -+ The port the log server should listen at -+ :keyword int log_server_level: -+ The level of the log server -+ :keyword str log_server_host: -+ The hostname/ip address of the host running the logs server. Defaults to "localhost". -+ :keyword str code_dir: -+ The path to the code root directory of the project being tested. This is important for proper -+ code-coverage paths. -+ :keyword bool inject_coverage: -+ Inject code-coverage related code in the generated CLI scripts -+ :keyword bool inject_sitecustomize: -+ Inject code in the generated CLI scripts in order for our `sitecustomise.py` to be loaded by -+ subprocesses. -+ :keyword str cwd: -+ The path to the current working directory -+ :keyword dict environ: -+ A dictionary of `key`, `value` pairs to add to the environment. -+ :keyword bool slow_stop: -+ Whether to terminate the processes by sending a :py:attr:`SIGTERM` signal or by calling -+ :py:meth:`~subprocess.Popen.terminate` on the sub-process. -+ When code coverage is enabled, one will want `slow_stop` set to `True` so that coverage data -+ can be written down to disk. -+ :keyword int start_timeout: -+ The amount of time, in seconds, to wait, until a subprocess is considered as not started. -+ :type stats_processes: saltfactories.plugins.sysstats.StatsProcesses -+ :keyword stats_processes: -+ This will be an `StatsProcesses` class instantiated on the :py:func:`~_pytest.hookspec.pytest_sessionstart` -+ hook accessible as a session scoped `stats_processes` fixture. -+ :keyword bool system_install: -+ If true, the daemons and CLI's are run against a system installed salt setup, ie, the default -+ salt system paths apply. -+ """ -+ -+ root_dir = attr.ib() -+ tmp_root_dir = attr.ib(init=False) -+ log_server_port = attr.ib() -+ log_server_level = attr.ib() -+ log_server_host = attr.ib() -+ code_dir = attr.ib(default=None) -+ inject_coverage = attr.ib(default=False) -+ inject_sitecustomize = attr.ib(default=False) -+ cwd = attr.ib(default=None) -+ environ = attr.ib(default=None) -+ slow_stop = attr.ib(default=True) -+ start_timeout = attr.ib(default=None) -+ stats_processes = attr.ib(repr=False, default=None) -+ system_install = attr.ib(repr=False, default=False) -+ event_listener = attr.ib(repr=False) -+ -+ # Internal attributes -+ scripts_dir = attr.ib(default=None, init=False, repr=False) -+ -+ def __attrs_post_init__(self): -+ self.tmp_root_dir = pathlib.Path(self.root_dir.strpath) -+ self.tmp_root_dir.mkdir(exist_ok=True) -+ if self.system_install is False: -+ self.root_dir = self.tmp_root_dir -+ else: -+ self.root_dir = pathlib.Path("/") -+ if self.start_timeout is None: -+ if not sys.platform.startswith(("win", "darwin")): -+ self.start_timeout = 60 -+ else: -+ # Windows and macOS are just slower -+ self.start_timeout = 120 -+ -+ if self.system_install is False: -+ # Setup the internal attributes -+ self.scripts_dir = self.root_dir / "scripts" -+ self.scripts_dir.mkdir(exist_ok=True) -+ -+ @staticmethod -+ def get_salt_log_handlers_path(): -+ """ -+ Returns the path to the Salt log handler this plugin provides -+ """ -+ return CODE_ROOT_DIR / "utils" / "saltext" / "log_handlers" -+ -+ @staticmethod -+ def get_salt_engines_path(): -+ """ -+ Returns the path to the Salt engine this plugin provides -+ """ -+ return CODE_ROOT_DIR / "utils" / "saltext" / "engines" -+ -+ def final_minion_config_tweaks(self, config): -+ pytest_key = "pytest-minion" -+ if pytest_key not in config: # pragma: no cover -+ config[pytest_key] = {} -+ config[pytest_key]["returner_address"] = self.event_listener.address -+ self.final_common_config_tweaks(config, "minion") -+ -+ def final_master_config_tweaks(self, config): -+ pytest_key = "pytest-master" -+ if pytest_key not in config: # pragma: no cover -+ config[pytest_key] = {} -+ config[pytest_key]["returner_address"] = self.event_listener.address -+ self.final_common_config_tweaks(config, "master") -+ -+ def final_syndic_config_tweaks(self, config): -+ self.final_common_config_tweaks(config, "syndic") -+ -+ def final_proxy_minion_config_tweaks(self, config): -+ self.final_common_config_tweaks(config, "minion") -+ -+ def final_cloud_config_tweaks(self, config): -+ self.final_common_config_tweaks(config, "cloud") -+ -+ def final_common_config_tweaks(self, config, role): -+ config.setdefault("engines", []) -+ if "pytest" not in config["engines"]: -+ config["engines"].append("pytest") -+ -+ config.setdefault("user", running_username()) -+ if not config["user"]: # pragma: no cover -+ # If this value is empty, None, False, just remove it -+ config.pop("user") -+ -+ pytest_key = "pytest-{}".format(role) -+ if pytest_key not in config: -+ config[pytest_key] = {} -+ -+ pytest_config = config[pytest_key] -+ if "log" not in pytest_config: # pragma: no cover -+ pytest_config["log"] = {} -+ -+ log_config = pytest_config["log"] -+ log_config.setdefault("host", self.log_server_host) -+ log_config.setdefault("port", self.log_server_port) -+ log_config.setdefault("level", "debug") -+ -+ def salt_master_daemon( -+ self, -+ master_id, -+ order_masters=False, -+ master_of_masters=None, -+ defaults=None, -+ overrides=None, -+ max_start_attempts=3, -+ start_timeout=None, -+ factory_class=daemons.master.SaltMaster, -+ **factory_class_kwargs -+ ): -+ """ -+ Configure a salt-master -+ -+ Args: -+ master_id(str): -+ The master ID -+ order_masters(bool): -+ Boolean flag to set if this master is going to control other masters(ie, master of masters), like, -+ for example, in a :ref:`Syndic <salt:syndic>` topology scenario -+ master_of_masters(:py:class:`saltfactories.daemons.master.SaltMaster`): -+ A :py:class:`saltfactories.daemons.master.SaltMaster` instance, like, for example, -+ in a :ref:`Syndic <salt:syndic>` topology scenario -+ defaults(dict): -+ A dictionary of default configuration to use when configuring the master -+ overrides(dict): -+ A dictionary of configuration overrides to use when configuring the master -+ max_start_attempts(int): -+ How many attempts should be made to start the master in case of failure to validate that its running -+ factory_class_kwargs(dict): -+ Extra keyword arguments to pass to :py:class:`saltfactories.daemons.master.SaltMaster` -+ -+ Returns: -+ :py:class:`saltfactories.daemons.master.SaltMaster`: -+ The master process class instance -+ """ -+ root_dir = self.get_root_dir_for_daemon( -+ master_id, defaults=defaults, factory_class=factory_class -+ ) -+ config = factory_class.configure( -+ self, -+ master_id, -+ root_dir=root_dir, -+ defaults=defaults, -+ overrides=overrides, -+ order_masters=order_masters, -+ master_of_masters=master_of_masters, -+ ) -+ self.final_master_config_tweaks(config) -+ loaded_config = factory_class.write_config(config) -+ return self._get_factory_class_instance( -+ "salt-master", -+ loaded_config, -+ factory_class, -+ master_id, -+ max_start_attempts, -+ start_timeout, -+ **factory_class_kwargs -+ ) -+ -+ def salt_minion_daemon( -+ self, -+ minion_id, -+ master=None, -+ defaults=None, -+ overrides=None, -+ max_start_attempts=3, -+ start_timeout=None, -+ factory_class=daemons.minion.SaltMinion, -+ **factory_class_kwargs -+ ): -+ """ -+ Spawn a salt-minion -+ -+ Args: -+ minion_id(str): -+ The minion ID -+ master(:py:class:`saltfactories.daemons.master.SaltMaster`): -+ An instance of :py:class:`saltfactories.daemons.master.SaltMaster` that -+ this minion will connect to. -+ defaults(dict): -+ A dictionary of default configuration to use when configuring the minion -+ overrides(dict): -+ A dictionary of configuration overrides to use when configuring the minion -+ max_start_attempts(int): -+ How many attempts should be made to start the minion in case of failure to validate that its running -+ factory_class_kwargs(dict): -+ Extra keyword arguments to pass to :py:class:`~saltfactories.daemons.minion.SaltMinion` -+ -+ Returns: -+ :py:class:`~saltfactories.daemons.minion.SaltMinion`: -+ The minion process class instance -+ """ -+ root_dir = self.get_root_dir_for_daemon( -+ minion_id, defaults=defaults, factory_class=factory_class -+ ) -+ -+ config = factory_class.configure( -+ self, -+ minion_id, -+ root_dir=root_dir, -+ defaults=defaults, -+ overrides=overrides, -+ master=master, -+ ) -+ self.final_minion_config_tweaks(config) -+ loaded_config = factory_class.write_config(config) -+ return self._get_factory_class_instance( -+ "salt-minion", -+ loaded_config, -+ factory_class, -+ minion_id, -+ max_start_attempts, -+ start_timeout, -+ **factory_class_kwargs -+ ) -+ -+ def salt_syndic_daemon( -+ self, -+ syndic_id, -+ master_of_masters=None, -+ defaults=None, -+ overrides=None, -+ max_start_attempts=3, -+ start_timeout=None, -+ factory_class=daemons.syndic.SaltSyndic, -+ master_defaults=None, -+ master_overrides=None, -+ master_factory_class=daemons.master.SaltMaster, -+ minion_defaults=None, -+ minion_overrides=None, -+ minion_factory_class=daemons.minion.SaltMinion, -+ **factory_class_kwargs -+ ): -+ """ -+ Spawn a salt-syndic -+ -+ Args: -+ syndic_id(str): -+ The Syndic ID. This ID will be shared by the ``salt-master``, ``salt-minion`` and ``salt-syndic`` -+ processes. -+ master_of_masters(:py:class:`saltfactories.daemons.master.SaltMaster`): -+ An instance of :py:class:`saltfactories.daemons.master.SaltMaster` that the -+ master configured in this :ref:`Syndic <salt:syndic>` topology scenario shall connect to. -+ defaults(dict): -+ A dictionary of default configurations with three top level keys, ``master``, ``minion`` and -+ ``syndic``, to use when configuring the ``salt-master``, ``salt-minion`` and ``salt-syndic`` -+ respectively. -+ overrides(dict): -+ A dictionary of configuration overrides with three top level keys, ``master``, ``minion`` and -+ ``syndic``, to use when configuring the ``salt-master``, ``salt-minion`` and ``salt-syndic`` -+ respectively. -+ max_start_attempts(int): -+ How many attempts should be made to start the syndic in case of failure to validate that its running -+ factory_class_kwargs(dict): -+ Extra keyword arguments to pass to :py:class:`~saltfactories.daemons.syndic.SaltSyndic` -+ -+ Returns: -+ :py:class:`~saltfactories.daemons.syndic.SaltSyndic`: -+ The syndic process class instance -+ """ -+ -+ root_dir = self.get_root_dir_for_daemon( -+ syndic_id, defaults=defaults, factory_class=factory_class -+ ) -+ -+ master_config = master_factory_class.configure( -+ self, -+ syndic_id, -+ root_dir=root_dir, -+ defaults=master_defaults, -+ overrides=master_overrides, -+ master_of_masters=master_of_masters, -+ ) -+ # Remove syndic related options -+ for key in list(master_config): -+ if key.startswith("syndic_"): -+ master_config.pop(key) -+ self.final_master_config_tweaks(master_config) -+ master_loaded_config = master_factory_class.write_config(master_config) -+ master_factory = self._get_factory_class_instance( -+ "salt-master", -+ master_loaded_config, -+ master_factory_class, -+ syndic_id, -+ max_start_attempts, -+ start_timeout, -+ ) -+ -+ minion_config = minion_factory_class.configure( -+ self, -+ syndic_id, -+ root_dir=root_dir, -+ defaults=minion_defaults, -+ overrides=minion_overrides, -+ master=master_factory, -+ ) -+ self.final_minion_config_tweaks(minion_config) -+ minion_loaded_config = minion_factory_class.write_config(minion_config) -+ minion_factory = self._get_factory_class_instance( -+ "salt-minion", -+ minion_loaded_config, -+ minion_factory_class, -+ syndic_id, -+ max_start_attempts, -+ start_timeout, -+ ) -+ -+ syndic_config = factory_class.default_config( -+ root_dir, -+ syndic_id=syndic_id, -+ defaults=defaults, -+ overrides=overrides, -+ master_of_masters=master_of_masters, -+ system_install=self.system_install, -+ ) -+ self.final_syndic_config_tweaks(syndic_config) -+ syndic_loaded_config = factory_class.write_config(syndic_config) -+ factory = self._get_factory_class_instance( -+ "salt-syndic", -+ syndic_loaded_config, -+ factory_class, -+ syndic_id, -+ max_start_attempts=max_start_attempts, -+ start_timeout=start_timeout, -+ master=master_factory, -+ minion=minion_factory, -+ **factory_class_kwargs -+ ) -+ -+ # We need the syndic master and minion running -+ factory.before_start(master_factory.start) -+ factory.before_start(minion_factory.start) -+ return factory -+ -+ def salt_proxy_minion_daemon( -+ self, -+ proxy_minion_id, -+ master=None, -+ defaults=None, -+ overrides=None, -+ max_start_attempts=3, -+ start_timeout=None, -+ factory_class=daemons.proxy.SaltProxyMinion, -+ **factory_class_kwargs -+ ): -+ """ -+ Spawn a salt-proxy -+ -+ Args: -+ proxy_minion_id(str): -+ The proxy minion ID -+ master(:py:class:`saltfactories.daemons.master.SaltMaster`): -+ An instance of :py:class:`saltfactories.daemons.master.SaltMaster` that this minion -+ will connect to. -+ defaults(dict): -+ A dictionary of default configuration to use when configuring the proxy minion -+ overrides(dict): -+ A dictionary of configuration overrides to use when configuring the proxy minion -+ max_start_attempts(int): -+ How many attempts should be made to start the proxy minion in case of failure to validate that -+ its running -+ factory_class_kwargs(dict): -+ Extra keyword arguments to pass to :py:class:`~saltfactories.daemons.proxy.SaltProxyMinion` -+ -+ Returns: -+ :py:class:`~saltfactories.daemons.proxy.SaltProxyMinion`: -+ The proxy minion process class instance -+ """ -+ root_dir = self.get_root_dir_for_daemon( -+ proxy_minion_id, defaults=defaults, factory_class=factory_class -+ ) -+ -+ config = factory_class.configure( -+ self, -+ proxy_minion_id, -+ root_dir=root_dir, -+ defaults=defaults, -+ overrides=overrides, -+ master=master, -+ ) -+ self.final_proxy_minion_config_tweaks(config) -+ loaded_config = factory_class.write_config(config) -+ return self._get_factory_class_instance( -+ "salt-proxy", -+ loaded_config, -+ factory_class, -+ proxy_minion_id, -+ max_start_attempts, -+ start_timeout, -+ **factory_class_kwargs -+ ) -+ -+ def salt_api_daemon( -+ self, -+ master, -+ max_start_attempts=3, -+ start_timeout=None, -+ factory_class=daemons.api.SaltApi, -+ **factory_class_kwargs -+ ): -+ """ -+ Spawn a salt-api -+ -+ Please see py:class:`~saltfactories.manager.FactoriesManager.salt_master_daemon` for argument -+ documentation. -+ -+ Returns: -+ :py:class:`~saltfactories.daemons.api.SaltApi`: -+ The salt-api process class instance -+ """ -+ return self._get_factory_class_instance( -+ "salt-api", -+ master.config, -+ factory_class, -+ master.id, -+ max_start_attempts=max_start_attempts, -+ start_timeout=start_timeout, -+ **factory_class_kwargs -+ ) -+ -+ def get_sshd_daemon( -+ self, -+ config_dir=None, -+ listen_address=None, -+ listen_port=None, -+ sshd_config_dict=None, -+ display_name=None, -+ script_name="sshd", -+ max_start_attempts=3, -+ start_timeout=None, -+ factory_class=daemons.sshd.Sshd, -+ **factory_class_kwargs -+ ): -+ """ -+ Start an sshd daemon -+ -+ Args: -+ max_start_attempts(int): -+ How many attempts should be made to start the proxy minion in case of failure to validate that -+ its running -+ config_dir(pathlib.Path): -+ The path to the sshd config directory -+ listen_address(str): -+ The address where the sshd server will listen to connections. Defaults to 127.0.0.1 -+ listen_port(int): -+ The port where the sshd server will listen to connections -+ sshd_config_dict(dict): -+ A dictionary of key-value pairs to construct the sshd config file -+ script_name(str): -+ The name or path to the binary to run. Defaults to ``sshd``. -+ factory_class_kwargs(dict): -+ Extra keyword arguments to pass to :py:class:`~saltfactories.daemons.sshd.Sshd` -+ -+ Returns: -+ :py:class:`~saltfactories.daemons.sshd.Sshd`: -+ The sshd process class instance -+ """ -+ if config_dir is None: -+ config_dir = self.get_root_dir_for_daemon("sshd", factory_class=factory_class) -+ try: -+ config_dir = pathlib.Path(config_dir.strpath).resolve() -+ except AttributeError: -+ config_dir = pathlib.Path(config_dir).resolve() -+ -+ return factory_class( -+ start_timeout=start_timeout or self.start_timeout, -+ slow_stop=self.slow_stop, -+ environ=self.environ, -+ cwd=self.cwd, -+ max_start_attempts=max_start_attempts, -+ factories_manager=self, -+ script_name=script_name, -+ display_name=display_name or "SSHD", -+ config_dir=config_dir, -+ listen_address=listen_address, -+ listen_port=listen_port, -+ sshd_config_dict=sshd_config_dict, -+ **factory_class_kwargs -+ ) -+ -+ def get_container( -+ self, -+ container_name, -+ image_name, -+ docker_client=None, -+ display_name=None, -+ factory_class=daemons.container.Container, -+ max_start_attempts=3, -+ start_timeout=None, -+ **factory_class_kwargs -+ ): -+ """ -+ Start a docker container -+ -+ Args: -+ container_name(str): -+ The name to give the container -+ image_name(str): -+ The image to use -+ docker_client: -+ An instance of the docker client to use -+ display_name(str): -+ Human readable name for the factory -+ factory_class: -+ A factory class. (Default :py:class:`~saltfactories.daemons.container.Container`) -+ max_start_attempts(int): -+ How many attempts should be made to start the container in case of failure to validate that -+ its running. -+ start_timeout(int): -+ The amount of time, in seconds, to wait, until the container is considered as not started. -+ factory_class_kwargs(dict): -+ Extra keyword arguments to pass to :py:class:`~saltfactories.daemons.container.Container` -+ -+ Returns: -+ :py:class:`~saltfactories.daemons.container.Container`: -+ The factory instance -+ """ -+ return factory_class( -+ name=container_name, -+ image=image_name, -+ docker_client=docker_client, -+ display_name=display_name or container_name, -+ environ=self.environ, -+ cwd=self.cwd, -+ start_timeout=start_timeout or self.start_timeout, -+ max_start_attempts=max_start_attempts, -+ **factory_class_kwargs -+ ) -+ -+ def get_salt_script_path(self, script_name): -+ """ -+ Return the path to the customized script path, generating one if needed. -+ """ -+ if self.system_install is True: -+ return script_name -+ return cli_scripts.generate_script( -+ self.scripts_dir, -+ script_name, -+ code_dir=self.code_dir, -+ inject_coverage=self.inject_coverage, -+ inject_sitecustomize=self.inject_sitecustomize, -+ ) -+ -+ def _get_factory_class_instance( -+ self, -+ script_name, -+ daemon_config, -+ factory_class, -+ daemon_id, -+ max_start_attempts, -+ start_timeout, -+ **factory_class_kwargs -+ ): -+ """ -+ Helper method to instantiate daemon factories -+ """ -+ if self.system_install: -+ script_path = script_name -+ else: -+ script_path = self.get_salt_script_path(script_name) -+ factory = factory_class( -+ config=daemon_config, -+ start_timeout=start_timeout or self.start_timeout, -+ slow_stop=self.slow_stop, -+ environ=self.environ, -+ cwd=self.cwd, -+ max_start_attempts=max_start_attempts, -+ event_listener=self.event_listener, -+ factories_manager=self, -+ script_name=script_path, -+ system_install=self.system_install, -+ **factory_class_kwargs -+ ) -+ return factory -+ -+ def get_root_dir_for_daemon(self, daemon_id, defaults=None, factory_class=None): -+ if defaults and "root_dir" in defaults: -+ try: -+ root_dir = pathlib.Path(defaults["root_dir"].strpath).resolve() -+ except AttributeError: -+ root_dir = pathlib.Path(defaults["root_dir"]).resolve() -+ root_dir.mkdir(parents=True, exist_ok=True) -+ return root_dir -+ if self.system_install is True and issubclass(factory_class, Salt): -+ return self.root_dir -+ elif self.system_install is True: -+ root_dir = self.tmp_root_dir -+ else: -+ root_dir = self.root_dir -+ counter = 1 -+ root_dir = root_dir / daemon_id -+ while True: -+ if not root_dir.is_dir(): -+ break -+ root_dir = self.root_dir / "{}_{}".format(daemon_id, counter) -+ counter += 1 -+ root_dir.mkdir(parents=True, exist_ok=True) -+ return root_dir ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/plugins/__init__.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/plugins/__init__.py -@@ -0,0 +1,84 @@ -+""" -+saltfactories.plugins -+~~~~~~~~~~~~~~~~~~~~~ -+ -+Salt Factories PyTest plugin interface -+""" -+import logging -+import os -+import tempfile -+ -+import pytest -+import pytestskipmarkers.utils.platform -+ -+import saltfactories.utils.tempfiles -+ -+log = logging.getLogger(__name__) -+ -+ -+def pytest_tempdir_temproot(): -+ # Taken from https://github.com/saltstack/salt/blob/v2019.2.0/tests/support/paths.py -+ # Avoid ${TMPDIR} and gettempdir() on MacOS as they yield a base path too long -+ # for unix sockets: ``error: AF_UNIX path too long`` -+ # Gentoo Portage prefers ebuild tests are rooted in ${TMPDIR} -+ if pytestskipmarkers.utils.platform.is_windows(): -+ tempdir = "C:/Windows/Temp" -+ elif pytestskipmarkers.utils.platform.is_darwin(): -+ tempdir = "/tmp" -+ else: -+ tempdir = os.environ.get("TMPDIR") or tempfile.gettempdir() -+ return os.path.abspath(os.path.realpath(tempdir)) -+ -+ -+def pytest_tempdir_basename(): -+ """ -+ Return the temporary directory basename for the salt test suite. -+ """ -+ return "saltfactories" -+ -+ -+def pytest_runtest_logstart(nodeid): -+ """ -+ signal the start of running a single test item. -+ -+ This hook will be called **before** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and -+ :func:`pytest_runtest_teardown` hooks. -+ -+ :param str nodeid: full id of the item -+ :param location: a triple of ``(filename, linenum, testname)`` -+ """ -+ log.debug(">>>>>>> START %s >>>>>>>", nodeid) -+ -+ -+def pytest_runtest_logfinish(nodeid): -+ """ -+ signal the complete finish of running a single test item. -+ -+ This hook will be called **after** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and -+ :func:`pytest_runtest_teardown` hooks. -+ -+ :param str nodeid: full id of the item -+ :param location: a triple of ``(filename, linenum, testname)`` -+ """ -+ log.debug("<<<<<<< END %s <<<<<<<", nodeid) -+ -+ -+def pytest_runtest_logreport(report): -+ """ -+ Process the :py:class:`_pytest.reports.TestReport` produced for each -+ of the setup, call and teardown runtest phases of an item. -+ See :func:`pytest_runtest_protocol` for a description of the runtest protocol. -+ """ -+ if report.when == "call": -+ log.debug("======= %s %s ========", report.outcome.upper(), report.nodeid) -+ -+ -+@pytest.hookimpl(trylast=True) -+def pytest_load_initial_conftests(*_): -+ """ -+ Register our pytest helpers -+ """ -+ if "temp_directory" not in pytest.helpers: -+ pytest.helpers.register(saltfactories.utils.tempfiles.temp_directory, name="temp_directory") -+ if "temp_file" not in pytest.helpers: -+ pytest.helpers.register(saltfactories.utils.tempfiles.temp_file, name="temp_file") ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/plugins/event_listener.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/plugins/event_listener.py -@@ -0,0 +1,482 @@ -+""" -+Event Listener -+============== -+ -+A salt events store for all daemons started by salt-factories -+""" -+import copy -+import fnmatch -+import logging -+import threading -+import weakref -+from collections import deque -+from datetime import datetime -+from datetime import timedelta -+ -+import attr -+import msgpack -+import pytest -+import zmq -+ -+from saltfactories.utils import ports -+from saltfactories.utils import time -+ -+log = logging.getLogger(__name__) -+ -+ -+def _convert_stamp(stamp): -+ try: -+ return datetime.fromisoformat(stamp) -+ except AttributeError: # pragma: no cover -+ # Python < 3.7 -+ return datetime.strptime(stamp, "%Y-%m-%dT%H:%M:%S.%f") -+ -+ -+@attr.s(kw_only=True, slots=True, hash=True, frozen=True) -+class Event: -+ """ -+ The ``Event`` class is a container for a salt event which will live on the -+ :py:class:`~saltfactories.plugins.event_listener.EventListener` store. -+ -+ :keyword str daemon_id: -+ The daemon ID which received this event. -+ :keyword str tag: -+ The event tag of the event. -+ :keyword ~datetime.datetime stamp: -+ When the event occurred -+ :keyword dict data: -+ The event payload, filtered of all of Salt's private keys like ``_stamp`` which prevents proper -+ assertions against it. -+ :keyword dict full_data: -+ The full event payload, as received by the daemon, including all of Salt's private keys. -+ :keyword int,float expire_seconds: -+ The time, in seconds, after which the event should be considered as expired and removed from the store. -+ """ -+ -+ daemon_id = attr.ib() -+ tag = attr.ib() -+ stamp = attr.ib(converter=_convert_stamp) -+ data = attr.ib(hash=False) -+ full_data = attr.ib(hash=False) -+ expire_seconds = attr.ib(hash=False) -+ _expire_at = attr.ib(init=False, hash=False) -+ -+ @_expire_at.default -+ def _set_expire_at(self): -+ return self.stamp + timedelta(seconds=self.expire_seconds) -+ -+ @property -+ def expired(self): -+ """ -+ Property to identify if the event has expired, at which time it should be removed from the store. -+ """ -+ if datetime.utcnow() < self._expire_at: -+ return False -+ return True -+ -+ -+@attr.s(kw_only=True, slots=True, hash=True, frozen=True) -+class MatchedEvents: -+ """ -+ The ``MatchedEvents`` class is a container which is returned by -+ :py:func:`~saltfactories.plugins.event_listener.EventListener.wait_for_events`. -+ -+ :keyword set matches: -+ A :py:class:`set` of :py:class:`~saltfactories.plugins.event_listener.Event` instances that matched. -+ :keyword set missed: -+ A :py:class:`set` of :py:class:`~saltfactories.plugins.event_listener.Event` instances that remained -+ unmatched. -+ -+ One can also easily iterate through all matched events of this class: -+ -+ .. code-block:: python -+ -+ matched_events = MatchedEvents(..., ...) -+ for event in matched_events: -+ print(event.tag) -+ """ -+ -+ matches = attr.ib() -+ missed = attr.ib() -+ -+ @property -+ def found_all_events(self): -+ """ -+ :return bool: :py:class:`True` if all events were matched, or :py:class:`False` otherwise. -+ """ -+ return (not self.missed) is True -+ -+ def __iter__(self): -+ return iter(self.matches) -+ -+ -+@attr.s(kw_only=True, slots=True, hash=True) -+class EventListener: -+ """ -+ The ``EventListener`` is a service started by salt-factories which receives all the events of all the -+ salt masters that it starts. The service runs throughout the whole pytest session. -+ -+ :keyword int timeout: -+ How long, in seconds, should a forwarded event stay in the store, after which, it will be deleted. -+ """ -+ -+ timeout = attr.ib(default=120) -+ address = attr.ib(init=False) -+ store = attr.ib(init=False, repr=False, hash=False) -+ sentinel = attr.ib(init=False, repr=False, hash=False) -+ sentinel_event = attr.ib(init=False, repr=False, hash=False) -+ running_event = attr.ib(init=False, repr=False, hash=False) -+ running_thread = attr.ib(init=False, repr=False, hash=False) -+ cleanup_thread = attr.ib(init=False, repr=False, hash=False) -+ auth_event_handlers = attr.ib(init=False, repr=False, hash=False) -+ -+ def __attrs_post_init__(self): -+ self.store = deque(maxlen=10000) -+ self.address = "tcp://127.0.0.1:{}".format(ports.get_unused_localhost_port()) -+ self.running_event = threading.Event() -+ self.running_thread = threading.Thread(target=self._run) -+ self.cleanup_thread = threading.Thread(target=self._cleanup) -+ self.sentinel = msgpack.dumps(None) -+ self.sentinel_event = threading.Event() -+ self.auth_event_handlers = weakref.WeakValueDictionary() -+ -+ def _run(self): -+ context = zmq.Context() -+ puller = context.socket(zmq.PULL) -+ log.debug("%s Binding PULL socket to %s", self, self.address) -+ puller.bind(self.address) -+ if msgpack.version >= (0, 5, 2): -+ msgpack_kwargs = {"raw": False} -+ else: # pragma: no cover -+ msgpack_kwargs = {"encoding": "utf-8"} -+ log.debug("%s started", self) -+ self.running_event.set() -+ while self.running_event.is_set(): -+ payload = puller.recv() -+ if payload == self.sentinel: -+ log.info("%s Received stop sentinel...", self) -+ self.sentinel_event.set() -+ break -+ try: -+ decoded = msgpack.loads(payload, **msgpack_kwargs) -+ except ValueError: # pragma: no cover -+ log.error( -+ "%s Failed to msgpack.load message with payload: %s", -+ self, -+ payload, -+ exc_info=True, -+ ) -+ continue -+ if decoded is None: -+ log.info("%s Received stop sentinel...", self) -+ self.sentinel_event.set() -+ break -+ try: -+ daemon_id, tag, data = decoded -+ # Salt's event data has some "private" keys, for example, "_stamp" which -+ # get in the way of direct assertions. -+ # We'll just store a full_data attribute and clean up the regular data of these keys -+ full_data = copy.deepcopy(data) -+ for key in list(data): -+ if key.startswith("_"): -+ data.pop(key) -+ event = Event( -+ daemon_id=daemon_id, -+ tag=tag, -+ stamp=full_data["_stamp"], -+ data=data, -+ full_data=full_data, -+ expire_seconds=self.timeout, -+ ) -+ log.info("%s received event: %s", self, event) -+ self.store.append(event) -+ if tag == "salt/auth": -+ auth_event_callback = self.auth_event_handlers.get(daemon_id) -+ if auth_event_callback: -+ try: -+ auth_event_callback(data) -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ log.error( -+ "%s Error calling %r: %s", -+ self, -+ auth_event_callback, -+ exc, -+ exc_info=True, -+ ) -+ log.debug("%s store size after event received: %d", self, len(self.store)) -+ except Exception: # pragma: no cover pylint: disable=broad-except -+ log.error("%s Something funky happened", self, exc_info=True) -+ puller.close(0) -+ context.term() -+ # We need to keep these events stored, restart zmq socket -+ context = zmq.Context() -+ puller = context.socket(zmq.PULL) -+ log.debug("%s Binding PULL socket to %s", self, self.address) -+ puller.bind(self.address) -+ puller.close(1500) -+ context.term() -+ log.debug("%s is no longer running", self) -+ -+ def _cleanup(self): -+ cleanup_at = time.time() + 30 -+ while self.running_event.is_set(): -+ if time.time() < cleanup_at: -+ time.sleep(1) -+ continue -+ -+ # Reset cleanup time -+ cleanup_at = time.time() + 30 -+ -+ # Cleanup expired events -+ to_remove = [] -+ for event in self.store: -+ if event.expired: -+ to_remove.append(event) -+ -+ for event in to_remove: -+ log.debug("%s Removing from event store: %s", self, event) -+ self.store.remove(event) -+ log.debug("%s store size after cleanup: %s", self, len(self.store)) -+ -+ def start(self): -+ if self.running_event.is_set(): # pragma: no cover -+ return -+ log.debug("%s is starting", self) -+ self.running_thread.start() -+ # Wait for the thread to start -+ if self.running_event.wait(5) is not True: -+ self.running_event.clear() -+ raise RuntimeError("Failed to start the event listener") -+ self.cleanup_thread.start() -+ -+ def stop(self): -+ if self.running_event.is_set() is False: # pragma: no cover -+ return -+ log.debug("%s is stopping", self) -+ self.store.clear() -+ self.auth_event_handlers.clear() -+ context = zmq.Context() -+ push = context.socket(zmq.PUSH) -+ push.connect(self.address) -+ try: -+ push.send(self.sentinel) -+ log.debug("%s Sent sentinel to trigger log server shutdown", self) -+ if self.sentinel_event.wait(5) is not True: # pragma: no cover -+ log.warning( -+ "%s Failed to wait for the reception of the stop sentinel message. Stopping anyway.", -+ self, -+ ) -+ finally: -+ push.close(1500) -+ context.term() -+ self.running_event.clear() -+ log.debug("%s Joining running thread...", self) -+ self.running_thread.join(7) -+ if self.running_thread.is_alive(): # pragma: no cover -+ log.debug("%s The running thread is still alive. Waiting a little longer...", self) -+ self.running_thread.join(5) -+ if self.running_thread.is_alive(): -+ log.debug( -+ "%s The running thread is still alive. Exiting anyway and let GC take care of it", -+ self, -+ ) -+ log.debug("%s Joining cleanup thread...", self) -+ self.cleanup_thread.join(7) -+ if self.cleanup_thread.is_alive(): # pragma: no cover -+ log.debug("%s The cleanup thread is still alive. Waiting a little longer...", self) -+ self.cleanup_thread.join(5) -+ if self.cleanup_thread.is_alive(): -+ log.debug( -+ "%s The cleanup thread is still alive. Exiting anyway and let GC take care of it", -+ self, -+ ) -+ log.debug("%s stopped", self) -+ -+ def get_events(self, patterns, after_time=None): -+ """ -+ Get events from the internal store. -+ -+ :param ~collections.abc.Sequence pattern: -+ An iterable of tuples in the form of ``("<daemon-id>", "<event-tag-pattern>")``, ie, which daemon ID -+ we're targeting and the event tag pattern which will be passed to :py:func:`~fnmatch.fnmatch` to -+ assert a match. -+ :keyword ~datetime.datetime,float after_time: -+ After which time to start matching events. -+ :return set: A set of matched events -+ """ -+ if after_time is None: -+ after_time = datetime.utcnow() -+ elif isinstance(after_time, float): -+ after_time = datetime.utcfromtimestamp(after_time) -+ after_time_iso = after_time.isoformat() -+ log.debug( -+ "%s is checking for event patterns happening after %s: %s", -+ self, -+ after_time_iso, -+ set(patterns), -+ ) -+ found_events = set() -+ patterns = set(patterns) -+ for event in copy.copy(self.store): -+ if event.expired: -+ # Too old, carry on -+ continue -+ if event.stamp < after_time: -+ continue -+ for pattern in set(patterns): -+ _daemon_id, _pattern = pattern -+ if event.daemon_id != _daemon_id: -+ continue -+ if fnmatch.fnmatch(event.tag, _pattern): -+ log.debug("%s Found matching pattern: %s", self, pattern) -+ found_events.add(event) -+ if found_events: -+ log.debug( -+ "%s found the following patterns happening after %s: %s", -+ self, -+ after_time_iso, -+ found_events, -+ ) -+ else: -+ log.debug( -+ "%s did not find any matching event patterns happening after %s", -+ self, -+ after_time_iso, -+ ) -+ return found_events -+ -+ def wait_for_events(self, patterns, timeout=30, after_time=None): -+ """ -+ Wait for a set of patterns to match or until timeout is reached. -+ -+ :param ~collections.abc.Sequence pattern: -+ An iterable of tuples in the form of ``("<daemon-id>", "<event-tag-pattern>")``, ie, which daemon ID -+ we're targeting and the event tag pattern which will be passed to :py:func:`~fnmatch.fnmatch` to -+ assert a match. -+ :keyword int,float timeout: -+ The amount of time to wait for the events, in seconds. -+ :keyword ~datetime.datetime,float after_time: -+ After which time to start matching events. -+ -+ :return: -+ An instance of :py:class:`~saltfactories.plugins.event_listener.MatchedEvents`. -+ :rtype ~saltfactories.plugins.event_listener.MatchedEvents: -+ """ -+ if after_time is None: -+ after_time = datetime.utcnow() -+ elif isinstance(after_time, float): -+ after_time = datetime.utcfromtimestamp(after_time) -+ after_time_iso = after_time.isoformat() -+ log.debug( -+ "%s is waiting for event patterns happening after %s: %s", -+ self, -+ after_time_iso, -+ set(patterns), -+ ) -+ found_events = set() -+ patterns = set(patterns) -+ timeout_at = time.time() + timeout -+ while True: -+ if not patterns: -+ return True -+ for event in copy.copy(self.store): -+ if event.expired: -+ # Too old, carry on -+ continue -+ if event.stamp < after_time: -+ continue -+ for pattern in set(patterns): -+ _daemon_id, _pattern = pattern -+ if event.daemon_id != _daemon_id: -+ continue -+ if fnmatch.fnmatch(event.tag, _pattern): -+ log.debug("%s Found matching pattern: %s", self, pattern) -+ found_events.add(event) -+ patterns.remove((event.daemon_id, _pattern)) -+ if not patterns: -+ break -+ if time.time() > timeout_at: -+ break -+ time.sleep(0.5) -+ return MatchedEvents(matches=found_events, missed=patterns) -+ -+ def register_auth_event_handler(self, master_id, callback): -+ """ -+ Register a callback to run for every authentication event, to accept or reject the minion authenticating. -+ -+ :param str master_id: -+ The master ID for which the callback should run -+ :type callback: ~collections.abc.Callable -+ :param callback: -+ The function while should be called -+ """ -+ self.auth_event_handlers[master_id] = callback -+ -+ def unregister_auth_event_handler(self, master_id): -+ """ -+ Un-register the authentication event callback, if any, for the provided master ID -+ -+ :param str master_id: -+ The master ID for which the callback is registered -+ """ -+ self.auth_event_handlers.pop(master_id, None) -+ -+ -+@pytest.fixture(scope="session") -+def event_listener(request): -+ """ -+ All started daemons will forward their events into an instance of -+ :py:class:`~saltfactories.plugins.event_listener.EventListener`. -+ -+ This fixture can be used to wait for events: -+ -+ .. code-block:: python -+ -+ def test_send(event_listener, salt_master, salt_minion, salt_call_cli): -+ event_tag = random_string("salt/test/event/") -+ data = {"event.fire": "just test it!!!!"} -+ start_time = time.time() -+ ret = salt_call_cli.run("event.send", event_tag, data=data) -+ assert ret.exitcode == 0 -+ assert ret.json -+ assert ret.json is True -+ -+ event_pattern = (salt_master.id, event_tag) -+ matched_events = event_listener.wait_for_events( -+ [event_pattern], after_time=start_time, timeout=30 -+ ) -+ assert matched_events.found_all_events -+ # At this stage, we got all the events we were waiting for -+ -+ -+ And assert against those events events: -+ -+ .. code-block:: python -+ -+ def test_send(event_listener, salt_master, salt_minion, salt_call_cli): -+ # ... check the example above for the initial code ... -+ assert matched_events.found_all_events -+ # At this stage, we got all the events we were waiting for -+ for event in matched_events: -+ assert event.data["id"] == salt_minion.id -+ assert event.data["cmd"] == "_minion_event" -+ assert "event.fire" in event.data["data"] -+ """ -+ return request.config.pluginmanager.get_plugin("saltfactories-event-listener") -+ -+ -+def pytest_configure(config): -+ event_listener = EventListener() -+ config.pluginmanager.register(event_listener, "saltfactories-event-listener") -+ -+ -+@pytest.hookimpl(tryfirst=True) -+def pytest_sessionstart(session): -+ event_listener = session.config.pluginmanager.get_plugin("saltfactories-event-listener") -+ event_listener.start() -+ -+ -+@pytest.hookimpl(trylast=True) -+def pytest_sessionfinish(session): -+ event_listener = session.config.pluginmanager.get_plugin("saltfactories-event-listener") -+ event_listener.stop() ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/plugins/factories.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/plugins/factories.py -@@ -0,0 +1,67 @@ -+""" -+.. -+ PYTEST_DONT_REWRITE -+ -+Daemon & CLI Factories -+====================== -+ -+Salt Daemon Factories PyTest Plugin -+""" -+import logging -+import os -+import pprint -+ -+import pytest -+ -+import saltfactories -+from saltfactories.manager import FactoriesManager -+ -+ -+log = logging.getLogger(__name__) -+ -+ -+@pytest.fixture(scope="session") -+def _salt_factories_config(request): -+ """ -+ Return a dictionary with the keyword arguments for FactoriesManager -+ """ -+ log_server = request.config.pluginmanager.get_plugin("saltfactories-log-server") -+ return { -+ "code_dir": saltfactories.CODE_ROOT_DIR.parent, -+ "inject_coverage": True, -+ "inject_sitecustomize": True, -+ "log_server_host": log_server.log_host, -+ "log_server_port": log_server.log_port, -+ "log_server_level": log_server.log_level, -+ "system_install": "SALT_FACTORIES_SYSTEM_INSTALL" in os.environ, -+ } -+ -+ -+@pytest.fixture(scope="session") -+def salt_factories_config(): -+ return {} -+ -+ -+@pytest.fixture(scope="session") -+def salt_factories( -+ tempdir, event_listener, stats_processes, salt_factories_config, _salt_factories_config -+): -+ if not isinstance(salt_factories_config, dict): -+ raise pytest.UsageError("The 'salt_factories_config' fixture MUST return a dictionary") -+ if salt_factories_config: -+ log.debug( -+ "Salt Factories Manager Default Config:\n%s", pprint.pformat(_salt_factories_config) -+ ) -+ log.debug("Salt Factories Manager User Config:\n%s", pprint.pformat(salt_factories_config)) -+ factories_config = _salt_factories_config.copy() -+ factories_config.update(salt_factories_config) -+ log.debug( -+ "Instantiating the Salt Factories Manager with the following keyword arguments:\n%s", -+ pprint.pformat(factories_config), -+ ) -+ return FactoriesManager( -+ root_dir=tempdir, -+ stats_processes=stats_processes, -+ event_listener=event_listener, -+ **factories_config -+ ) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/plugins/loader.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/plugins/loader.py -@@ -0,0 +1,82 @@ -+""" -+Loader -+====== -+ -+Salt loader mock support for tests -+""" -+import logging -+ -+import pytest -+ -+from saltfactories.utils.loader import LoaderModuleMock -+ -+log = logging.getLogger(__name__) -+ -+ -+@pytest.hookimpl(trylast=True) -+def pytest_collection_modifyitems(items): -+ """ -+ Iterate through the collected items, in particular their test modules, to see if there's a function -+ named ``configure_loader_modules``. If there is, assert that it's a fixture. If not, raise an error. -+ """ -+ seen_modules = set() -+ for item in items: -+ if item.module.__name__ in seen_modules: -+ # No need to check the same module more than once -+ continue -+ seen_modules.add(item.module.__name__) -+ # Some users have reported that this was not working and it was due to the fixture having the -+ # wrong name. Let's look for typos. -+ typos = ("configure_loader_module", "configure_load_module", "configure_load_modules") -+ for typo in typos: -+ try: -+ fixture = getattr(item.module, typo) -+ try: -+ fixture._pytestfixturefunction # pylint: disable=pointless-statement -+ raise RuntimeError( -+ "The module {} defines a '{}' fixture but the correct fixture name " -+ "is 'configure_loader_modules'".format(item.module, typo) -+ ) -+ except AttributeError: -+ # It's a regular function?! -+ # Carry on -+ pass -+ except AttributeError: -+ # The test module does not define a function with the typo as the name. Good. -+ pass -+ # If the test module defines a configure_loader_modules function, let's confirm that it's actually a fixture -+ try: -+ fixture = item.module.configure_loader_modules -+ except AttributeError: -+ # The test module does not define a `configure_loader_modules` function at all -+ continue -+ else: -+ # The test module defines a `configure_loader_modules` function. Is it a fixture? -+ try: -+ fixture._pytestfixturefunction -+ except AttributeError: -+ # It's not a fixture, raise an error -+ raise RuntimeError( -+ "The module {} defines a 'configure_loader_modules' function but " -+ "that function is not a fixture".format(item.module) -+ ) from None -+ -+ -+@pytest.fixture(autouse=True) -+def setup_loader_mock(request): -+ """ -+ Setup Salt's loader mocking/patching if the test module defines a ``configure_loader_modules`` fixture -+ """ -+ # If the test module defines a configure_loader_modules function, we'll setup the LoaderModuleMock -+ # which is what actually sets up the salt loader mocking, if not, it's a no-op -+ try: -+ request.node.module.configure_loader_modules -+ except AttributeError: -+ # The test module does not define a `configure_loader_modules` function at all -+ # Carry on testing -+ yield -+ else: -+ # Mock salt's loader with what the `configure_loader_modules` fixture returns -+ configure_loader_modules = request.getfixturevalue("configure_loader_modules") -+ with LoaderModuleMock(configure_loader_modules) as loader_mock: -+ yield loader_mock ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/plugins/log_server.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/plugins/log_server.py -@@ -0,0 +1,211 @@ -+""" -+Log Server -+========== -+""" -+import logging -+import threading -+ -+import attr -+import msgpack -+import pytest -+import zmq -+from pytestskipmarkers.utils import platform -+from pytestskipmarkers.utils import ports -+ -+from saltfactories.utils import time -+ -+log = logging.getLogger(__name__) -+ -+ -+@attr.s(kw_only=True, slots=True, hash=True) -+class LogServer: -+ log_host = attr.ib() -+ log_port = attr.ib() -+ log_level = attr.ib() -+ socket_hwm = attr.ib() -+ running_event = attr.ib(init=False, repr=False, hash=False) -+ sentinel_event = attr.ib(init=False, repr=False, hash=False) -+ process_queue_thread = attr.ib(init=False, repr=False, hash=False) -+ -+ @log_host.default -+ def _default_log_host(self): -+ if platform.is_windows(): -+ # Windows cannot bind to 0.0.0.0 -+ return "127.0.0.1" -+ return "0.0.0.0" -+ -+ @log_port.default -+ def _default_log_port(self): -+ return ports.get_unused_localhost_port() -+ -+ @socket_hwm.default -+ def _default_socket_hwm(self): -+ # ~1MB -+ return 1000000 -+ -+ def start(self): -+ log.info("%s starting...", self) -+ self.sentinel_event = threading.Event() -+ self.running_event = threading.Event() -+ self.process_queue_thread = threading.Thread(target=self.process_logs) -+ self.process_queue_thread.start() -+ # Wait for the thread to start -+ if self.running_event.wait(5) is not True: # pragma: no cover -+ self.running_event.clear() -+ raise RuntimeError("Failed to start the log server") -+ log.info("%s started", self) -+ -+ def stop(self): -+ log.info("%s stopping...", self) -+ address = "tcp://{}:{}".format(self.log_host, self.log_port) -+ context = zmq.Context() -+ sender = context.socket(zmq.PUSH) -+ sender.connect(address) -+ try: -+ sender.send(msgpack.dumps(None)) -+ log.debug("%s Sent sentinel to trigger log server shutdown", self) -+ if self.sentinel_event.wait(5) is not True: # pragma: no cover -+ log.warning( -+ "%s Failed to wait for the reception of the stop sentinel message. Stopping anyway.", -+ self, -+ ) -+ finally: -+ sender.close(1000) -+ context.term() -+ -+ # Clear the running even, the log process thread know it should stop -+ self.running_event.clear() -+ log.info("%s Joining the logging server process thread", self) -+ self.process_queue_thread.join(7) -+ if not self.process_queue_thread.is_alive(): -+ log.debug("%s Stopped", self) -+ else: # pragma: no cover -+ log.warning( -+ "%s The logging server thread is still running. Waiting a little longer...", self -+ ) -+ self.process_queue_thread.join(5) -+ if not self.process_queue_thread.is_alive(): -+ log.debug("%s Stopped", self) -+ else: -+ log.warning("%s The logging server thread is still running...", self) -+ -+ def process_logs(self): -+ address = "tcp://{}:{}".format(self.log_host, self.log_port) -+ context = zmq.Context() -+ puller = context.socket(zmq.PULL) -+ puller.set_hwm(self.socket_hwm) -+ exit_timeout_seconds = 5 -+ exit_timeout = None -+ try: -+ puller.bind(address) -+ except zmq.ZMQError: # pragma: no cover -+ log.exception("%s Unable to bind to puller at %s", self, address) -+ return -+ try: -+ self.running_event.set() -+ poller = zmq.Poller() -+ poller.register(puller, zmq.POLLIN) -+ while True: -+ if not self.running_event.is_set(): -+ if exit_timeout is None: -+ log.debug( -+ "%s Waiting %d seconds to process any remaning log messages " -+ "before exiting...", -+ self, -+ exit_timeout_seconds, -+ ) -+ exit_timeout = time.time() + exit_timeout_seconds -+ -+ if time.time() >= exit_timeout: -+ log.debug( -+ "%s Unable to process remaining log messages in time. Exiting anyway.", -+ self, -+ ) -+ break -+ try: -+ if not poller.poll(1000): -+ continue -+ msg = puller.recv() -+ if msgpack.version >= (0, 5, 2): -+ record_dict = msgpack.loads(msg, raw=False) -+ else: # pragma: no cover -+ record_dict = msgpack.loads(msg, encoding="utf-8") -+ if record_dict is None: -+ # A sentinel to stop processing the queue -+ log.info("%s Received the sentinel to shutdown", self) -+ self.sentinel_event.set() -+ break -+ try: -+ record_dict["message"] -+ except KeyError: # pragma: no cover -+ # This log record was msgpack dumped from Py2 -+ for key, value in record_dict.copy().items(): -+ skip_update = True -+ if isinstance(value, bytes): -+ value = value.decode("utf-8") -+ skip_update = False -+ if isinstance(key, bytes): -+ key = key.decode("utf-8") -+ skip_update = False -+ if skip_update is False: -+ record_dict[key] = value -+ # Just log everything, filtering will happen on the main process -+ # logging handlers -+ record = logging.makeLogRecord(record_dict) -+ logger = logging.getLogger(record.name) -+ logger.handle(record) -+ except (EOFError, KeyboardInterrupt, SystemExit): # pragma: no cover -+ break -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ log.warning( -+ "%s An exception occurred in the processing queue thread: %s", -+ self, -+ exc, -+ exc_info=True, -+ ) -+ finally: -+ puller.close(1) -+ context.term() -+ log.debug("%s Process log thread terminated", self) -+ -+ -+@pytest.hookimpl(trylast=True) -+def pytest_configure(config): -+ # If PyTest has no logging configured, default to ERROR level -+ levels = [logging.ERROR] -+ logging_plugin = config.pluginmanager.get_plugin("logging-plugin") -+ try: -+ level = logging_plugin.log_cli_handler.level -+ if level is not None: -+ levels.append(level) -+ except AttributeError: # pragma: no cover -+ # PyTest CLI logging not configured -+ pass -+ try: -+ level = logging_plugin.log_file_level -+ if level is not None: -+ levels.append(level) -+ except AttributeError: # pragma: no cover -+ # PyTest Log File logging not configured -+ pass -+ -+ if logging.NOTSET in levels: -+ # We don't want the NOTSET level on the levels -+ levels.pop(levels.index(logging.NOTSET)) -+ -+ log_level = logging.getLevelName(min(levels)) -+ -+ log_server = LogServer(log_level=log_level) -+ config.pluginmanager.register(log_server, "saltfactories-log-server") -+ -+ -+@pytest.hookimpl(tryfirst=True) -+def pytest_sessionstart(session): -+ log_server = session.config.pluginmanager.get_plugin("saltfactories-log-server") -+ log_server.start() -+ -+ -+@pytest.hookimpl(trylast=True) -+def pytest_sessionfinish(session): -+ log_server = session.config.pluginmanager.get_plugin("saltfactories-log-server") -+ log_server.stop() ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/plugins/markers.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/plugins/markers.py -@@ -0,0 +1,49 @@ -+import pytest -+ -+import saltfactories.utils.functional -+import saltfactories.utils.markers -+ -+ -+@pytest.hookimpl(tryfirst=True) -+def pytest_runtest_setup(item): -+ """ -+ Fixtures injection based on markers or test skips based on CLI arguments -+ """ -+ __tracebackhide__ = True -+ saltfactories.utils.markers.evaluate_markers(item) -+ -+ -+@pytest.mark.trylast -+def pytest_configure(config): -+ """ -+ called after command line options have been parsed -+ and all plugins and initial conftest files been loaded. -+ """ -+ # 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.", -+ ) -+ -+ -+@pytest.fixture(scope="session") -+def session_markers_loader(salt_factories): -+ minion_id = "session-markers-minion" -+ overrides = { -+ "file_client": "local", -+ "features": {"enable_slsvars_fixes": True}, -+ } -+ factory = salt_factories.salt_minion_daemon( -+ minion_id, -+ overrides=overrides, -+ ) -+ loader_instance = saltfactories.utils.functional.Loaders(factory.config.copy()) -+ # Sync Everything -+ loader_instance.modules.saltutil.sync_all() -+ # Reload Everything - This is required or custom modules in _modules will not be found -+ loader_instance.reload_all() -+ return loader_instance ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/plugins/sysinfo.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/plugins/sysinfo.py -@@ -0,0 +1,86 @@ -+""" -+.. -+ PYTEST_DONT_REWRITE -+ -+ -+System Information -+================== -+""" -+import io -+import pathlib -+import tempfile -+ -+import pytest -+import salt.config -+import salt.loader -+import salt.utils.yaml -+import salt.version -+ -+ -+def pytest_addoption(parser): -+ """ -+ register argparse-style options and ini-style config values. -+ """ -+ output_options_group = parser.getgroup("Output Options") -+ output_options_group.addoption( -+ "--sys-info", -+ "--sysinfo", -+ default=False, -+ action="store_true", -+ help="Print system information on test session startup", -+ ) -+ -+ -+@pytest.hookimpl(hookwrapper=True, trylast=True) -+def pytest_sessionstart(session): -+ """called after the ``Session`` object has been created and before performing collection -+ and entering the run test loop. -+ -+ :param _pytest.main.Session session: the pytest session object -+ """ -+ # Let PyTest do its own thing -+ yield -+ if session.config.getoption("--sys-info") is True: -+ # And now we add our reporting sections -+ terminal_reporter = session.config.pluginmanager.getplugin("terminalreporter") -+ terminal_reporter.ensure_newline() -+ terminal_reporter.section("System Information", sep=">") -+ terminal_reporter.section("Salt Versions Report", sep="-", bold=True) -+ terminal_reporter.write( -+ "\n".join( -+ " {}".format(line.rstrip()) for line in salt.version.versions_report() -+ ).rstrip() -+ + "\n" -+ ) -+ terminal_reporter.ensure_newline() -+ # System Grains -+ root_dir = pathlib.Path(tempfile.mkdtemp()) -+ conf_file = root_dir / "conf" / "minion" -+ conf_file.parent.mkdir() -+ minion_config_defaults = salt.config.DEFAULT_MINION_OPTS.copy() -+ minion_config_defaults.update( -+ { -+ "id": "saltfactories-reports-minion", -+ "root_dir": str(root_dir), -+ "conf_file": str(conf_file), -+ "cachedir": "cache", -+ "pki_dir": "pki", -+ "file_client": "local", -+ "server_id_use_crc": "adler32", -+ } -+ ) -+ minion_config = salt.config.minion_config(None, defaults=minion_config_defaults) -+ grains = salt.loader.grains(minion_config) -+ grains_output_file = io.StringIO() -+ salt.utils.yaml.safe_dump(grains, grains_output_file, default_flow_style=False) -+ grains_output_file.seek(0) -+ terminal_reporter.section("System Grains Report", sep="-") -+ terminal_reporter.write( -+ "\n".join( -+ " {}".format(line.rstrip()) for line in grains_output_file.read().splitlines() -+ ).rstrip() -+ + "\n" -+ ) -+ terminal_reporter.ensure_newline() -+ terminal_reporter.section("System Information", sep="<") -+ terminal_reporter.ensure_newline() ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/plugins/sysstats.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/plugins/sysstats.py -@@ -0,0 +1,203 @@ -+""" -+ -+System Statistics -+================= -+ -+Process stats PyTest plugin interface -+""" -+import os -+from collections import OrderedDict -+ -+import attr -+import psutil -+import pytest -+from pytestskipmarkers.utils import platform -+ -+ -+@attr.s(kw_only=True, slots=True, hash=True) -+class StatsProcesses: -+ processes = attr.ib(init=False, default=attr.Factory(OrderedDict), hash=False) -+ -+ def add(self, display_name, process): -+ if isinstance(process, int): -+ # This is a process pid -+ process = psutil.Process(process) -+ self.processes[display_name] = process -+ -+ def remove(self, display_name): -+ self.processes.pop(display_name, None) -+ -+ def items(self): -+ return self.processes.items() -+ -+ def __iter__(self): -+ return iter(self.processes) -+ -+ -+@attr.s(kw_only=True, slots=True, hash=True) -+class SystemStatsReporter: -+ -+ config = attr.ib(repr=False, hash=False) -+ stats_processes = attr.ib(repr=False, hash=False) -+ terminalreporter = attr.ib(repr=False, hash=False) -+ show_sys_stats = attr.ib(init=False) -+ sys_stats_no_children = attr.ib(init=False) -+ sys_stats_mem_type = attr.ib(init=False) -+ -+ def __attrs_post_init__(self): -+ self.show_sys_stats = ( -+ self.config.getoption("--sys-stats") is True -+ and self.config.getoption("--no-sys-stats") is False -+ ) -+ self.sys_stats_no_children = self.config.getoption("--sys-stats-no-children") is True -+ if self.config.getoption("--sys-stats-uss-mem") is True: -+ self.sys_stats_mem_type = "uss" -+ if platform.is_freebsd(): -+ # FreeBSD doesn't apparently support uss -+ self.sys_stats_mem_type = "rss" -+ else: -+ self.sys_stats_mem_type = "rss" -+ -+ @pytest.hookimpl(trylast=True) -+ def pytest_runtest_logreport(self, report): -+ if self.terminalreporter.verbosity <= 0: -+ return -+ -+ if report.when != "call": -+ return -+ -+ if self.show_sys_stats is False: -+ return -+ -+ if self.terminalreporter.verbosity > 1: -+ remove_from_stats = set() -+ self.terminalreporter.ensure_newline() -+ self.terminalreporter.section("Processes Statistics", sep="-", bold=True) -+ left_padding = len(max(["System"] + list(self.stats_processes), key=len)) -+ template = ( -+ " ...{dots} {name} - CPU: {cpu:6.2f} % MEM: {mem:6.2f} % (Virtual Memory)" -+ ) -+ -+ stats = { -+ "name": "System", -+ "dots": "." * (left_padding - len("System")), -+ "cpu": psutil.cpu_percent(), -+ "mem": psutil.virtual_memory().percent, -+ } -+ -+ swap = psutil.swap_memory().percent -+ if swap > 0: -+ template += " SWAP: {swap:6.2f} %" -+ stats["swap"] = swap -+ -+ template += "\n" -+ self.terminalreporter.write(template.format(**stats)) -+ -+ template = " ...{dots} {name} - CPU: {cpu:6.2f} % MEM: {mem:6.2f} % ({m_type})" -+ children_template = ( -+ template + " MEM SUM: {c_mem} % ({m_type}) CHILD PROCS: {c_count}\n" -+ ) -+ no_children_template = template + "\n" -+ for name, psproc in self.stats_processes.items(): -+ template = no_children_template -+ dots = "." * (left_padding - len(name)) -+ pids = [] -+ try: -+ with psproc.oneshot(): -+ stats = { -+ "name": name, -+ "dots": dots, -+ "cpu": psproc.cpu_percent(), -+ "mem": psproc.memory_percent(self.sys_stats_mem_type), -+ "m_type": self.sys_stats_mem_type.upper(), -+ } -+ if self.sys_stats_no_children is False: -+ pids.append(psproc.pid) -+ children = psproc.children(recursive=True) -+ if children: -+ template = children_template -+ stats["c_count"] = 0 -+ c_mem = stats["mem"] -+ for child in children: -+ if child.pid in pids: # pragma: no cover -+ continue -+ pids.append(child.pid) -+ if not psutil.pid_exists(child.pid): # pragma: no cover -+ remove_from_stats.add(name) -+ continue -+ try: -+ c_mem += child.memory_percent(self.sys_stats_mem_type) -+ stats["c_count"] += 1 -+ except ( -+ psutil.AccessDenied, -+ psutil.NoSuchProcess, -+ ): # pragma: no cover -+ continue -+ if stats["c_count"]: -+ stats["c_mem"] = "{:6.2f}".format(c_mem) -+ else: -+ template = no_children_template -+ self.terminalreporter.write(template.format(**stats)) -+ except psutil.NoSuchProcess: # pragma: no cover -+ remove_from_stats.add(name) -+ continue -+ if remove_from_stats: # pragma: no cover -+ for name in remove_from_stats: -+ self.stats_processes.remove(name) -+ -+ -+def pytest_addoption(parser): -+ """ -+ register argparse-style options and ini-style config values. -+ """ -+ output_options_group = parser.getgroup("Output Options") -+ output_options_group.addoption( -+ "--sys-stats", -+ default=False, -+ action="store_true", -+ help="Print System CPU and MEM statistics after each test execution.", -+ ) -+ output_options_group.addoption( -+ "--no-sys-stats", -+ default=False, -+ action="store_true", -+ help="Do not print System CPU and MEM statistics after each test execution.", -+ ) -+ output_options_group.addoption( -+ "--sys-stats-no-children", -+ default=False, -+ action="store_true", -+ help="Don't include child processes memory statistics.", -+ ) -+ output_options_group.addoption( -+ "--sys-stats-uss-mem", -+ default=False, -+ action="store_true", -+ help='Use the USS("Unique Set Size", memory unique to a process which would be freed if the process was ' -+ "terminated) memory instead which is more expensive to calculate.", -+ ) -+ -+ -+@pytest.hookimpl(trylast=True) -+def pytest_sessionstart(session): -+ if ( -+ session.config.getoption("--sys-stats") is True -+ and session.config.getoption("--no-sys-stats") is False -+ ): -+ stats_processes = StatsProcesses() -+ stats_processes.add("Test Suite Run", os.getpid()) -+ else: -+ stats_processes = None -+ -+ session.config.pluginmanager.register(stats_processes, "saltfactories-sysstats-processes") -+ -+ terminalreporter = session.config.pluginmanager.getplugin("terminalreporter") -+ sys_stats_reporter = SystemStatsReporter( -+ config=session.config, stats_processes=stats_processes, terminalreporter=terminalreporter -+ ) -+ session.config.pluginmanager.register(sys_stats_reporter, "saltfactories-sysstats-reporter") -+ -+ -+@pytest.fixture(scope="session") -+def stats_processes(request): -+ return request.config.pluginmanager.get_plugin("saltfactories-sysstats-processes") ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/utils/__init__.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/utils/__init__.py -@@ -0,0 +1,67 @@ -+""" -+.. -+ PYTEST_DONT_REWRITE -+ -+Utility functions -+""" -+import random -+import string -+from functools import lru_cache -+ -+import salt.utils.user -+ -+ -+def random_string(prefix, size=6, uppercase=True, lowercase=True, digits=True): -+ """ -+ Generates a random string. -+ -+ :keyword str prefix: The prefix for the random string -+ :keyword int size: The size of the random string -+ :keyword bool uppercase: If true, include upper-cased ascii chars in choice sample -+ :keyword bool lowercase: If true, include lower-cased ascii chars in choice sample -+ :keyword bool digits: If true, include digits in choice sample -+ :return str: The random string -+ """ -+ if not any([uppercase, lowercase, digits]): -+ raise RuntimeError("At least one of 'uppercase', 'lowercase' or 'digits' needs to be true") -+ choices = [] -+ if uppercase: -+ choices.extend(string.ascii_uppercase) -+ if lowercase: -+ choices.extend(string.ascii_lowercase) -+ if digits: -+ choices.extend(string.digits) -+ -+ return prefix + "".join(random.choice(choices) for _ in range(size)) -+ -+ -+@lru_cache(maxsize=1) -+def running_username(): -+ return salt.utils.user.get_user() -+ -+ -+def format_callback_to_string(callback, args=None, kwargs=None): -+ """ -+ Convert a callback, its arguments and keyword arguments to a string suitable for logging purposes -+ -+ :param ~collections.abc.Callable,str callback: -+ The callback function -+ :param list,tuple args: -+ The callback arguments -+ :param dict kwargs: -+ The callback keyword arguments -+ :rtype: str -+ """ -+ if not isinstance(callback, str): -+ try: -+ callback_str = "{}(".format(callback.__qualname__) -+ except AttributeError: -+ callback_str = "{}(".format(callback.__name__) -+ else: -+ callback_str = "{}(".format(callback) -+ if args: -+ callback_str += ", ".join([repr(arg) for arg in args]) -+ if kwargs: -+ callback_str += ", ".join(["{}={!r}".format(k, v) for (k, v) in kwargs.items()]) -+ callback_str += ")" -+ return callback_str ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/utils/cli_scripts.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/utils/cli_scripts.py -@@ -0,0 +1,244 @@ -+""" -+saltfactories.utils.cli_scripts -+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -+ -+Code to generate Salt CLI scripts for test runs -+""" -+import logging -+import pathlib -+import stat -+import textwrap -+ -+import pytest -+ -+log = logging.getLogger(__name__) -+ -+SCRIPT_TEMPLATES = { -+ "salt": textwrap.dedent( -+ """ -+ import atexit -+ import traceback -+ from salt.scripts import salt_main -+ -+ if __name__ == '__main__': -+ exitcode = 0 -+ try: -+ salt_main() -+ except SystemExit as exc: -+ exitcode = exc.code -+ # https://docs.python.org/3/library/exceptions.html#SystemExit -+ if exitcode is None: -+ exitcode = 0 -+ if not isinstance(exitcode, int): -+ # A string?! -+ sys.stderr.write(exitcode) -+ exitcode = 1 -+ except Exception as exc: -+ sys.stderr.write( -+ "An un-handled exception was caught: " + str(exc) + "\\n" + traceback.format_exc() -+ ) -+ exitcode = 1 -+ sys.stdout.flush() -+ sys.stderr.flush() -+ atexit._run_exitfuncs() -+ os._exit(exitcode) -+ """ -+ ), -+ "salt-api": textwrap.dedent( -+ """ -+ import atexit -+ import traceback -+ import salt.cli.api -+ import salt.utils.process -+ -+ salt.utils.process.notify_systemd() -+ -+ def main(): -+ sapi = salt.cli.api.SaltAPI() -+ sapi.start() -+ -+ if __name__ == '__main__': -+ exitcode = 0 -+ try: -+ main() -+ except SystemExit as exc: -+ exitcode = exc.code -+ # https://docs.python.org/3/library/exceptions.html#SystemExit -+ if exitcode is None: -+ exitcode = 0 -+ if not isinstance(exitcode, int): -+ # A string?! -+ sys.stderr.write(exitcode) -+ exitcode = 1 -+ except Exception as exc: -+ sys.stderr.write( -+ "An un-handled exception was caught: " + str(exc) + "\\n" + traceback.format_exc() -+ ) -+ exitcode = 1 -+ sys.stdout.flush() -+ sys.stderr.flush() -+ atexit._run_exitfuncs() -+ os._exit(exitcode) -+ """ -+ ), -+ "common": textwrap.dedent( -+ """ -+ import atexit -+ import traceback -+ from salt.scripts import salt_{0} -+ -+ def main(): -+ if sys.platform.startswith("win"): -+ import os.path -+ import py_compile -+ cfile = os.path.splitext(__file__)[0] + '.pyc' -+ if not os.path.exists(cfile): -+ py_compile.compile(__file__, cfile) -+ salt_{0}() -+ -+ if __name__ == '__main__': -+ exitcode = 0 -+ try: -+ main() -+ except SystemExit as exc: -+ exitcode = exc.code -+ # https://docs.python.org/3/library/exceptions.html#SystemExit -+ if exitcode is None: -+ exitcode = 0 -+ if not isinstance(exitcode, int): -+ # A string?! -+ sys.stderr.write(exitcode) -+ exitcode = 1 -+ except Exception as exc: -+ sys.stderr.write( -+ "An un-handled exception was caught: " + str(exc) + "\\n" + traceback.format_exc() -+ ) -+ exitcode = 1 -+ sys.stdout.flush() -+ sys.stderr.flush() -+ atexit._run_exitfuncs() -+ os._exit(exitcode) -+ """ -+ ), -+ "coverage": textwrap.dedent( -+ """ -+ # Setup coverage environment variables -+ COVERAGE_FILE = os.path.join(CODE_DIR, '.coverage') -+ COVERAGE_PROCESS_START = os.path.join(CODE_DIR, '.coveragerc') -+ os.environ[str('COVERAGE_FILE')] = str(COVERAGE_FILE) -+ os.environ[str('COVERAGE_PROCESS_START')] = str(COVERAGE_PROCESS_START) -+ """ -+ ), -+ "sitecustomize": textwrap.dedent( -+ """ -+ # Allow sitecustomize.py to be importable for test coverage purposes -+ SITECUSTOMIZE_DIR = r'{sitecustomize_dir}' -+ PYTHONPATH = os.environ.get('PYTHONPATH') or None -+ if PYTHONPATH is None: -+ PYTHONPATH_ENV_VAR = SITECUSTOMIZE_DIR -+ else: -+ PYTHON_PATH_ENTRIES = PYTHONPATH.split(os.pathsep) -+ if SITECUSTOMIZE_DIR in PYTHON_PATH_ENTRIES: -+ PYTHON_PATH_ENTRIES.remove(SITECUSTOMIZE_DIR) -+ PYTHON_PATH_ENTRIES.insert(0, SITECUSTOMIZE_DIR) -+ PYTHONPATH_ENV_VAR = os.pathsep.join(PYTHON_PATH_ENTRIES) -+ os.environ[str('PYTHONPATH')] = str(PYTHONPATH_ENV_VAR) -+ if SITECUSTOMIZE_DIR in sys.path: -+ sys.path.remove(SITECUSTOMIZE_DIR) -+ sys.path.insert(0, SITECUSTOMIZE_DIR) -+ """ -+ ), -+} -+ -+ -+def generate_script( -+ bin_dir, -+ script_name, -+ code_dir=None, -+ inject_coverage=False, -+ inject_sitecustomize=False, -+): -+ """ -+ Generate a CLI script -+ :param ~pathlib.Path bin_dir: The path to the directory which will contain the CLI scripts -+ :param str script_name: The CLI script name -+ :param ~pathlib.Path code_dir: The project's being tested root directory path -+ :param bool inject_coverage: Inject code to track code coverage -+ :param bool inject_sitecustomize: Inject code to support code coverage in subprocesses -+ """ -+ if isinstance(bin_dir, str): -+ bin_dir = pathlib.Path(bin_dir) -+ bin_dir.mkdir(exist_ok=True) -+ -+ cli_script_name = "cli_{}.py".format(script_name.replace("-", "_")) -+ script_path = bin_dir / cli_script_name -+ -+ if not script_path.is_file(): -+ log.info("Generating %s", script_path) -+ -+ with script_path.open("w") as sfh: -+ script_template = SCRIPT_TEMPLATES.get(script_name, None) -+ if script_template is None: -+ script_template = SCRIPT_TEMPLATES.get("common", None) -+ -+ script_contents = ( -+ textwrap.dedent( -+ """ -+ from __future__ import absolute_import -+ import os -+ import sys -+ -+ # We really do not want buffered output -+ os.environ[str("PYTHONUNBUFFERED")] = str("1") -+ # Don't write .pyc files or create them in __pycache__ directories -+ os.environ[str("PYTHONDONTWRITEBYTECODE")] = str("1") -+ """ -+ ).strip() -+ + "\n\n" -+ ) -+ -+ if code_dir: -+ script_contents += ( -+ textwrap.dedent( -+ """ -+ CODE_DIR = r'{code_dir}' -+ if CODE_DIR in sys.path: -+ sys.path.remove(CODE_DIR) -+ sys.path.insert(0, CODE_DIR)""".format( -+ code_dir=code_dir -+ ) -+ ).strip() -+ + "\n\n" -+ ) -+ -+ if inject_coverage and not code_dir: -+ raise pytest.UsageError( -+ "The inject coverage code needs to know the code root to find the " -+ "path to the '.coveragerc' file. Please pass 'code_dir'." -+ ) -+ if inject_coverage: -+ script_contents += SCRIPT_TEMPLATES["coverage"].strip() + "\n\n" -+ -+ if inject_sitecustomize: -+ script_contents += ( -+ SCRIPT_TEMPLATES["sitecustomize"] -+ .format( -+ sitecustomize_dir=str(pathlib.Path(__file__).resolve().parent / "coverage") -+ ) -+ .strip() -+ + "\n\n" -+ ) -+ -+ script_contents += ( -+ script_template.format(script_name.replace("salt-", "").replace("-", "_")).strip() -+ + "\n" -+ ) -+ sfh.write(script_contents) -+ log.debug( -+ "Wrote the following contents to temp script %s:\n%s", script_path, script_contents -+ ) -+ fst = script_path.stat() -+ script_path.chmod(fst.st_mode | stat.S_IEXEC) -+ -+ log.info("Returning script path %r", script_path) -+ return str(script_path) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/utils/coverage/sitecustomize.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/utils/coverage/sitecustomize.py -@@ -0,0 +1,6 @@ -+try: -+ import coverage -+ -+ coverage.process_startup() -+except ImportError: -+ pass ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/utils/functional.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/utils/functional.py -@@ -0,0 +1,503 @@ -+""" -+saltfactories.utils.functional -+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -+ -+Salt functional testing support -+""" -+import copy -+import logging -+import operator -+ -+import attr -+import salt.loader -+import salt.pillar -+ -+from saltfactories.utils import format_callback_to_string -+ -+try: -+ import salt.features # pylint: disable=ungrouped-imports -+ -+ HAS_SALT_FEATURES = True -+except ImportError: # pragma: no cover -+ HAS_SALT_FEATURES = False -+ -+log = logging.getLogger(__name__) -+ -+ -+class Loaders: -+ """ -+ This class provides the required functionality for functional testing against the salt loaders -+ -+ :param dict opts: -+ The options dictionary to load the salt loaders. -+ -+ Example usage: -+ -+ .. code-block:: python -+ -+ import salt.config -+ from saltfactories.utils.functional import Loaders -+ -+ -+ @pytest.fixture(scope="module") -+ def minion_opts(): -+ return salt.config.minion_config(None) -+ -+ -+ @pytest.fixture(scope="module") -+ def loaders(minion_opts): -+ return Loaders(minion_opts) -+ -+ -+ @pytest.fixture(autouse=True) -+ def reset_loaders_state(loaders): -+ try: -+ # Run the tests -+ yield -+ finally: -+ # Reset the loaders state -+ loaders.reset_state() -+ """ -+ -+ def __init__(self, opts): -+ self.opts = opts -+ self.context = {} -+ self._original_opts = copy.deepcopy(opts) -+ self._reset_state_funcs = [self.context.clear] -+ self._reload_all_funcs = [self.reset_state] -+ self._grains = None -+ self._modules = None -+ self._pillar = None -+ self._serializers = None -+ self._states = None -+ self._utils = None -+ if HAS_SALT_FEATURES: -+ salt.features.setup_features(self.opts) -+ self.reload_all() -+ # Force the minion to populate it's cache if need be -+ self.modules.saltutil.sync_all() -+ # Now reload again so that the loader takes into account said cache -+ self.reload_all() -+ -+ def reset_state(self): -+ for func in self._reset_state_funcs: -+ func() -+ -+ def reload_all(self): -+ for func in self._reload_all_funcs: -+ try: -+ func() -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ log.warning("Failed to run '%s': %s", func.__name__, exc, exc_info=True) -+ self.opts = copy.deepcopy(self._original_opts) -+ self._grains = None -+ self._modules = None -+ self._pillar = None -+ self._serializers = None -+ self._states = None -+ self._utils = None -+ self.opts["grains"] = self.grains -+ self.refresh_pillar() -+ -+ @property -+ def grains(self): -+ """ -+ The grains loaded by the salt loader -+ """ -+ if self._grains is None: -+ self._grains = salt.loader.grains(self.opts, context=self.context) -+ return self._grains -+ -+ @property -+ def utils(self): -+ """ -+ The utils loaded by the salt loader -+ """ -+ if self._utils is None: -+ self._utils = salt.loader.utils(self.opts, context=self.context) -+ return self._utils -+ -+ @property -+ def modules(self): -+ """ -+ The execution modules loaded by the salt loader -+ """ -+ if self._modules is None: -+ _modules = salt.loader.minion_mods( -+ self.opts, context=self.context, utils=self.utils, initial_load=True -+ ) -+ -+ if isinstance(_modules.loaded_modules, dict): -+ for func_name in ("single", "sls", "template", "template_str"): -+ full_func_name = "state.{}".format(func_name) -+ -+ if func_name == "single": -+ wrapper_cls = StateResult -+ else: -+ wrapper_cls = MultiStateResult -+ replacement_function = StateModuleFuncWrapper( -+ _modules[full_func_name], wrapper_cls -+ ) -+ -+ _modules._dict[full_func_name] = replacement_function -+ _modules.loaded_modules["state"][func_name] = replacement_function -+ setattr( -+ _modules.loaded_modules["state"], -+ func_name, -+ replacement_function, -+ ) -+ else: -+ # Newer version of Salt where only one dictionary with the loaded functions is maintained -+ -+ class ModulesLoaderDict(_modules.mod_dict_class): -+ def __setitem__(self, key, value): -+ """ -+ We hijack __setitem__ so that we can replace specific state functions with a -+ wrapper which will return a more pythonic data structure to assert against. -+ """ -+ -+ if key in ( -+ "state.single", -+ "state.sls", -+ "state.template", -+ "state.template_str", -+ ): -+ if key == "state.single": -+ wrapper_cls = StateResult -+ else: -+ wrapper_cls = MultiStateResult -+ value = StateModuleFuncWrapper(value, wrapper_cls) -+ return super().__setitem__(key, value) -+ -+ loader_dict = _modules._dict.copy() -+ _modules._dict = ModulesLoaderDict() -+ for key, value in loader_dict.items(): -+ _modules._dict[key] = value -+ -+ self._modules = _modules -+ return self._modules -+ -+ @property -+ def serializers(self): -+ """ -+ The serializers loaded by the salt loader -+ """ -+ if self._serializers is None: -+ self._serializers = salt.loader.serializers(self.opts) -+ return self._serializers -+ -+ @property -+ def states(self): -+ """ -+ The state modules loaded by the salt loader -+ """ -+ if self._states is None: -+ _states = salt.loader.states( -+ self.opts, -+ functions=self.modules, -+ utils=self.utils, -+ serializers=self.serializers, -+ context=self.context, -+ ) -+ # For state execution modules, because we'd have to almost copy/paste what salt.modules.state.single -+ # does, we actually "proxy" the call through salt.modules.state.single instead of calling the state -+ # execution modules directly. This was also how the non pytest test suite worked -+ # Let's load all modules now -+ -+ # Now, we proxy loaded modules through salt.modules.state.single -+ if isinstance(_states.loaded_modules, dict): -+ # Old Salt? -+ _states._load_all() -+ for module_name in list(_states.loaded_modules): -+ for func_name in list(_states.loaded_modules[module_name]): -+ full_func_name = "{}.{}".format(module_name, func_name) -+ replacement_function = StateFunction( -+ self.modules.state.single, full_func_name -+ ) -+ _states._dict[full_func_name] = replacement_function -+ _states.loaded_modules[module_name][func_name] = replacement_function -+ setattr( -+ _states.loaded_modules[module_name], -+ func_name, -+ replacement_function, -+ ) -+ else: -+ # Newer version of Salt where only one dictionary with the loaded functions is maintained -+ -+ class StatesLoaderDict(_states.mod_dict_class): -+ def __init__(self, proxy_func, *args, **kwargs): -+ super().__init__(*args, **kwargs) -+ self.__proxy_func__ = proxy_func -+ -+ def __setitem__(self, name, func): -+ """ -+ We hijack __setitem__ so that we can replace the loaded functions -+ with a wrapper -+ For state execution modules, because we'd have to almost copy/paste what -+ ``salt.modules.state.single`` does, we actually "proxy" the call through -+ ``salt.modules.state.single`` instead of calling the state execution -+ modules directly. This was also how the non pytest test suite worked -+ """ -+ func = StateFunction(self.__proxy_func__, name) -+ return super().__setitem__(name, func) -+ -+ loader_dict = _states._dict.copy() -+ _states._dict = StatesLoaderDict(self.modules.state.single) -+ for key, value in loader_dict.items(): -+ _states._dict[key] = value -+ -+ self._states = _states -+ return self._states -+ -+ @property -+ def pillar(self): -+ """ -+ The pillar loaded by the salt loader -+ """ -+ if self._pillar is None: -+ self._pillar = salt.pillar.get_pillar( -+ self.opts, -+ self.grains, -+ self.opts["id"], -+ saltenv=self.opts["saltenv"], -+ pillarenv=self.opts.get("pillarenv"), -+ ).compile_pillar() -+ return self._pillar -+ -+ def refresh_pillar(self): -+ self._pillar = None -+ self.opts["pillar"] = self.pillar -+ -+ -+@attr.s -+class StateResult: -+ """ -+ This class wraps a single salt state return into a more pythonic object in order to simplify assertions -+ -+ :param dict raw: -+ A single salt state return result -+ -+ .. code-block:: python -+ -+ def test_user_absent(loaders): -+ ret = loaders.states.user.absent(name=random_string("account-", uppercase=False)) -+ assert ret.result is True -+ """ -+ -+ raw = attr.ib() -+ state_id = attr.ib(init=False) -+ full_return = attr.ib(init=False) -+ filtered = attr.ib(init=False) -+ -+ @state_id.default -+ def _state_id(self): -+ if not isinstance(self.raw, dict): -+ raise ValueError("The state result errored: {}".format(self.raw)) -+ return next(iter(self.raw.keys())) -+ -+ @full_return.default -+ def _full_return(self): -+ return self.raw[self.state_id] -+ -+ @filtered.default -+ def _filtered_default(self): -+ _filtered = {} -+ for key, value in self.full_return.items(): -+ if key.startswith("_") or key in ("duration", "start_time"): -+ continue -+ _filtered[key] = value -+ return _filtered -+ -+ @property -+ def run_num(self): -+ """ -+ The ``__run_num__`` key on the full state return dictionary -+ """ -+ return self.full_return["__run_num__"] or 0 -+ -+ @property -+ def name(self): -+ """ -+ The ``name`` key on the full state return dictionary -+ """ -+ return self.full_return["name"] -+ -+ @property -+ def result(self): -+ """ -+ The ``result`` key on the full state return dictionary -+ """ -+ return self.full_return["result"] -+ -+ @property -+ def changes(self): -+ """ -+ The ``changes`` key on the full state return dictionary -+ """ -+ return self.full_return["changes"] -+ -+ @property -+ def comment(self): -+ """ -+ The ``comment`` key on the full state return dictionary -+ """ -+ return self.full_return["comment"] -+ -+ @property -+ def warnings(self): -+ """ -+ The ``warnings`` key on the full state return dictionary -+ """ -+ return self.full_return.get("warnings") or [] -+ -+ def __contains__(self, key): -+ """ -+ Checks for the existence of ``key`` in the full state return dictionary -+ """ -+ return key in self.full_return -+ -+ def __eq__(self, _): -+ raise TypeError( -+ "Please assert comparisons with {}.filtered instead".format(self.__class__.__name__) -+ ) -+ -+ def __bool__(self): -+ raise TypeError( -+ "Please assert comparisons with {}.filtered instead".format(self.__class__.__name__) -+ ) -+ -+ -+@attr.s -+class StateFunction: -+ """ -+ Simple wrapper around Salt's state execution module functions which actually proxies the call -+ through Salt's ``state.single`` execution module -+ """ -+ -+ proxy_func = attr.ib(repr=False) -+ state_func = attr.ib() -+ -+ def __call__(self, *args, **kwargs): -+ log.info( -+ "Calling %s", -+ format_callback_to_string("state.single", (self.state_func,) + args, kwargs), -+ ) -+ return self.proxy_func(self.state_func, *args, **kwargs) -+ -+ -+@attr.s -+class MultiStateResult: -+ ''' -+ This class wraps multiple salt state returns, for example, running the ``state.sls`` execution module, -+ into a more pythonic object in order to simplify assertions -+ -+ :param dict,list raw: -+ The multiple salt state returns result, a dictionary on success or a list on failure -+ -+ Example usage on the test suite: -+ -+ .. code-block:: python -+ -+ def test_issue_1876_syntax_error(loaders, state_tree, tmp_path): -+ testfile = tmp_path / "issue-1876.txt" -+ sls_contents = """ -+ {}: -+ file: -+ - managed -+ - source: salt://testfile -+ -+ file.append: -+ - text: foo -+ """.format( -+ testfile -+ ) -+ with pytest.helpers.temp_file("issue-1876.sls", sls_contents, state_tree): -+ ret = loaders.modules.state.sls("issue-1876") -+ assert ret.failed -+ errmsg = ( -+ "ID '{}' in SLS 'issue-1876' contains multiple state declarations of the" -+ " same type".format(testfile) -+ ) -+ assert errmsg in ret.errors -+ -+ -+ def test_pydsl(loaders, state_tree, tmp_path): -+ testfile = tmp_path / "testfile" -+ sls_contents = """ -+ #!pydsl -+ -+ state("{}").file("touch") -+ """.format( -+ testfile -+ ) -+ with pytest.helpers.temp_file("pydsl.sls", sls_contents, state_tree): -+ ret = loaders.modules.state.sls("pydsl") -+ for staterun in ret: -+ assert staterun.result is True -+ assert testfile.exists() -+ ''' -+ -+ raw = attr.ib() -+ _structured = attr.ib(init=False) -+ -+ @_structured.default -+ def _set_structured(self): -+ if self.failed: -+ return [] -+ state_result = [StateResult({state_id: data}) for state_id, data in self.raw.items()] -+ return sorted(state_result, key=operator.attrgetter("run_num")) -+ -+ def __iter__(self): -+ return iter(self._structured) -+ -+ def __contains__(self, key): -+ for state_result in self: -+ if state_result.state_id == key: -+ return True -+ return False -+ -+ def __getitem__(self, state_id_or_index): -+ if isinstance(state_id_or_index, int): -+ # We're trying to get the state run by index -+ return self._structured[state_id_or_index] -+ for state_result in self: -+ if state_result.state_id == state_id_or_index: -+ return state_result -+ raise KeyError("No state by the ID of '{}' was found".format(state_id_or_index)) -+ -+ @property -+ def failed(self): -+ """ -+ Return ``True`` or ``False`` if the multiple state run was not successful -+ """ -+ return isinstance(self.raw, list) -+ -+ @property -+ def errors(self): -+ """ -+ Return the list of errors in case the multiple state run was not successful -+ """ -+ if not self.failed: -+ return [] -+ return list(self.raw) -+ -+ -+@attr.s(frozen=True) -+class StateModuleFuncWrapper: -+ """ -+ This class simply wraps a single or multiple state returns into a more pythonic object, -+ :py:class:`~saltfactories.utils.functional.StateResult` or -+ py:class:`~saltfactories.utils.functional.MultiStateResult` -+ -+ :param callable func: -+ A salt loader function -+ :param ~saltfactories.utils.functional.StateResult,~saltfactories.utils.functional.MultiStateResult wrapper: -+ The wrapper to use for the return of the salt loader function's return -+ """ -+ -+ func = attr.ib() -+ wrapper = attr.ib() -+ -+ def __call__(self, *args, **kwargs): -+ ret = self.func(*args, **kwargs) -+ return self.wrapper(ret) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/utils/loader.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/utils/loader.py -@@ -0,0 +1,187 @@ -+""" -+saltfactories.utils.loader -+~~~~~~~~~~~~~~~~~~~~~~~~~~ -+ -+Salt's Loader PyTest Mock Support -+ -+""" -+import logging -+import sys -+import types -+from collections import deque -+from unittest.mock import patch -+ -+import attr -+import pytest -+ -+from saltfactories.utils import format_callback_to_string -+ -+log = logging.getLogger(__name__) -+ -+ -+@attr.s(init=True, slots=True, frozen=True) -+class LoaderModuleMock: -+ -+ setup_loader_modules = attr.ib(init=True) -+ # These dunders should always exist at the module global scope -+ salt_module_dunders = attr.ib( -+ init=True, -+ repr=False, -+ kw_only=True, -+ default=( -+ "__opts__", -+ "__salt__", -+ "__runner__", -+ "__context__", -+ "__utils__", -+ "__ext_pillar__", -+ "__thorium__", -+ "__states__", -+ "__serializers__", -+ "__ret__", -+ "__grains__", -+ "__pillar__", -+ "__sdb__", -+ ), -+ ) -+ # These dunders might exist at the module global scope -+ salt_module_dunders_optional = attr.ib( -+ init=True, -+ repr=False, -+ kw_only=True, -+ default=("__proxy__",), -+ ) -+ # These dunders might exist at the function global scope -+ salt_module_dunder_attributes = attr.ib( -+ init=True, -+ repr=False, -+ kw_only=True, -+ default=( -+ # Salt states attributes -+ "__env__", -+ "__low__", -+ "__instance_id__", -+ "__orchestration_jid__", -+ # Salt runners attributes -+ "__jid_event__", -+ # Salt cloud attributes -+ "__active_provider_name__", -+ # Proxy Minions -+ "__proxyenabled__", -+ ), -+ ) -+ _finalizers = attr.ib(init=False, repr=False, hash=False, default=attr.Factory(deque)) -+ -+ def start(self): -+ module_globals = {dunder: {} for dunder in self.salt_module_dunders} -+ for module, globals_to_mock in self.setup_loader_modules.items(): -+ log.trace("Setting up loader globals for %s; globals: %s", module, globals_to_mock) -+ if not isinstance(module, types.ModuleType): -+ raise pytest.UsageError( -+ "The dictionary keys returned by setup_loader_modules() " -+ "must be an imported module, not {}".format(type(module)) -+ ) -+ if not isinstance(globals_to_mock, dict): -+ raise pytest.UsageError( -+ "The dictionary values returned by setup_loader_modules() " -+ "must be a dictionary, not {}".format(type(globals_to_mock)) -+ ) -+ for key in self.salt_module_dunders: -+ if not hasattr(module, key): -+ # Set the dunder name as an attribute on the module if not present -+ setattr(module, key, {}) -+ # Remove the added attribute after the test finishes -+ self.addfinalizer(delattr, module, key) -+ -+ # Patch sys.modules as the first step -+ self._patch_sys_modules(globals_to_mock) -+ -+ # Now patch the module globals -+ # We actually want to grab a copy of the module globals so that if mocking -+ # multiple modules, and at least one of the modules has a function to path, -+ # the patch only happens on the module it's supposed to patch and not all of them. -+ # It's not a deepcopy because we want to maintain the reference to the salt dunders -+ # added in the start of this function -+ self._patch_module_globals(module, globals_to_mock, module_globals.copy()) -+ -+ def stop(self): -+ while self._finalizers: -+ func, args, kwargs = self._finalizers.popleft() -+ func_repr = format_callback_to_string(func, args, kwargs) -+ try: -+ log.trace("Calling finalizer %s", func_repr) -+ func(*args, **kwargs) -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ log.error( -+ "Failed to run finalizer %s: %s", -+ func_repr, -+ exc, -+ exc_info=True, -+ ) -+ -+ def addfinalizer(self, func, *args, **kwargs): -+ """ -+ Register a function to run when stopping -+ """ -+ self._finalizers.append((func, args, kwargs)) -+ -+ def _patch_sys_modules(self, mocks): -+ if "sys.modules" not in mocks: -+ return -+ sys_modules = mocks["sys.modules"] -+ if not isinstance(sys_modules, dict): -+ raise pytest.UsageError( -+ "'sys.modules' must be a dictionary not: {}".format(type(sys_modules)) -+ ) -+ patcher = patch.dict(sys.modules, values=sys_modules) -+ patcher.start() -+ self.addfinalizer(patcher.stop) -+ -+ def _patch_module_globals(self, module, mocks, module_globals): -+ salt_dunder_dicts = self.salt_module_dunders + self.salt_module_dunders_optional -+ allowed_salt_dunders = salt_dunder_dicts + self.salt_module_dunder_attributes -+ for key in mocks: -+ if key == "sys.modules": -+ # sys.modules is addressed on another function -+ continue -+ -+ if key.startswith("__"): -+ if key in ("__init__", "__virtual__"): -+ raise pytest.UsageError( -+ "No need to patch {!r}. Passed loader module dict: {}".format( -+ key, -+ self.setup_loader_modules, -+ ) -+ ) -+ elif key not in allowed_salt_dunders: -+ raise pytest.UsageError( -+ "Don't know how to handle {!r}. Passed loader module dict: {}".format( -+ key, -+ self.setup_loader_modules, -+ ) -+ ) -+ elif key in salt_dunder_dicts and not hasattr(module, key): -+ # Add the key as a dictionary attribute to the module so it can be patched by `patch.dict`' -+ setattr(module, key, {}) -+ # Remove the added attribute after the test finishes -+ self.addfinalizer(delattr, module, key) -+ -+ if not hasattr(module, key): -+ # Set the key as an attribute so it can be patched -+ setattr(module, key, None) -+ # Remove the added attribute after the test finishes -+ self.addfinalizer(delattr, module, key) -+ module_globals[key] = mocks[key] -+ -+ # Patch the module! -+ log.trace("Patching globals for %s; globals: %s", module, module_globals) -+ patcher = patch.multiple(module, **module_globals) -+ patcher.start() -+ self.addfinalizer(patcher.stop) -+ -+ def __enter__(self): -+ self.start() -+ return self -+ -+ def __exit__(self, *args): -+ self.stop() ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/utils/markers.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/utils/markers.py -@@ -0,0 +1,124 @@ -+""" -+.. -+ PYTEST_DONT_REWRITE -+ -+ -+saltfactories.utils.markers -+~~~~~~~~~~~~~~~~~~~~~~~~~~~ -+ -+PyTest Markers related utilities -+""" -+import fnmatch -+import logging -+ -+import pytest -+ -+log = logging.getLogger(__name__) -+ -+ -+def check_required_loader_attributes(loader_instance, loader_attr, required_items): -+ """ -+ :type loader_instance: ~saltfactories.utils.functional.Loaders -+ :param loader_instance: -+ An instance of :py:class:`~saltfactories.utils.functional.Loaders` -+ :param str loader_attr: -+ The name of the minion attribute to check, such as 'modules' or 'states' -+ :param tuple required_items: -+ The items that must be part of the loader attribute for the decorated test -+ :return: The modules that are not available -+ :rtype: set -+ -+ """ -+ required_salt_items = set(required_items) -+ available_items = list(getattr(loader_instance, loader_attr)) -+ not_available_items = set() -+ -+ name = "__not_available_{items}s__".format(items=loader_attr) -+ if not hasattr(loader_instance, name): -+ cached_not_available_items = set() -+ setattr(loader_instance, name, cached_not_available_items) -+ loader_instance._reload_all_funcs.append(cached_not_available_items.clear) -+ else: -+ cached_not_available_items = getattr(loader_instance, name) -+ -+ for not_available_item in cached_not_available_items: -+ if not_available_item in required_salt_items: -+ not_available_items.add(not_available_item) -+ required_salt_items.remove(not_available_item) -+ -+ for required_item_name in required_salt_items: -+ search_name = required_item_name -+ if "." not in search_name: -+ search_name += ".*" -+ if not fnmatch.filter(available_items, search_name): -+ not_available_items.add(required_item_name) -+ cached_not_available_items.add(required_item_name) -+ -+ return not_available_items -+ -+ -+def evaluate_markers(item): -+ """ -+ Fixtures injection based on markers or test skips based on CLI arguments -+ """ -+ -+ # Two special markers, requires_salt_modules and requires_salt_states. These need access to a -+ # saltfactories.utils.functional.Loader instance -+ # They will use a session_markers_loader fixture to gain access to that -+ requires_salt_modules_marker = item.get_closest_marker("requires_salt_modules") -+ if requires_salt_modules_marker is not None: -+ if requires_salt_modules_marker.kwargs: -+ raise pytest.UsageError( -+ "The 'required_salt_modules' marker does not accept keyword arguments" -+ ) -+ required_salt_modules = requires_salt_modules_marker.args -+ if not required_salt_modules: -+ raise pytest.UsageError( -+ "The 'required_salt_modules' marker needs at least one module name to be passed" -+ ) -+ for arg in required_salt_modules: -+ if not isinstance(arg, str): -+ raise pytest.UsageError( -+ "The 'required_salt_modules' marker only accepts strings as arguments" -+ ) -+ session_markers_loader = item._request.getfixturevalue("session_markers_loader") -+ required_salt_modules = set(required_salt_modules) -+ not_available_modules = check_required_loader_attributes( -+ session_markers_loader, "modules", 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: -+ if requires_salt_states_marker.kwargs: -+ raise pytest.UsageError( -+ "The 'required_salt_states' marker does not accept keyword arguments" -+ ) -+ required_salt_states = requires_salt_states_marker.args -+ if not required_salt_states: -+ raise pytest.UsageError( -+ "The 'required_salt_states' marker needs at least one state module name to be passed" -+ ) -+ for arg in required_salt_states: -+ if not isinstance(arg, str): -+ raise pytest.UsageError( -+ "The 'required_salt_states' marker only accepts strings as arguments" -+ ) -+ session_markers_loader = item._request.getfixturevalue("session_markers_loader") -+ required_salt_states = set(required_salt_states) -+ not_available_states = check_required_loader_attributes( -+ session_markers_loader, "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)) -+ ) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/utils/ports.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/utils/ports.py -@@ -0,0 +1,64 @@ -+""" -+saltfactories.utils.ports -+~~~~~~~~~~~~~~~~~~~~~~~~~ -+ -+Ports related utility functions -+""" -+import contextlib -+import logging -+ -+import pytest -+ -+from saltfactories.utils import socket -+ -+log = logging.getLogger(__name__) -+ -+ -+def get_unused_localhost_port(use_cache=False): -+ """ -+ :keyword bool use_cache: -+ If ``use_cache`` is ``True``, consecutive calls to this function will never return the cached port. -+ -+ Return a random unused port on localhost -+ """ -+ if not isinstance(use_cache, bool): -+ raise pytest.UsageError( -+ "The value of 'use_cache' needs to be an boolean, not {}".format(type(use_cache)) -+ ) -+ -+ with contextlib.closing(socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)) as usock: -+ usock.bind(("127.0.0.1", 0)) -+ port = usock.getsockname()[1] -+ -+ if use_cache: -+ try: -+ cached_ports = get_unused_localhost_port.__cached_ports__ -+ except AttributeError: -+ cached_ports = get_unused_localhost_port.__cached_ports__ = set() -+ if port in cached_ports: -+ return get_unused_localhost_port(use_cache=use_cache) -+ cached_ports.add(port) -+ -+ return port -+ -+ -+def get_connectable_ports(ports): -+ """ -+ :param ~collections.abc.Iterable ports: An iterable of ports to try and connect to -+ :rtype: set -+ :return: Returns a set of the ports where connection was successful -+ """ -+ connectable_ports = set() -+ ports = set(ports) -+ -+ for port in set(ports): -+ with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: -+ conn = sock.connect_ex(("localhost", port)) -+ try: -+ if conn == 0: -+ log.debug("Port %s is connectable!", port) -+ connectable_ports.add(port) -+ sock.shutdown(socket.SHUT_RDWR) -+ except OSError: -+ continue -+ return connectable_ports ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/utils/processes.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/utils/processes.py -@@ -0,0 +1,311 @@ -+""" -+saltfactories.utils.processes -+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -+ -+Process related utilities -+""" -+import errno -+import logging -+import pprint -+import signal -+import weakref -+ -+import attr -+import psutil -+ -+ -+log = logging.getLogger(__name__) -+ -+ -+@attr.s(frozen=True) -+class ProcessResult: -+ """ -+ This class serves the purpose of having a common result class which will hold the -+ resulting data from a subprocess command. -+ -+ :keyword int exitcode: -+ The exitcode returned by the process -+ :keyword str stdout: -+ The ``stdout`` returned by the process -+ :keyword str stderr: -+ The ``stderr`` returned by the process -+ :keyword list,tuple cmdline: -+ The command line used to start the process -+ -+ .. admonition:: Note -+ -+ Cast :py:class:`~saltfactories.utils.processes.ProcessResult` to a string to pretty-print it. -+ """ -+ -+ exitcode = attr.ib() -+ stdout = attr.ib() -+ stderr = attr.ib() -+ cmdline = attr.ib(default=None, kw_only=True) -+ -+ @exitcode.validator -+ def _validate_exitcode(self, attribute, value): -+ if not isinstance(value, int): -+ raise ValueError("'exitcode' needs to be an integer, not '{}'".format(type(value))) -+ -+ def __str__(self): -+ message = self.__class__.__name__ -+ if self.cmdline: -+ message += "\n Command Line: {}".format(self.cmdline) -+ if self.exitcode is not None: -+ message += "\n Exitcode: {}".format(self.exitcode) -+ if self.stdout or self.stderr: -+ message += "\n Process Output:" -+ if self.stdout: -+ message += "\n >>>>> STDOUT >>>>>\n{}\n <<<<< STDOUT <<<<<".format(self.stdout) -+ if self.stderr: -+ message += "\n >>>>> STDERR >>>>>\n{}\n <<<<< STDERR <<<<<".format(self.stderr) -+ return message + "\n" -+ -+ -+@attr.s(frozen=True) -+class ShellResult(ProcessResult): -+ """ -+ This class serves the purpose of having a common result class which will hold the -+ resulting data from a subprocess command. -+ -+ :keyword dict json: -+ The dictionary returned from the process ``stdout`` if it could JSON decode it. -+ -+ Please look at :py:class:`~saltfactories.utils.processes.ProcessResult` for the additional supported keyword -+ arguments documentation. -+ """ -+ -+ json = attr.ib(default=None, kw_only=True) -+ -+ def __str__(self): -+ message = super().__str__().rstrip() -+ if self.json: -+ message += "\n JSON Object:\n" -+ message += "".join(" {}".format(line) for line in pprint.pformat(self.json)) -+ return message + "\n" -+ -+ def __eq__(self, other): -+ """ -+ Allow comparison against the parsed JSON or the output -+ """ -+ if self.json: -+ return self.json == other -+ return self.stdout == other -+ -+ -+def collect_child_processes(pid): -+ """ -+ Try to collect any started child processes of the provided pid -+ -+ :param int pid: -+ The PID of the process -+ """ -+ # Let's get the child processes of the started subprocess -+ try: -+ parent = psutil.Process(pid) -+ children = parent.children(recursive=True) -+ except psutil.NoSuchProcess: -+ children = [] -+ return children -+ -+ -+def _get_cmdline(proc): -+ # pylint: disable=protected-access -+ try: -+ return proc._cmdline -+ except AttributeError: -+ # Cache the cmdline since that will be inaccessible once the process is terminated -+ # and we use it in log calls -+ try: -+ cmdline = proc.cmdline() -+ except (psutil.NoSuchProcess, psutil.AccessDenied): -+ # OSX is more restrictive about the above information -+ cmdline = None -+ except OSError: # pragma: no cover -+ # On Windows we've seen something like: -+ # File " c: ... \lib\site-packages\pytestsalt\utils\__init__.py", line 182, in terminate_process -+ # terminate_process_list(process_list, kill=slow_stop is False, slow_stop=slow_stop) -+ # File " c: ... \lib\site-packages\pytestsalt\utils\__init__.py", line 130, in terminate_process_list -+ # _terminate_process_list(process_list, kill=kill, slow_stop=slow_stop) -+ # File " c: ... \lib\site-packages\pytestsalt\utils\__init__.py", line 78, in _terminate_process_list -+ # cmdline = process.cmdline() -+ # File " c: ... \lib\site-packages\psutil\__init__.py", line 786, in cmdline -+ # return self._proc.cmdline() -+ # File " c: ... \lib\site-packages\psutil\_pswindows.py", line 667, in wrapper -+ # return fun(self, *args, **kwargs) -+ # File " c: ... \lib\site-packages\psutil\_pswindows.py", line 745, in cmdline -+ # ret = cext.proc_cmdline(self.pid, use_peb=True) -+ # OSError: [WinError 299] Only part of a ReadProcessMemory or WriteProcessMemory request was completed: 'originated from ReadProcessMemory(ProcessParameters) -+ cmdline = None -+ except RuntimeError: # pragma: no cover -+ # Also on windows -+ # saltfactories\utils\processes\helpers.py:68: in _get_cmdline -+ # cmdline = proc.as_dict() -+ # c: ... \lib\site-packages\psutil\__init__.py:634: in as_dict -+ # ret = meth() -+ # c: ... \lib\site-packages\psutil\__init__.py:1186: in memory_full_info -+ # return self._proc.memory_full_info() -+ # c: ... \lib\site-packages\psutil\_pswindows.py:667: in wrapper -+ # return fun(self, *args, **kwargs) -+ # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ -+ # -+ # self = <psutil._pswindows.Process object at 0x0000029B7FDA5558> -+ # -+ # @wrap_exceptions -+ # def memory_full_info(self): -+ # basic_mem = self.memory_info() -+ # > uss = cext.proc_memory_uss(self.pid) -+ # E RuntimeError: NtQueryVirtualMemory failed -+ # -+ # c: ... \lib\site-packages\psutil\_pswindows.py:806: RuntimeError -+ cmdline = None -+ -+ if not cmdline: -+ try: -+ cmdline = proc.as_dict() -+ except psutil.NoSuchProcess: -+ cmdline = "<could not be retrived; dead process: {}>".format(proc) -+ except (psutil.AccessDenied, OSError): # pragma: no cover -+ cmdline = weakref.proxy(proc) -+ proc._cmdline = cmdline -+ return proc._cmdline -+ # pylint: enable=protected-access -+ -+ -+def _terminate_process_list(process_list, kill=False, slow_stop=False): -+ log.info( -+ "Terminating process list:\n%s", -+ pprint.pformat([_get_cmdline(proc) for proc in process_list]), -+ ) -+ for process in process_list[:]: # Iterate over copy of the list -+ if not psutil.pid_exists(process.pid): -+ process_list.remove(process) -+ continue -+ try: -+ if not kill and process.status() == psutil.STATUS_ZOMBIE: # pragma: no cover -+ # Zombie processes will exit once child processes also exit -+ continue -+ if kill: -+ log.info("Killing process(%s): %s", process.pid, _get_cmdline(process)) -+ process.kill() -+ else: -+ log.info("Terminating process(%s): %s", process.pid, _get_cmdline(process)) -+ try: -+ if slow_stop: -+ # Allow coverage data to be written down to disk -+ process.send_signal(signal.SIGTERM) -+ try: -+ process.wait(2) -+ except psutil.TimeoutExpired: # pragma: no cover -+ if psutil.pid_exists(process.pid): -+ continue -+ else: -+ process.terminate() -+ except OSError as exc: # pragma: no cover -+ if exc.errno not in (errno.ESRCH, errno.EACCES): -+ raise -+ if not psutil.pid_exists(process.pid): -+ process_list.remove(process) -+ except psutil.NoSuchProcess: -+ process_list.remove(process) -+ -+ -+def terminate_process_list(process_list, kill=False, slow_stop=False): -+ """ -+ Terminate a list of processes -+ -+ :param ~collections.abc.Iterable process_list: -+ An iterable of :py:class:`psutil.Process` instances to terminate -+ :keyword bool kill: -+ Kill the process instead of terminating it. -+ :keyword bool slow_stop: -+ First try to terminate each process in the list, and if termination was not successful, kill it. -+ """ -+ -+ def on_process_terminated(proc): -+ log.info( -+ "Process %s terminated with exit code: %s", -+ getattr(proc, "_cmdline", proc), -+ proc.returncode, -+ ) -+ -+ # Try to terminate processes with the provided kill and slow_stop parameters -+ log.info("Terminating process list. 1st step. kill: %s, slow stop: %s", kill, slow_stop) -+ -+ # Remove duplicates from the process list -+ seen_pids = [] -+ start_count = len(process_list) -+ for proc in process_list[:]: -+ if proc.pid in seen_pids: -+ process_list.remove(proc) -+ seen_pids.append(proc.pid) -+ end_count = len(process_list) -+ if end_count < start_count: -+ log.debug("Removed %d duplicates from the initial process list", start_count - end_count) -+ -+ _terminate_process_list(process_list, kill=kill, slow_stop=slow_stop) -+ psutil.wait_procs(process_list, timeout=5, callback=on_process_terminated) -+ -+ if process_list: -+ # If there's still processes to be terminated, retry and kill them if slow_stop is False -+ log.info( -+ "Terminating process list. 2nd step. kill: %s, slow stop: %s", -+ slow_stop is False, -+ slow_stop, -+ ) -+ _terminate_process_list(process_list, kill=slow_stop is False, slow_stop=slow_stop) -+ psutil.wait_procs(process_list, timeout=5, callback=on_process_terminated) -+ -+ if process_list: -+ # If there's still processes to be terminated, just kill them, no slow stopping now -+ log.info("Terminating process list. 3rd step. kill: True, slow stop: False") -+ _terminate_process_list(process_list, kill=True, slow_stop=False) -+ psutil.wait_procs(process_list, timeout=5, callback=on_process_terminated) -+ -+ if process_list: -+ # In there's still processes to be terminated, log a warning about it -+ log.warning("Some processes failed to properly terminate: %s", process_list) -+ -+ -+def terminate_process(pid=None, process=None, children=None, kill_children=None, slow_stop=False): -+ """ -+ Try to terminate/kill the started process -+ -+ :keyword int pid: -+ The PID of the process -+ :keyword ~psutil.Process process: -+ An instance of :py:class:`psutil.Process` -+ :keyword ~collections.abc.Iterable children: -+ An iterable of :py:class:`psutil.Process` instances, children to the process being terminated -+ :keyword bool kill_children: -+ Also try to terminate/kill child processes -+ :keyword bool slow_stop: -+ First try to terminate each process in the list, and if termination was not successful, kill it. -+ """ -+ children = children or [] -+ process_list = [] -+ -+ if kill_children is None: -+ # Always kill children if kill the parent process and kill_children was not set -+ kill_children = True if slow_stop is False else kill_children -+ -+ if pid and not process: -+ try: -+ process = psutil.Process(pid) -+ process_list.append(process) -+ except psutil.NoSuchProcess: -+ # Process is already gone -+ process = None -+ -+ if kill_children: -+ if process: -+ children.extend(collect_child_processes(pid)) -+ if children: -+ process_list.extend(children) -+ -+ if process_list: -+ if process: -+ log.info("Stopping process %s and respective children: %s", process, children) -+ else: -+ log.info("Terminating process list: %s", process_list) -+ terminate_process_list(process_list, kill=slow_stop is False, slow_stop=slow_stop) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/utils/saltext/__init__.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/utils/saltext/__init__.py -@@ -0,0 +1,4 @@ -+""" -+Salt Extensions -+=============== -+""" ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/utils/saltext/engines/pytest_engine.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/utils/saltext/engines/pytest_engine.py -@@ -0,0 +1,174 @@ -+# -*- coding: utf-8 -*- -+""" -+pytest_engine -+~~~~~~~~~~~~~ -+ -+Simple salt engine which will setup a socket to accept connections allowing us to know -+when a daemon is up and running -+""" -+import atexit -+import datetime -+import logging -+import threading -+ -+import zmq -+ -+try: -+ from collections.abc import MutableMapping -+except ImportError: -+ # Py2 compat -+ from collections import MutableMapping -+try: -+ import msgpack -+ -+ HAS_MSGPACK = True -+except ImportError: -+ HAS_MSGPACK = False -+ -+import salt.utils.event -+ -+try: -+ import salt.utils.immutabletypes as immutabletypes -+except ImportError: -+ immutabletypes = None -+try: -+ from salt.utils.data import CaseInsensitiveDict -+except ImportError: -+ CaseInsensitiveDict = None -+ -+ -+log = logging.getLogger(__name__) -+ -+__virtualname__ = "pytest" -+ -+ -+def __virtual__(): -+ role = __opts__["__role"] -+ pytest_key = "pytest-{}".format(role) -+ if pytest_key not in __opts__: -+ return False, "No '{}' key in opts dictionary".format(pytest_key) -+ -+ pytest_config = __opts__[pytest_key] -+ if "returner_address" not in pytest_config: -+ return False, "No 'returner_address' key in opts['{}'] dictionary".format(pytest_key) -+ if HAS_MSGPACK is False: -+ return False, "msgpack was not importable. Please install msgpack." -+ return True -+ -+ -+def start(): -+ opts = __opts__ # pylint: disable=undefined-variable -+ try: -+ pytest_engine = PyTestEventForwardEngine(opts=opts) -+ pytest_engine.start() -+ except Exception: # pragma: no cover pylint: disable=broad-except -+ log.error("Failed to start PyTestEventForwardEngine", exc_info=True) -+ raise -+ -+ -+def ext_type_encoder(obj): -+ """ -+ Convert any types that msgpack cannot handle on it's own -+ """ -+ if isinstance(obj, (datetime.datetime, datetime.date)): -+ # msgpack doesn't support datetime.datetime and datetime.date datatypes. -+ return obj.strftime("%Y%m%dT%H:%M:%S.%f") -+ # The same for immutable types -+ elif immutabletypes is not None and isinstance(obj, immutabletypes.ImmutableDict): -+ return dict(obj) -+ elif immutabletypes is not None and isinstance(obj, immutabletypes.ImmutableList): -+ return list(obj) -+ elif immutabletypes is not None and isinstance(obj, immutabletypes.ImmutableSet): -+ # msgpack can't handle set so translate it to tuple -+ return tuple(obj) -+ elif isinstance(obj, set): -+ # msgpack can't handle set so translate it to tuple -+ return tuple(obj) -+ elif CaseInsensitiveDict is not None and isinstance(obj, CaseInsensitiveDict): -+ return dict(obj) -+ elif isinstance(obj, MutableMapping): -+ return dict(obj) -+ # Nothing known exceptions found. Let msgpack raise its own. -+ return obj -+ -+ -+class PyTestEventForwardEngine: -+ -+ __slots__ = ("opts", "id", "role", "returner_address", "running_event") -+ -+ def __init__(self, opts): -+ self.opts = opts -+ self.id = self.opts["id"] -+ self.role = self.opts["__role"] -+ self.returner_address = self.opts["pytest-{}".format(self.role)]["returner_address"] -+ self.running_event = threading.Event() -+ -+ def __repr__(self): -+ return "<{} role={!r} id={!r}, returner_address={!r} running={!r}>".format( -+ self.__class__.__name__, -+ self.role, -+ self.id, -+ self.returner_address, -+ self.running_event.is_set(), -+ ) -+ -+ def start(self): -+ if self.running_event.is_set(): -+ return -+ log.info("%s is starting", self) -+ atexit.register(self.stop) -+ -+ self.running_event.set() -+ try: -+ context = zmq.Context() -+ push = context.socket(zmq.PUSH) -+ log.debug("%s connecting PUSH socket to %s", self, self.returner_address) -+ push.connect(self.returner_address) -+ opts = self.opts.copy() -+ opts["file_client"] = "local" -+ with salt.utils.event.get_event( -+ self.role, -+ sock_dir=opts["sock_dir"], -+ opts=opts, -+ listen=True, -+ ) as eventbus: -+ if self.role == "master": -+ event_tag = "salt/master/{}/start".format(self.id) -+ log.info("%s firing event on engine start. Tag: %s", self, event_tag) -+ load = {"id": self.id, "tag": event_tag, "data": {}} -+ eventbus.fire_event(load, event_tag) -+ log.info("%s started", self) -+ while self.running_event.is_set(): -+ for event in eventbus.iter_events(full=True, auto_reconnect=True): -+ if not event: -+ continue -+ tag = event["tag"] -+ data = event["data"] -+ log.debug("%s Received Event; TAG: %r DATA: %r", self, tag, data) -+ forward = (self.id, tag, data) -+ try: -+ dumped = msgpack.dumps( -+ forward, use_bin_type=True, default=ext_type_encoder -+ ) -+ push.send(dumped) -+ log.info("%s forwarded event: %r", self, forward) -+ except Exception: # pragma: no cover pylint: disable=broad-except -+ log.error( -+ "%s failed to forward event: %r", self, forward, exc_info=True -+ ) -+ finally: -+ if self.running_event.is_set(): -+ # Some exception happened, unset -+ self.running_event.clear() -+ if not push.closed: -+ push.close(1500) -+ if not context.closed: -+ context.term() -+ -+ def stop(self): -+ if self.running_event.is_set() is False: -+ return -+ -+ log.info("Stopping %s", self) -+ self.running_event.clear() -+ log.info("%s stopped", self) ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/utils/saltext/log_handlers/pytest_log_handler.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/utils/saltext/log_handlers/pytest_log_handler.py -@@ -0,0 +1,384 @@ -+""" -+pytest_log_handler -+~~~~~~~~~~~~~~~~~~ -+ -+Salt External Logging Handler -+""" -+import copy -+import logging -+import os -+import pprint -+import socket -+import sys -+import time -+import traceback -+ -+try: -+ from salt.utils.stringutils import to_unicode -+except ImportError: # pragma: no cover -+ # This likely due to running backwards compatibility tests against older minions -+ from salt.utils import to_unicode -+try: -+ from salt._logging.impl import LOG_LEVELS -+ from salt._logging.mixins import ExcInfoOnLogLevelFormatMixin -+except ImportError: # pragma: no cover -+ # This likely due to running backwards compatibility tests against older minions -+ from salt.log.setup import LOG_LEVELS -+ from salt.log.mixins import ExcInfoOnLogLevelFormatMixIn as ExcInfoOnLogLevelFormatMixin -+try: -+ from salt._logging.mixins import NewStyleClassMixin -+except ImportError: # pragma: no cover -+ try: -+ # This likely due to running backwards compatibility tests against older minions -+ from salt.log.mixins import NewStyleClassMixIn as NewStyleClassMixin -+ except ImportError: # pragma: no cover -+ # NewStyleClassMixin was removed from salt -+ -+ class NewStyleClassMixin(object): -+ """ -+ A copy of Salt's previous NewStyleClassMixin implementation -+ """ -+ -+ -+try: -+ import msgpack -+ -+ HAS_MSGPACK = True -+except ImportError: # pragma: no cover -+ HAS_MSGPACK = False -+try: -+ import zmq -+ -+ HAS_ZMQ = True -+except ImportError: # pragma: no cover -+ HAS_ZMQ = False -+ -+ -+__virtualname__ = "pytest_log_handler" -+ -+log = logging.getLogger(__name__) -+ -+ -+def __virtual__(): -+ role = __opts__["__role"] -+ pytest_key = "pytest-{}".format(role) -+ -+ pytest_config = __opts__[pytest_key] -+ if "log" not in pytest_config: -+ return False, "No 'log' key in opts {} dictionary".format(pytest_key) -+ -+ log_opts = pytest_config["log"] -+ if "port" not in log_opts: -+ return ( -+ False, -+ "No 'port' key in opts['pytest']['log'] or opts['pytest'][{}]['log']".format( -+ __opts__["role"] -+ ), -+ ) -+ if HAS_MSGPACK is False: -+ return False, "msgpack was not importable. Please install msgpack." -+ if HAS_ZMQ is False: -+ return False, "zmq was not importable. Please install pyzmq." -+ return True -+ -+ -+def setup_handlers(): -+ role = __opts__["__role"] -+ pytest_key = "pytest-{}".format(role) -+ pytest_config = __opts__[pytest_key] -+ log_opts = pytest_config["log"] -+ if log_opts.get("disabled"): -+ return -+ host_addr = log_opts.get("host") -+ if not host_addr: -+ import subprocess -+ -+ if log_opts["pytest_windows_guest"] is True: -+ proc = subprocess.Popen("ipconfig", stdout=subprocess.PIPE) -+ for line in proc.stdout.read().strip().encode(__salt_system_encoding__).splitlines(): -+ if "Default Gateway" in line: -+ parts = line.split() -+ host_addr = parts[-1] -+ break -+ else: -+ proc = subprocess.Popen( -+ "netstat -rn | grep -E '^0.0.0.0|default' | awk '{ print $2 }'", -+ shell=True, -+ stdout=subprocess.PIPE, -+ ) -+ host_addr = proc.stdout.read().strip().encode(__salt_system_encoding__) -+ host_port = log_opts["port"] -+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -+ try: -+ sock.connect((host_addr, host_port)) -+ except OSError as exc: -+ # Don't even bother if we can't connect -+ log.warning("Cannot connect back to log server at %s:%d: %s", host_addr, host_port, exc) -+ return -+ finally: -+ sock.close() -+ -+ pytest_log_prefix = log_opts.get("prefix") -+ try: -+ level = LOG_LEVELS[(log_opts.get("level") or "error").lower()] -+ except KeyError: -+ level = logging.ERROR -+ handler = ZMQHandler(host=host_addr, port=host_port, log_prefix=pytest_log_prefix, level=level) -+ handler.setLevel(level) -+ handler.start() -+ return handler -+ -+ -+class ZMQHandler(ExcInfoOnLogLevelFormatMixin, logging.Handler, NewStyleClassMixin): -+ -+ # We implement a lazy start approach which is deferred until sending the -+ # first message because, logging handlers, on platforms which support -+ # forking, are inherited by forked processes, and we want to minimize the ZMQ -+ # machinery inherited. -+ # For the cases where the ZMQ machinery is still inherited because a -+ # process was forked after ZMQ has been prepped up, we check the handler's -+ # pid attribute against the current process pid. If it's not a match, we -+ # reconnect the ZMQ machinery. -+ -+ def __init__( -+ self, host="127.0.0.1", port=3330, log_prefix=None, level=logging.NOTSET, socket_hwm=100000 -+ ): -+ super(ZMQHandler, self).__init__(level=level) -+ self.host = host -+ self.port = port -+ self._log_prefix = log_prefix -+ self.socket_hwm = socket_hwm -+ self.log_prefix = self._get_log_prefix(log_prefix) -+ self.context = self.pusher = None -+ self._exiting = False -+ self.dropped_messages_count = 0 -+ # We set the formatter so that we only include the actual log message and not any other -+ # fields found in the log record -+ self.__formatter = logging.Formatter("%(message)s") -+ self.pid = os.getpid() -+ -+ def _get_formatter(self): -+ return self.__formatter -+ -+ def _set_formatter(self, fmt): -+ if fmt is not None: -+ self.setFormatter(fmt) -+ -+ def _del_formatter(self): -+ raise RuntimeError("Cannot delete the 'formatter' attribute") -+ -+ # We set formatter as a property to make it immutable -+ formatter = property(_get_formatter, _set_formatter, _del_formatter) -+ -+ def setFormatter(self, _): -+ raise RuntimeError("Do not set a formatter on {}".format(self.__class__.__name__)) -+ -+ def __getstate__(self): -+ return { -+ "host": self.host, -+ "port": self.port, -+ "log_prefix": self._log_prefix, -+ "level": self.level, -+ "socket_hwm": self.socket_hwm, -+ } -+ -+ def __setstate__(self, state): -+ self.__init__(**state) -+ self.stop() -+ self._exiting = False -+ -+ def __repr__(self): -+ return "<{} host={} port={} level={}>".format( -+ self.__class__.__name__, self.host, self.port, logging.getLevelName(self.level) -+ ) -+ -+ def _get_log_prefix(self, log_prefix): -+ if log_prefix is None: -+ return -+ if sys.argv[0] == sys.executable: -+ cli_arg_idx = 1 -+ else: -+ cli_arg_idx = 0 -+ cli_name = os.path.basename(sys.argv[cli_arg_idx]) -+ return log_prefix.format(cli_name=cli_name) -+ -+ def start(self): -+ if self.pid != os.getpid(): -+ self.stop() -+ self._exiting = False -+ -+ if self._exiting is True: -+ return -+ -+ if self.pusher is not None: -+ # We're running ... -+ return -+ -+ self.dropped_messages_count = 0 -+ context = pusher = None -+ try: -+ context = zmq.Context() -+ self.context = context -+ except zmq.ZMQError as exc: -+ sys.stderr.write( -+ "Failed to create the ZMQ Context: {}\n{}\n".format(exc, traceback.format_exc()) -+ ) -+ sys.stderr.flush() -+ self.stop() -+ # Allow the handler to re-try starting -+ self._exiting = False -+ return -+ -+ try: -+ pusher = context.socket(zmq.PUSH) -+ pusher.set_hwm(self.socket_hwm) -+ pusher.connect("tcp://{}:{}".format(self.host, self.port)) -+ self.pusher = pusher -+ except zmq.ZMQError as exc: -+ if pusher is not None: -+ pusher.close(0) -+ sys.stderr.write( -+ "Failed to connect the ZMQ PUSH socket: {}\n{}\n".format( -+ exc, traceback.format_exc() -+ ) -+ ) -+ sys.stderr.flush() -+ self.stop() -+ # Allow the handler to re-try starting -+ self._exiting = False -+ return -+ -+ self.pid = os.getpid() -+ -+ def stop(self, flush=True): -+ if self._exiting: -+ return -+ -+ self._exiting = True -+ -+ if self.dropped_messages_count: -+ sys.stderr.write( -+ "Dropped {} messages from getting forwarded. High water mark reached...\n".format( -+ self.dropped_messages_count -+ ) -+ ) -+ sys.stderr.flush() -+ -+ try: -+ if self.pusher is not None and not self.pusher.closed: -+ if flush: -+ # Give it 1.5 seconds to flush any messages in it's queue -+ linger = 1500 -+ else: -+ linger = 0 -+ self.pusher.close(linger) -+ self.pusher = None -+ if self.context is not None and not self.context.closed: -+ self.context.term() -+ self.context = None -+ except (SystemExit, KeyboardInterrupt): # pragma: no cover pylint: disable=try-except-raise -+ # Don't write these exceptions to stderr -+ raise -+ except Exception as exc: # pragma: no cover pylint: disable=broad-except -+ sys.stderr.write( -+ "Failed to terminate ZMQHandler: {}\n{}\n".format(exc, traceback.format_exc()) -+ ) -+ sys.stderr.flush() -+ raise -+ finally: -+ if self.pusher is not None and not self.pusher.closed: -+ self.pusher.close(0) -+ self.pusher = None -+ if self.context is not None and not self.context.closed: -+ self.context.term() -+ self.context = None -+ self.pid = None -+ -+ def format(self, record): -+ msg = super(ZMQHandler, self).format(record) -+ if self.log_prefix: -+ msg = "[{}] {}".format(to_unicode(self.log_prefix), to_unicode(msg)) -+ return msg -+ -+ def prepare(self, record): -+ msg = self.format(record) -+ record = copy.copy(record) -+ record.msg = msg -+ # Reduce network bandwidth, we don't need these any more -+ record.args = None -+ record.exc_info = None -+ record.exc_text = None -+ record.message = None # redundant with msg -+ # On Python >= 3.5 we also have stack_info, but we've formatted already so, reset it -+ record.stack_info = None -+ try: -+ return msgpack.dumps(record.__dict__, use_bin_type=True) -+ except TypeError as exc: -+ # Failed to serialize something with msgpack -+ sys.stderr.write( -+ "Failed to serialize log record:{}.\n{}\nLog Record:\n{}\n".format( -+ exc, traceback.format_exc(), pprint.pformat(record.__dict__) -+ ) -+ ) -+ sys.stderr.flush() -+ self.handleError(record) -+ -+ def emit(self, record): -+ """ -+ Emit a record. -+ -+ Writes the LogRecord to the queue, preparing it for pickling first. -+ """ -+ # Python's logging machinery acquires a lock before calling this method -+ # that's why it's safe to call the start method without an explicit acquire -+ if self._exiting: -+ return -+ self.start() -+ if self.pusher is None: -+ sys.stderr.write( -+ "Not sending log message over the wire because " -+ "we were unable to connect to the log server.\n" -+ ) -+ sys.stderr.flush() -+ return -+ try: -+ msg = self.prepare(record) -+ if msg: -+ try: -+ self._send_message(msg) -+ except zmq.error.Again: -+ # Sleep a little and give up -+ time.sleep(0.001) -+ try: -+ self._send_message(msg) -+ except zmq.error.Again: -+ # We can't send it nor queue it for send. -+ # Drop it, otherwise, this call blocks until we can at least queue the message -+ self.dropped_messages_count += 1 -+ except (SystemExit, KeyboardInterrupt): # pragma: no cover pylint: disable=try-except-raise -+ # Catch and raise SystemExit and KeyboardInterrupt so that we can handle -+ # all other exception below -+ self.stop(flush=False) -+ raise -+ except Exception: # pragma: no cover pylint: disable=broad-except -+ self.handleError(record) -+ -+ def _send_message(self, msg): -+ self.pusher.send(msg, flags=zmq.NOBLOCK) -+ if self.dropped_messages_count: -+ logging.getLogger(__name__).debug( -+ "Dropped %s messages from getting forwarded. High water mark reached...", -+ self.dropped_messages_count, -+ ) -+ self.dropped_messages_count = 0 -+ -+ def close(self): -+ """ -+ Tidy up any resources used by the handler. -+ """ -+ # The logging machinery has asked to stop this handler -+ self.stop(flush=False) -+ # self._exiting should already be True, nonetheless, we set it here -+ # too to ensure the handler doesn't get a chance to restart itself -+ self._exiting = True -+ super().close() ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/utils/socket.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/utils/socket.py -@@ -0,0 +1,9 @@ -+""" -+saltfactories.utils.socket -+========================== -+ -+This module's sole purpose is to have the standard library :py:mod:`socket` module functions under a different -+namespace to be used in salt-factories so that projects using it, which need to mock :py:mod:`socket` functions, -+don't influence the salt-factories run time behavior. -+""" -+from socket import * # pylint: disable=wildcard-import,unused-wildcard-import ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/utils/tempfiles.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/utils/tempfiles.py -@@ -0,0 +1,414 @@ -+""" -+.. -+ PYTEST_DONT_REWRITE -+ -+ -+saltfactories.utils.tempfiles -+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -+ -+Temporary files utilities -+""" -+import logging -+import os -+import pathlib -+import shutil -+import tempfile -+import textwrap -+from contextlib import contextmanager -+ -+import attr -+ -+log = logging.getLogger(__name__) -+ -+ -+@contextmanager -+def temp_directory(name=None, basepath=None): -+ """ -+ This helper creates a temporary directory. -+ It should be used as a context manager which returns the temporary directory path, and, once out of context, -+ deletes it. -+ -+ :keyword str name: -+ The name of the directory to create -+ :keyword basepath name: -+ The base path of where to create the directory. Defaults to :py:func:`~tempfile.gettempdir` -+ :rtype: pathlib.Path -+ -+ Can be directly imported and used: -+ -+ .. code-block:: python -+ -+ from saltfactories.utils.tempfiles import temp_directory -+ -+ -+ def test_func(): -+ -+ with temp_directory() as temp_path: -+ assert temp_path.is_dir() -+ -+ assert not temp_path.is_dir() is False -+ -+ Or, it can be used as a pytest helper function: -+ -+ .. code-block:: python -+ -+ import pytest -+ -+ -+ def test_blah(): -+ with pytest.helpers.temp_directory() as temp_path: -+ assert temp_path.is_dir() -+ -+ assert not temp_path.is_dir() is False -+ """ -+ if basepath is None: -+ basepath = pathlib.Path(tempfile.gettempdir()) -+ try: -+ if name is not None: -+ directory_path = basepath / name -+ else: -+ directory_path = pathlib.Path(tempfile.mkdtemp(dir=str(basepath))) -+ -+ if not directory_path.is_dir(): -+ directory_path.mkdir(parents=True) -+ log.debug("Created temp directory: %s", directory_path) -+ -+ yield directory_path -+ finally: -+ created_directory = directory_path -+ while True: -+ if str(created_directory) == str(basepath): -+ break -+ if not any(created_directory.iterdir()): -+ shutil.rmtree(str(created_directory), ignore_errors=True) -+ log.debug("Deleted temp directory: %s", created_directory) -+ else: -+ log.debug("Not deleting %s because it's not empty", created_directory) -+ created_directory = created_directory.parent -+ -+ -+@contextmanager -+def temp_file(name=None, contents=None, directory=None, strip_first_newline=True): -+ """ -+ This helper creates a temporary file. It should be used as a context manager -+ which returns the temporary file path, and, once out of context, deletes it. -+ -+ :keyword str name: -+ The temporary file name -+ :keyword str contents: -+ The contents of the temporary file -+ :keyword str,pathlib.Path directory: -+ The directory where to create the temporary file. Defaults to the value of :py:func:`~tempfile.gettempdir` -+ :keyword bool strip_first_newline: -+ Either strip the initial first new line char or not. -+ -+ :rtype: pathlib.Path -+ -+ Can be directly imported and used: -+ -+ .. code-block:: python -+ -+ from saltfactories.utils.tempfiles import temp_file -+ -+ -+ def test_func(): -+ -+ with temp_file(name="blah.txt") as temp_path: -+ assert temp_path.is_file() -+ -+ assert not temp_path.is_file() is False -+ -+ Or, it can be used as a pytest helper function: -+ -+ .. code-block:: python -+ -+ import pytest -+ -+ -+ def test_blah(): -+ with pytest.helpers.temp_file("blah.txt") as temp_path: -+ assert temp_path.is_file() -+ -+ assert not temp_path.is_file() is False -+ """ -+ if directory is None: -+ directory = tempfile.gettempdir() -+ -+ if not isinstance(directory, pathlib.Path): -+ directory = pathlib.Path(str(directory)) -+ -+ if name is not None: -+ file_path = directory / name -+ else: -+ handle, file_path = tempfile.mkstemp(dir=str(directory)) -+ os.close(handle) -+ file_path = pathlib.Path(file_path) -+ -+ # Find out if we were given sub-directories on `name` -+ create_directories = file_path.parent.relative_to(directory) -+ -+ if create_directories: -+ with temp_directory(create_directories, basepath=directory): -+ with _write_or_touch(file_path, contents, strip_first_newline=strip_first_newline): -+ yield file_path -+ else: -+ with _write_or_touch(file_path, contents, strip_first_newline=strip_first_newline): -+ yield file_path -+ -+ -+@contextmanager -+def _write_or_touch(file_path, contents, strip_first_newline=True): -+ try: -+ if contents is not None: -+ if contents: -+ if contents.startswith("\n") and strip_first_newline: -+ contents = contents[1:] -+ file_contents = textwrap.dedent(contents) -+ else: -+ file_contents = contents -+ -+ file_path.write_text(file_contents) -+ log_contents = "{0} Contents of {1}\n{2}\n{3} Contents of {1}".format( -+ ">" * 6, file_path, file_contents, "<" * 6 -+ ) -+ log.debug("Created temp file: %s\n%s", file_path, log_contents) -+ else: -+ file_path.touch() -+ log.debug("Touched temp file: %s", file_path) -+ yield -+ finally: -+ if file_path.exists(): -+ file_path.unlink() -+ log.debug("Deleted temp file: %s", file_path) -+ -+ -+@attr.s(kw_only=True, slots=True) -+class SaltEnv: -+ """ -+ This helper class represent a Salt Environment, either for states or pillar. -+ It's base purpose it to handle temporary file creation/deletion during testing. -+ -+ :keyword str name: -+ The salt environment name, commonly, 'base' or 'prod' -+ :keyword list paths: -+ The salt environment list of paths. -+ -+ .. admonition:: Note -+ -+ The first entry in this list, is the path that will get used to create temporary files in, -+ ie, the return value of the :py:attr:`saltfactories.utils.tempfiles.SaltEnv.write_path` -+ attribute. -+ """ -+ -+ name = attr.ib() -+ paths = attr.ib(default=attr.Factory(list)) -+ -+ def __attrs_post_init__(self): -+ for idx, path in enumerate(self.paths[:]): -+ if not isinstance(path, pathlib.Path): -+ # We have to cast path to a string because on Py3.5, path might be an instance of pathlib2.Path -+ path = pathlib.Path(str(path)) -+ self.paths[idx] = path -+ path.mkdir(parents=True, exist_ok=True) -+ -+ @property -+ def write_path(self): -+ """ -+ The path where temporary files are created -+ """ -+ return self.paths[0] -+ -+ def temp_file(self, name, contents=None, strip_first_newline=True): -+ """ -+ Create a temporary file within this saltenv. -+ -+ Please check :py:func:`saltfactories.utils.tempfiles.temp_file` for documentation. -+ """ -+ return temp_file( -+ name=name, -+ contents=contents, -+ directory=self.write_path, -+ strip_first_newline=strip_first_newline, -+ ) -+ -+ def as_dict(self): -+ """ -+ Returns a dictionary of the right types to update the salt configuration -+ :return dict: -+ """ -+ return {self.name: [str(p) for p in self.paths]} -+ -+ -+@attr.s(kw_only=True) -+class SaltEnvs: -+ """ -+ This class serves as a container for multiple salt environments for states or pillar. -+ -+ :keyword dict envs: -+ The `envs` dictionary should be a mapping of a string as key, the `saltenv`, commonly 'base' or 'prod', -+ and the value an instance of :py:class:`~saltfactories.utils.tempfiles.SaltEnv` or a list of strings(paths). -+ In the case where a list of strings(paths) is passed, it is converted to an instance of -+ :py:class:`~saltfactories.utils.tempfiles.SaltEnv` -+ -+ To provide a better user experience, the salt environments can be accessed as attributes of this class. -+ -+ .. code-block:: python -+ -+ envs = SaltEnvs( -+ { -+ "base": [ -+ "/path/to/base/env", -+ ], -+ "prod": [ -+ "/path/to/prod/env", -+ ], -+ } -+ ) -+ with envs.base.temp_file("foo.txt", "foo contents") as base_foo_path: -+ ... -+ with envs.prod.temp_file("foo.txt", "foo contents") as prod_foo_path: -+ ... -+ -+ """ -+ -+ envs = attr.ib() -+ -+ def __attrs_post_init__(self): -+ for envname, envtree in self.envs.items(): -+ if not isinstance(envtree, SaltEnv): -+ if isinstance(envtree, str): -+ envtree = [envtree] -+ self.envs[envname] = SaltEnv(name=envname, paths=envtree) -+ setattr(self, envname, self.envs[envname]) -+ -+ def as_dict(self): -+ """ -+ Returns a dictionary of the right types to update the salt configuration -+ :return dict: -+ """ -+ config = {} -+ for env in self.envs.values(): -+ config.update(env.as_dict()) -+ return config -+ -+ -+@attr.s(kw_only=True) -+class SaltStateTree(SaltEnvs): -+ """ -+ Helper class which handles temporary file creation within the state tree. -+ -+ :keyword dict envs: -+ A mapping of a ``saltenv`` to a list of paths. -+ -+ .. code-block:: python -+ -+ envs = { -+ "base": [ -+ "/path/to/base/env", -+ "/another/path/to/base/env", -+ ], -+ "prod": [ -+ "/path/to/prod/env", -+ "/another/path/to/prod/env", -+ ], -+ } -+ -+ The state tree environments can be accessed by attribute: -+ -+ .. code-block:: python -+ -+ # See example of envs definition above -+ state_tree = SaltStateTree(envs=envs) -+ -+ # To access the base saltenv -+ base = state_tree.envs["base"] -+ -+ # Alternatively, in a simpler form -+ base = state_tree.base -+ -+ When setting up the Salt configuration to use an instance of -+ :py:class:`~saltfactories.utils.tempfiles.SaltStateTree`, the following pseudo code can be followed. -+ -+ .. code-block:: python -+ -+ # Using the state_tree defined above: -+ salt_config = { -+ # ... other salt config entries ... -+ "file_roots": state_tree.as_dict() -+ # ... other salt config entries ... -+ } -+ -+ .. admonition:: Attention -+ -+ The temporary files created by the :py:meth:`~saltfactories.utils.tempfiles.SaltStateTree.temp_file` -+ are written to the first path passed when instantiating the ``SaltStateTree``, ie, the return value -+ of the :py:attr:`saltfactories.utils.tempfiles.SaltStateTree.write_path` attribute. -+ -+ .. code-block:: python -+ -+ # Given the example mapping shown above ... -+ -+ with state_tree.base.temp_file("foo.sls") as path: -+ assert str(path) == "/path/to/base/env/foo.sls" -+ """ -+ -+ -+@attr.s(kw_only=True) -+class SaltPillarTree(SaltEnvs): -+ """ -+ Helper class which handles temporary file creation within the pillar tree. -+ -+ :keyword dict envs: -+ A mapping of a ``saltenv`` to a list of paths. -+ -+ .. code-block:: python -+ -+ envs = { -+ "base": [ -+ "/path/to/base/env", -+ "/another/path/to/base/env", -+ ], -+ "prod": [ -+ "/path/to/prod/env", -+ "/another/path/to/prod/env", -+ ], -+ } -+ -+ The pillar tree environments can be accessed by attribute: -+ -+ .. code-block:: python -+ -+ # See example of envs definition above -+ pillar_tree = SaltPillarTree(envs=envs) -+ -+ # To access the base saltenv -+ base = pillar_tree.envs["base"] -+ -+ # Alternatively, in a simpler form -+ base = pillar_tree.base -+ -+ When setting up the Salt configuration to use an instance of -+ :py:class:`~saltfactories.utils.tempfiles.SaltPillarTree`, the following pseudo code can be followed. -+ -+ .. code-block:: python -+ -+ # Using the pillar_tree defined above: -+ salt_config = { -+ # ... other salt config entries ... -+ "pillar_roots": pillar_tree.as_dict() -+ # ... other salt config entries ... -+ } -+ -+ .. admonition:: Attention -+ -+ The temporary files created by the :py:meth:`~saltfactories.utils.tempfiles.SaltPillarTree.temp_file` -+ are written to the first path passed when instantiating the ``SaltPillarTree``, ie, the return value -+ of the :py:attr:`saltfactories.utils.tempfiles.SaltPillarTree.write_path` attribute. -+ -+ .. code-block:: python -+ -+ # Given the example mapping shown above ... -+ -+ with state_tree.base.temp_file("foo.sls") as path: -+ assert str(path) == "/path/to/base/env/foo.sls" -+ """ ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/utils/time.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/utils/time.py -@@ -0,0 +1,9 @@ -+""" -+saltfactories.utils.time -+======================== -+ -+This module's sole purpose is to have the standard library :py:mod:`time` module functions under a different -+namespace to be used in salt-factories so that projects using it, which need to mock :py:mod:`time` functions, -+don't influence the salt-factories run time behavior. -+""" -+from time import * # pylint: disable=wildcard-import,unused-wildcard-import ---- pytest-salt-factories-0.912.2.orig/src/saltfactories/utils/virtualenv.py -+++ pytest-salt-factories-0.912.2/src/saltfactories/utils/virtualenv.py -@@ -0,0 +1,203 @@ -+import json -+import logging -+import os -+import pathlib -+import shutil -+import subprocess -+import sys -+import tempfile -+import textwrap -+ -+import attr -+from pytestskipmarkers.utils import platform -+ -+from saltfactories.exceptions import ProcessFailed -+from saltfactories.utils.processes import ProcessResult -+ -+log = logging.getLogger(__name__) -+ -+ -+def _cast_to_pathlib_path(value): -+ if isinstance(value, pathlib.Path): -+ return value -+ return pathlib.Path(str(value)) -+ -+ -+@attr.s(frozen=True, slots=True) -+class VirtualEnv: -+ """ -+ Helper class to create and use a virtual environment -+ -+ :keyword str,~pathlib.Path venv_dir: -+ The path to the directory where the virtual environment should be created -+ :keyword list venv_create_args: -+ Additional list of strings to pass when creating the virtualenv -+ :keyword dict env: -+ Additional environment entries -+ :keyword str,~pathlib.Path cwd: -+ The default ``cwd`` to use. Can be overridden when calling -+ :py:func:`~saltfactories.utils.virtualenv.VirtualEnv.run` and -+ :py:func:`~saltfactories.utils.virtualenv.VirtualEnv.install` -+ -+ .. code-block:: python -+ -+ with VirtualEnv("/tmp/venv") as venv: -+ venv.install("pep8") -+ -+ assert "pep8" in venv.get_installed_packages() -+ """ -+ -+ venv_dir = attr.ib(converter=_cast_to_pathlib_path) -+ venv_create_args = attr.ib(default=attr.Factory(list)) -+ env = attr.ib(default=None) -+ cwd = attr.ib(default=None) -+ environ = attr.ib(init=False, repr=False) -+ venv_python = attr.ib(init=False, repr=False) -+ venv_bin_dir = attr.ib(init=False, repr=False) -+ -+ @venv_dir.default -+ def _default_venv_dir(self): -+ return pathlib.Path(tempfile.mkdtemp(dir=tempfile.gettempdir())) -+ -+ @environ.default -+ def _default_environ(self): -+ environ = os.environ.copy() -+ if self.env: -+ environ.update(self.env) -+ return environ -+ -+ @venv_python.default -+ def _default_venv_python(self): -+ # Once we drop Py3.5 we can stop casting to string -+ if platform.is_windows(): -+ return str(self.venv_dir / "Scripts" / "python.exe") -+ return str(self.venv_dir / "bin" / "python") -+ -+ @venv_bin_dir.default -+ def _default_venv_bin_dir(self): -+ return pathlib.Path(self.venv_python).parent -+ -+ def __enter__(self): -+ try: -+ self._create_virtualenv() -+ except subprocess.CalledProcessError as exc: -+ raise AssertionError("Failed to create virtualenv") from exc -+ return self -+ -+ def __exit__(self, *args): -+ shutil.rmtree(str(self.venv_dir), ignore_errors=True) -+ -+ def install(self, *args, **kwargs): -+ return self.run(self.venv_python, "-m", "pip", "install", *args, **kwargs) -+ -+ def run(self, *args, **kwargs): -+ """ -+ Run a shell command -+ -+ :rtype: ~saltfactories.utils.processes.ProcessResult -+ """ -+ check = kwargs.pop("check", True) -+ kwargs.setdefault("cwd", str(self.cwd or self.venv_dir)) -+ kwargs.setdefault("stdout", subprocess.PIPE) -+ kwargs.setdefault("stderr", subprocess.PIPE) -+ kwargs.setdefault("universal_newlines", True) -+ kwargs.setdefault("env", self.environ) -+ proc = subprocess.run(args, check=False, **kwargs) -+ ret = ProcessResult( -+ exitcode=proc.returncode, -+ stdout=proc.stdout, -+ stderr=proc.stderr, -+ cmdline=proc.args, -+ ) -+ log.debug(ret) -+ if check is True: -+ try: -+ proc.check_returncode() -+ except subprocess.CalledProcessError as exc: # pragma: no cover -+ raise ProcessFailed( -+ "Command failed return code check", -+ cmdline=proc.args, -+ stdout=proc.stdout, -+ stderr=proc.stderr, -+ exitcode=proc.returncode, -+ ) from exc -+ return ret -+ -+ @staticmethod -+ def get_real_python(): -+ """ -+ The reason why the virtualenv creation is proxied by this function is mostly -+ because under windows, we can't seem to properly create a virtualenv off of -+ another virtualenv(we can on Linux) and also because, we really don't want to -+ test virtualenv creation off of another virtualenv, we want a virtualenv created -+ from the original python. -+ Also, on windows, we must also point to the virtualenv binary outside the existing -+ virtualenv because it will fail otherwise -+ """ -+ try: -+ if platform.is_windows(): -+ return os.path.join(sys.real_prefix, os.path.basename(sys.executable)) -+ else: -+ python_binary_names = [ -+ "python{}.{}".format(*sys.version_info), -+ "python{}".format(*sys.version_info), -+ "python", -+ ] -+ for binary_name in python_binary_names: -+ python = os.path.join(sys.real_prefix, "bin", binary_name) -+ if os.path.exists(python): -+ break -+ else: -+ raise AssertionError( -+ "Couldn't find a python binary name under '{}' matching: {}".format( -+ os.path.join(sys.real_prefix, "bin"), python_binary_names -+ ) -+ ) -+ return python -+ except AttributeError: -+ return sys.executable -+ -+ def run_code(self, code_string, **kwargs): -+ """ -+ Run python code using the virtualenv python environment -+ -+ :param str code_string: -+ -+ The code string to run against the virtualenv python interpreter -+ """ -+ if code_string.startswith("\n"): -+ code_string = code_string[1:] -+ code_string = textwrap.dedent(code_string).rstrip() -+ log.debug("Code to run passed to python:\n>>>>>>>>>>\n%s\n<<<<<<<<<<", code_string) -+ return self.run(str(self.venv_python), "-c", code_string, **kwargs) -+ -+ def get_installed_packages(self): -+ """ -+ Get a dictionary of the installed packages where the keys are the package -+ names and the values their versions -+ """ -+ data = {} -+ ret = self.run(str(self.venv_python), "-m", "pip", "list", "--format", "json") -+ for pkginfo in json.loads(ret.stdout): -+ data[pkginfo["name"]] = pkginfo["version"] -+ return data -+ -+ def _create_virtualenv(self): -+ args = [ -+ self.get_real_python(), -+ "-m", -+ "virtualenv", -+ ] -+ passed_python = False -+ for arg in self.venv_create_args: -+ if arg.startswith(("--python", "--python=")): -+ passed_python = True -+ args.append(arg) -+ if passed_python is False: -+ args.append("--python={}".format(self.get_real_python())) -+ args.append(str(self.venv_dir)) -+ # We pass CWD because run defaults to the venv_dir, which, at this stage -+ # is not yet created -+ self.run(*args, cwd=os.getcwd()) -+ self.install("-U", "pip", "setuptools!=50.*,!=51.*,!=52.*") -+ log.debug("Created virtualenv in %s", self.venv_dir) diff --git a/components/python/pytest-salt-factories/pkg5 b/components/python/pytest-salt-factories/pkg5 index 7be5f43..9e393b9 100644 --- a/components/python/pytest-salt-factories/pkg5 +++ b/components/python/pytest-salt-factories/pkg5 @@ -5,8 +5,10 @@ "library/python/psutil-39", "library/python/pytest-39", "library/python/pytest-helpers-namespace-39", + "library/python/pytest-shell-utilities-39", "library/python/pytest-skip-markers-39", - "library/python/pytest-tempdir-39", + "library/python/pytest-system-statistics-39", + "library/python/pyyaml-39", "library/python/pyzmq-39", "library/python/setuptools-39", "library/python/setuptools-declarative-requirements-39", @@ -16,8 +18,8 @@ "runtime/python-39" ], "fmris": [ - "library/python/pytest-salt-factories-39", - "library/python/pytest-salt-factories" + "library/python/pytest-salt-factories", + "library/python/pytest-salt-factories-39" ], "name": "pytest-salt-factories" -} \ No newline at end of file +} diff --git a/components/python/pytest-salt-factories/pytest-salt-factories-PYVER.p5m b/components/python/pytest-salt-factories/pytest-salt-factories-PYVER.p5m index b56e0cf..9d0c18b 100644 --- a/components/python/pytest-salt-factories/pytest-salt-factories-PYVER.p5m +++ b/components/python/pytest-salt-factories/pytest-salt-factories-PYVER.p5m @@ -59,18 +59,22 @@ file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/plugins/log_server.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/plugins/markers.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/plugins/sysinfo.py -file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/plugins/sysstats.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/__init__.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/cli_scripts.py +file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/coverage/sitecustomize.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/functional.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/loader.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/markers.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/ports.py +file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/ports.pyi file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/processes.py +file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/processes.pyi file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/saltext/__init__.py -file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/socket.py +file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/saltext/engines/__init__.py +file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/saltext/engines/pytest_engine.py +file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/saltext/log_handlers/__init__.py +file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/saltext/log_handlers/pytest_log_handler.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/tempfiles.py -file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/time.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/utils/virtualenv.py file path=usr/lib/python$(PYVER)/vendor-packages/saltfactories/version.py @@ -84,7 +88,9 @@ depend type=require fmri=pkg:/library/python/psutil-$(PYV) depend type=require fmri=pkg:/library/python/pytest-$(PYV) depend type=require fmri=pkg:/library/python/pytest-helpers-namespace-$(PYV) +depend type=require fmri=pkg:/library/python/pytest-shell-utilities-$(PYV) depend type=require fmri=pkg:/library/python/pytest-skip-markers-$(PYV) -depend type=require fmri=pkg:/library/python/pytest-tempdir-$(PYV) +depend type=require fmri=pkg:/library/python/pytest-system-statistics-$(PYV) +depend type=require fmri=pkg:/library/python/pyyaml-$(PYV) depend type=require fmri=pkg:/library/python/pyzmq-$(PYV) depend type=require fmri=pkg:/library/python/virtualenv-$(PYV) diff --git a/components/python/pytest-salt-factories/python-integrate-project.conf b/components/python/pytest-salt-factories/python-integrate-project.conf index 5c059f0..65205b8 100644 --- a/components/python/pytest-salt-factories/python-integrate-project.conf +++ b/components/python/pytest-salt-factories/python-integrate-project.conf @@ -13,4 +13,6 @@ # Copyright 2023 Marcel Telka # -%patch% 01-sdist-incomplete.patch +%include-2% +# https://github.com/saltstack/pytest-system-statistics/issues/4 +TEST_STYLE = none diff --git a/components/python/pytest-salt-factories/test/results-all.master b/components/python/pytest-salt-factories/test/results-all.master deleted file mode 100644 index ebab2da..0000000 --- a/components/python/pytest-salt-factories/test/results-all.master +++ /dev/null @@ -1,70 +0,0 @@ -$(PYTHON_DIR)/vendor-packages/setuptools/__init__.py:84: _DeprecatedInstaller: setuptools.installer and fetch_build_eggs are deprecated. -!! - - ******************************************************************************** - Requirements should be satisfied by a PEP 517 installer. - If you are using pip, you can try `pip install --use-pep517`. - ******************************************************************************** - -!! - dist.fetch_build_eggs(dist.setup_requires) -Requirements parsed from 'requirements/base.txt': 'pytest>=6.0.0', 'attrs>=19.2.0', 'pytest-tempdir>=2019.9.16', 'pytest-helpers-namespace>=2021.4.29', 'pytest-skip-markers>=1.1.2', 'psutil', 'pyzmq', 'msgpack', 'virtualenv' -Requirements parsed from 'requirements/docs.txt': 'furo', 'sphinx', 'sphinx-copybutton', 'sphinx-prompt', 'sphinxcontrib-spelling', 'sphinxcontrib-towncrier >= 0.2.1a0' -Requirements parsed from 'requirements/lint.txt': 'pylint==2.7.4', 'saltpylint==2020.9.28', 'pyenchant', 'black; python_version >= '3.7'', 'reorder-python-imports; python_version >= '3.7'' -Requirements parsed from 'requirements/tests.txt': 'docker', 'pytest-subtests', 'pyfakefs==4.4.0; python_version == '3.5'', 'pyfakefs; python_version > '3.5'' -running test -WARNING: Testing via this command is deprecated and will be removed in a future version. Users looking for a generic test entry point independent of test runner are encouraged to use tox. -$(PYTHON_DIR)/vendor-packages/setuptools/command/test.py:193: _DeprecatedInstaller: setuptools.installer and fetch_build_eggs are deprecated. -!! - - ******************************************************************************** - Requirements should be satisfied by a PEP 517 installer. - If you are using pip, you can try `pip install --use-pep517`. - ******************************************************************************** - -!! - ir_d = dist.fetch_build_eggs(dist.install_requires) -$(PYTHON_DIR)/vendor-packages/setuptools/command/test.py:194: _DeprecatedInstaller: setuptools.installer and fetch_build_eggs are deprecated. -!! - - ******************************************************************************** - Requirements should be satisfied by a PEP 517 installer. - If you are using pip, you can try `pip install --use-pep517`. - ******************************************************************************** - -!! - tr_d = dist.fetch_build_eggs(dist.tests_require or []) -$(PYTHON_DIR)/vendor-packages/setuptools/command/test.py:195: _DeprecatedInstaller: setuptools.installer and fetch_build_eggs are deprecated. -!! - - ******************************************************************************** - Requirements should be satisfied by a PEP 517 installer. - If you are using pip, you can try `pip install --use-pep517`. - ******************************************************************************** - -!! - er_d = dist.fetch_build_eggs( -running egg_info -writing src/pytest_salt_factories.egg-info/PKG-INFO -writing dependency_links to src/pytest_salt_factories.egg-info/dependency_links.txt -writing entry points to src/pytest_salt_factories.egg-info/entry_points.txt -writing requirements to src/pytest_salt_factories.egg-info/requires.txt -writing top-level names to src/pytest_salt_factories.egg-info/top_level.txt -reading manifest file 'src/pytest_salt_factories.egg-info/SOURCES.txt' -reading manifest template 'MANIFEST.in' -warning: no files found matching '*.py' under directory 'saltfactories' -warning: no previously-included files found matching '.coveragerc' -warning: no previously-included files found matching '.codecov.yml' -warning: no previously-included files found matching '.pre-commit-config.yaml' -warning: no previously-included files found matching '.pylint*' -warning: no previously-included files found matching 'noxfile.py' -warning: no previously-included files matching '*.*' found under directory '.pre-commit-hooks' -warning: no previously-included files matching '*.*' found under directory '.github' -adding license file 'LICENSE' -writing manifest file 'src/pytest_salt_factories.egg-info/SOURCES.txt' -running build_ext - ----------------------------------------------------------------------- -Ran 0 tests - -OK -- Gitblit v1.9.3