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