diff --git a/_lastrevision b/_lastrevision
index e724caf..3ccde1b 100644
--- a/_lastrevision
+++ b/_lastrevision
@@ -1 +1 @@
-d0c2f35ff4a0b21786b20c884cbb191ad2e63904
\ No newline at end of file
+32a086961fa3b3b60287a78169b2ad66a000073a
\ No newline at end of file
diff --git a/_multibuild b/_multibuild
new file mode 100644
index 0000000..a0cd1a3
--- /dev/null
+++ b/_multibuild
@@ -0,0 +1,3 @@
+
+ testsuite
+
diff --git a/discover-both-.yml-and-.yaml-playbooks-bsc-1211888.patch b/discover-both-.yml-and-.yaml-playbooks-bsc-1211888.patch
new file mode 100644
index 0000000..5aefe29
--- /dev/null
+++ b/discover-both-.yml-and-.yaml-playbooks-bsc-1211888.patch
@@ -0,0 +1,188 @@
+From 05fbd376090c5d7f997c510db0abb62be54d6d40 Mon Sep 17 00:00:00 2001
+From: Johannes Hahn
+Date: Tue, 20 Feb 2024 15:38:08 +0100
+Subject: [PATCH] Discover both *.yml and *.yaml playbooks (bsc#1211888)
+
+Allow for 'playbook_extension' to be either a string or a tuple and
+change the default behavior to discover both.
+---
+ changelog/66048.changed.md | 1 +
+ salt/modules/ansiblegate.py | 46 +++++++++----------
+ .../pytests/unit/modules/test_ansiblegate.py | 3 ++
+ .../example_playbooks/playbook1.yaml | 5 ++
+ 4 files changed, 30 insertions(+), 25 deletions(-)
+ create mode 100644 changelog/66048.changed.md
+ create mode 100644 tests/unit/files/playbooks/example_playbooks/playbook1.yaml
+
+diff --git a/changelog/66048.changed.md b/changelog/66048.changed.md
+new file mode 100644
+index 0000000000..b042e0d313
+--- /dev/null
++++ b/changelog/66048.changed.md
+@@ -0,0 +1 @@
++Ansiblegate discover_playbooks was changed to find playbooks as either *.yml or *.yaml files
+diff --git a/salt/modules/ansiblegate.py b/salt/modules/ansiblegate.py
+index 2f60a7444f..920c374e5a 100644
+--- a/salt/modules/ansiblegate.py
++++ b/salt/modules/ansiblegate.py
+@@ -111,7 +111,7 @@ def __virtual__():
+ if proc.returncode != 0:
+ return (
+ False,
+- "Failed to get the listing of ansible modules:\n{}".format(proc.stderr),
++ f"Failed to get the listing of ansible modules:\n{proc.stderr}",
+ )
+
+ module_funcs = dir(sys.modules[__name__])
+@@ -240,7 +240,7 @@ def call(module, *args, **kwargs):
+ _kwargs = {k: v for (k, v) in kwargs.items() if not k.startswith("__pub")}
+
+ for key, value in _kwargs.items():
+- module_args.append("{}={}".format(key, salt.utils.json.dumps(value)))
++ module_args.append(f"{key}={salt.utils.json.dumps(value)}")
+
+ with NamedTemporaryFile(mode="w") as inventory:
+
+@@ -367,15 +367,15 @@ def playbooks(
+ if diff:
+ command.append("--diff")
+ if isinstance(extra_vars, dict):
+- command.append("--extra-vars='{}'".format(json.dumps(extra_vars)))
++ command.append(f"--extra-vars='{json.dumps(extra_vars)}'")
+ elif isinstance(extra_vars, str) and extra_vars.startswith("@"):
+- command.append("--extra-vars={}".format(extra_vars))
++ command.append(f"--extra-vars={extra_vars}")
+ if flush_cache:
+ command.append("--flush-cache")
+ if inventory:
+- command.append("--inventory={}".format(inventory))
++ command.append(f"--inventory={inventory}")
+ if limit:
+- command.append("--limit={}".format(limit))
++ command.append(f"--limit={limit}")
+ if list_hosts:
+ command.append("--list-hosts")
+ if list_tags:
+@@ -383,25 +383,25 @@ def playbooks(
+ if list_tasks:
+ command.append("--list-tasks")
+ if module_path:
+- command.append("--module-path={}".format(module_path))
++ command.append(f"--module-path={module_path}")
+ if skip_tags:
+- command.append("--skip-tags={}".format(skip_tags))
++ command.append(f"--skip-tags={skip_tags}")
+ if start_at_task:
+- command.append("--start-at-task={}".format(start_at_task))
++ command.append(f"--start-at-task={start_at_task}")
+ if syntax_check:
+ command.append("--syntax-check")
+ if tags:
+- command.append("--tags={}".format(tags))
++ command.append(f"--tags={tags}")
+ if playbook_kwargs:
+ for key, value in playbook_kwargs.items():
+ key = key.replace("_", "-")
+ if value is True:
+- command.append("--{}".format(key))
++ command.append(f"--{key}")
+ elif isinstance(value, str):
+- command.append("--{}={}".format(key, value))
++ command.append(f"--{key}={value}")
+ elif isinstance(value, dict):
+- command.append("--{}={}".format(key, json.dumps(value)))
+- command.append("--forks={}".format(forks))
++ command.append(f"--{key}={json.dumps(value)}")
++ command.append(f"--forks={forks}")
+ cmd_kwargs = {
+ "env": {
+ "ANSIBLE_STDOUT_CALLBACK": "json",
+@@ -502,7 +502,7 @@ def discover_playbooks(
+ List of paths to discover playbooks from.
+
+ :param playbook_extension:
+- File extension of playbooks file to search for. Default: "yml"
++ File extension(s) of playbook files to search for, can be a string or tuple of strings. Default: (".yml", ".yaml")
+
+ :param hosts_filename:
+ Filename of custom playbook inventory to search for. Default: "hosts"
+@@ -533,19 +533,17 @@ def discover_playbooks(
+ )
+
+ if not playbook_extension:
+- playbook_extension = "yml"
++ playbook_extension = (".yml", ".yaml")
+ if not hosts_filename:
+ hosts_filename = "hosts"
+
+ if path:
+ if not os.path.isabs(path):
+ raise CommandExecutionError(
+- "The given path is not an absolute path: {}".format(path)
++ f"The given path is not an absolute path: {path}"
+ )
+ if not os.path.isdir(path):
+- raise CommandExecutionError(
+- "The given path is not a directory: {}".format(path)
+- )
++ raise CommandExecutionError(f"The given path is not a directory: {path}")
+ return {
+ path: _explore_path(path, playbook_extension, hosts_filename, syntax_check)
+ }
+@@ -573,7 +571,7 @@ def _explore_path(path, playbook_extension, hosts_filename, syntax_check):
+ # Check files in the given path
+ for _f in os.listdir(path):
+ _path = os.path.join(path, _f)
+- if os.path.isfile(_path) and _path.endswith("." + playbook_extension):
++ if os.path.isfile(_path) and _path.endswith(playbook_extension):
+ ret[_f] = {"fullpath": _path}
+ # Check for custom inventory file
+ if os.path.isfile(os.path.join(path, hosts_filename)):
+@@ -584,9 +582,7 @@ def _explore_path(path, playbook_extension, hosts_filename, syntax_check):
+ # Check files in the 1st level of subdirectories
+ for _f2 in os.listdir(_path):
+ _path2 = os.path.join(_path, _f2)
+- if os.path.isfile(_path2) and _path2.endswith(
+- "." + playbook_extension
+- ):
++ if os.path.isfile(_path2) and _path2.endswith(playbook_extension):
+ ret[os.path.join(_f, _f2)] = {"fullpath": _path2}
+ # Check for custom inventory file
+ if os.path.isfile(os.path.join(_path, hosts_filename)):
+@@ -599,7 +595,7 @@ def _explore_path(path, playbook_extension, hosts_filename, syntax_check):
+ )
+ except Exception as exc:
+ raise CommandExecutionError(
+- "There was an exception while discovering playbooks: {}".format(exc)
++ f"There was an exception while discovering playbooks: {exc}"
+ )
+
+ # Run syntax check validation
+diff --git a/tests/pytests/unit/modules/test_ansiblegate.py b/tests/pytests/unit/modules/test_ansiblegate.py
+index 6201809c22..272da721bf 100644
+--- a/tests/pytests/unit/modules/test_ansiblegate.py
++++ b/tests/pytests/unit/modules/test_ansiblegate.py
+@@ -198,6 +198,9 @@ def test_ansible_discover_playbooks_single_path():
+ assert ret[playbooks_dir]["playbook1.yml"] == {
+ "fullpath": os.path.join(playbooks_dir, "playbook1.yml")
+ }
++ assert ret[playbooks_dir]["playbook1.yaml"] == {
++ "fullpath": os.path.join(playbooks_dir, "playbook1.yaml")
++ }
+ assert ret[playbooks_dir]["example-playbook2/site.yml"] == {
+ "fullpath": os.path.join(playbooks_dir, "example-playbook2/site.yml"),
+ "custom_inventory": os.path.join(playbooks_dir, "example-playbook2/hosts"),
+diff --git a/tests/unit/files/playbooks/example_playbooks/playbook1.yaml b/tests/unit/files/playbooks/example_playbooks/playbook1.yaml
+new file mode 100644
+index 0000000000..e258a101e1
+--- /dev/null
++++ b/tests/unit/files/playbooks/example_playbooks/playbook1.yaml
+@@ -0,0 +1,5 @@
++---
++- hosts: all
++ gather_facts: false
++ tasks:
++ - ping:
+--
+2.43.1
+
diff --git a/fix-problematic-tests-and-allow-smooth-tests-executi.patch b/fix-problematic-tests-and-allow-smooth-tests-executi.patch
new file mode 100644
index 0000000..ecc3de8
--- /dev/null
+++ b/fix-problematic-tests-and-allow-smooth-tests-executi.patch
@@ -0,0 +1,2695 @@
+From 1b1bbc3e46ab2eed98f07a23368877fc068dbc06 Mon Sep 17 00:00:00 2001
+From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?=
+
+Date: Mon, 26 Feb 2024 11:25:22 +0000
+Subject: [PATCH] Fix problematic tests and allow smooth tests
+ executions on containers
+
+* Align boto imports in tests with Salt modules
+
+Some modules `import boto` to set log levels. The related tests don't
+import `boto`. This can cause a problem when `boto` is not available.
+
+Tests are skipped when HAS_BOTO in the test_boto_*.py is False. Not
+trying to `import boto` can leave HAS_BOTO=True in the test file even
+though HAS_BOTO is False on the application side. In this case, tests
+are not skipped and fail.
+
+* Fix mock order in test_dig (test_network.py)
+
+`salt.utils.path.which` should not be mocked before `network.__utils__`. The
+Salt loader calls `salt.utils.network.linux_interfaces`, which needs the real
+`salt.utils.path.which`.
+
+* Fix mock calls
+
+Signed-off-by: Pedro Algarvio
+(cherry picked from commit 3506e7fd0e84320b2873370f1fe527025c244dca)
+
+* Skip venafiapi test if vcert module not available
+
+The same HAS_VCERT check is done in the runner module.
+
+* Moving tests/integration/modules/test_cmdmod.py to pytest, Gareth J Greenaway original author
+
+(cherry picked from commit 2c1040b4c2885efaa86576fd88eb36bb550b5996)
+
+* The repo.saltproject.io `index.html` file changed it's contents. Fix tests.
+
+Signed-off-by: Pedro Algarvio
+(cherry picked from commit 672f6586d7c3cdb0e8c5ee42524895035aafcc23)
+
+* Skip hwclock test when executed inside a container
+
+* Skip git pillar tests when executed inside a container
+
+These tests require a Git repository container, which is hard to correctly set
+up when executing the tests in the container in GH Actions.
+
+Using --network host can help, but there was still an error (the git repos were
+empty) when I tried to set this up.
+
+* Skip test requiring systemd inside container
+
+* Exclude tests for hgfs if missing hglib
+
+* Skip and fix tests when running on containers
+
+* Fix some failing test causing problem in SUSE environments
+
+* Skip more tests when running on containers
+
+* Use skipif instead of skip_if as it seems not behaving equally
+
+* Skip more tests that cannot run in a container
+
+* Remove SSH test which doesn't make sense after lock mechanism
+
+* Fix failing boto tests
+
+* Skip unmaintained tests upstream around zcbuildout
+
+* Skip some tests that does not run well in GH Actions
+
+---------
+
+Co-authored-by: Pedro Algarvio
+Co-authored-by: Gareth J. Greenaway
+Co-authored-by: Alexander Graul
+---
+ .../integration/externalapi/test_venafiapi.py | 10 +-
+ tests/integration/modules/test_cmdmod.py | 634 ------------------
+ tests/integration/modules/test_cp.py | 24 +-
+ tests/integration/modules/test_timezone.py | 3 +
+ tests/integration/pillar/test_git_pillar.py | 3 +
+ tests/integration/ssh/test_state.py | 47 --
+ tests/pytests/functional/cache/test_consul.py | 4 +
+ tests/pytests/functional/cache/test_mysql.py | 4 +
+ .../functional/fileserver/hgfs/test_hgfs.py | 2 +
+ .../pytests/functional/modules/test_cmdmod.py | 561 ++++++++++++++++
+ .../functional/modules/test_dockermod.py | 4 +
+ .../pytests/functional/modules/test_swarm.py | 5 +
+ .../pytests/functional/modules/test_system.py | 3 +
+ .../pillar/hg_pillar/test_hg_pillar.py | 1 +
+ .../states/rabbitmq/test_cluster.py | 4 +
+ .../functional/states/rabbitmq/test_plugin.py | 4 +
+ .../functional/states/rabbitmq/test_policy.py | 4 +
+ .../states/rabbitmq/test_upstream.py | 4 +
+ .../functional/states/rabbitmq/test_user.py | 4 +
+ .../functional/states/rabbitmq/test_vhost.py | 4 +
+ .../functional/states/test_docker_network.py | 5 +
+ tests/pytests/functional/states/test_pkg.py | 6 +-
+ .../integration/cli/test_syndic_eauth.py | 3 +
+ .../integration/daemons/test_memory_leak.py | 4 +
+ .../integration/modules/test_cmdmod.py | 93 +++
+ .../pytests/integration/modules/test_virt.py | 4 +
+ tests/pytests/integration/ssh/test_log.py | 3 +
+ tests/pytests/integration/ssh/test_master.py | 5 +
+ .../integration/ssh/test_py_versions.py | 3 +
+ .../pytests/integration/ssh/test_ssh_setup.py | 2 +
+ .../scenarios/compat/test_with_versions.py | 4 +
+ .../multimaster/test_failover_master.py | 3 +
+ tests/pytests/scenarios/setup/test_install.py | 6 +
+ tests/pytests/unit/modules/test_aptpkg.py | 12 +-
+ .../pytests/unit/modules/test_linux_sysctl.py | 8 +-
+ tests/pytests/unit/modules/test_win_ip.py | 4 +-
+ tests/pytests/unit/test_master.py | 2 +-
+ tests/pytests/unit/test_minion.py | 4 +-
+ tests/pytests/unit/utils/event/test_event.py | 24 +-
+ tests/unit/modules/test_boto_apigateway.py | 1 +
+ .../unit/modules/test_boto_cognitoidentity.py | 1 +
+ .../modules/test_boto_elasticsearch_domain.py | 1 +
+ tests/unit/modules/test_boto_lambda.py | 1 +
+ tests/unit/modules/test_network.py | 6 +-
+ tests/unit/modules/test_nilrt_ip.py | 4 +-
+ tests/unit/modules/test_zcbuildout.py | 2 +
+ .../unit/netapi/rest_tornado/test_saltnado.py | 22 +-
+ tests/unit/states/test_boto_apigateway.py | 1 +
+ .../unit/states/test_boto_cognitoidentity.py | 1 +
+ tests/unit/states/test_zcbuildout.py | 1 +
+ 50 files changed, 824 insertions(+), 741 deletions(-)
+ delete mode 100644 tests/integration/modules/test_cmdmod.py
+ create mode 100644 tests/pytests/functional/modules/test_cmdmod.py
+
+diff --git a/tests/integration/externalapi/test_venafiapi.py b/tests/integration/externalapi/test_venafiapi.py
+index ad08605430f..3ae1e3392d8 100644
+--- a/tests/integration/externalapi/test_venafiapi.py
++++ b/tests/integration/externalapi/test_venafiapi.py
+@@ -13,6 +13,14 @@ from cryptography.hazmat.backends import default_backend
+ from cryptography.hazmat.primitives import serialization
+ from cryptography.x509.oid import NameOID
+
++try:
++ import vcert
++ from vcert.common import CertificateRequest
++
++ HAS_VCERT = True
++except ImportError:
++ HAS_VCERT = False
++
+ from tests.support.case import ShellCase
+
+
+@@ -36,6 +44,7 @@ def with_random_name(func):
+ return wrapper
+
+
++@pytest.mark.skipif(HAS_VCERT is False, reason="The vcert module must be installed.")
+ class VenafiTest(ShellCase):
+ """
+ Test the venafi runner
+@@ -86,7 +95,6 @@ class VenafiTest(ShellCase):
+ @with_random_name
+ @pytest.mark.slow_test
+ def test_sign(self, name):
+-
+ csr_pem = """-----BEGIN CERTIFICATE REQUEST-----
+ MIIFbDCCA1QCAQAwgbQxCzAJBgNVBAYTAlVTMQ0wCwYDVQQIDARVdGFoMRIwEAYD
+ VQQHDAlTYWx0IExha2UxFDASBgNVBAoMC1ZlbmFmaSBJbmMuMRQwEgYDVQQLDAtJ
+diff --git a/tests/integration/modules/test_cmdmod.py b/tests/integration/modules/test_cmdmod.py
+deleted file mode 100644
+index 800111174f0..00000000000
+--- a/tests/integration/modules/test_cmdmod.py
++++ /dev/null
+@@ -1,634 +0,0 @@
+-import os
+-import random
+-import sys
+-import tempfile
+-from contextlib import contextmanager
+-
+-import pytest
+-
+-import salt.utils.path
+-import salt.utils.platform
+-import salt.utils.user
+-from tests.support.case import ModuleCase
+-from tests.support.helpers import SKIP_INITIAL_PHOTONOS_FAILURES, dedent
+-from tests.support.runtests import RUNTIME_VARS
+-
+-AVAILABLE_PYTHON_EXECUTABLE = salt.utils.path.which_bin(
+- ["python", "python2", "python2.6", "python2.7"]
+-)
+-
+-
+-@pytest.mark.windows_whitelisted
+-class CMDModuleTest(ModuleCase):
+- """
+- Validate the cmd module
+- """
+-
+- def setUp(self):
+- self.runas_usr = "nobody"
+- if salt.utils.platform.is_darwin():
+- self.runas_usr = "macsalttest"
+-
+- @contextmanager
+- def _ensure_user_exists(self, name):
+- if name in self.run_function("user.info", [name]).values():
+- # User already exists; don't touch
+- yield
+- else:
+- # Need to create user for test
+- self.run_function("user.add", [name])
+- try:
+- yield
+- finally:
+- self.run_function("user.delete", [name], remove=True)
+-
+- @pytest.mark.slow_test
+- @pytest.mark.skip_on_windows
+- def test_run(self):
+- """
+- cmd.run
+- """
+- shell = os.environ.get("SHELL")
+- if shell is None:
+- # Failed to get the SHELL var, don't run
+- self.skipTest("Unable to get the SHELL environment variable")
+-
+- self.assertTrue(self.run_function("cmd.run", ["echo $SHELL"]))
+- self.assertEqual(
+- self.run_function(
+- "cmd.run", ["echo $SHELL", "shell={}".format(shell)], python_shell=True
+- ).rstrip(),
+- shell,
+- )
+- self.assertEqual(
+- self.run_function("cmd.run", ["ls / | grep etc"], python_shell=True), "etc"
+- )
+- self.assertEqual(
+- self.run_function(
+- "cmd.run",
+- ['echo {{grains.id}} | awk "{print $1}"'],
+- template="jinja",
+- python_shell=True,
+- ),
+- "minion",
+- )
+- self.assertEqual(
+- self.run_function(
+- "cmd.run", ["grep f"], stdin="one\ntwo\nthree\nfour\nfive\n"
+- ),
+- "four\nfive",
+- )
+- self.assertEqual(
+- self.run_function(
+- "cmd.run", ['echo "a=b" | sed -e s/=/:/g'], python_shell=True
+- ),
+- "a:b",
+- )
+-
+- @pytest.mark.slow_test
+- def test_stdout(self):
+- """
+- cmd.run_stdout
+- """
+- self.assertEqual(
+- self.run_function("cmd.run_stdout", ['echo "cheese"']).rstrip(),
+- "cheese" if not salt.utils.platform.is_windows() else '"cheese"',
+- )
+-
+- @pytest.mark.slow_test
+- def test_stderr(self):
+- """
+- cmd.run_stderr
+- """
+- if sys.platform.startswith(("freebsd", "openbsd")):
+- shell = "/bin/sh"
+- else:
+- shell = "/bin/bash"
+-
+- self.assertEqual(
+- self.run_function(
+- "cmd.run_stderr",
+- ['echo "cheese" 1>&2', "shell={}".format(shell)],
+- python_shell=True,
+- ).rstrip(),
+- "cheese" if not salt.utils.platform.is_windows() else '"cheese"',
+- )
+-
+- @pytest.mark.slow_test
+- def test_run_all(self):
+- """
+- cmd.run_all
+- """
+- if sys.platform.startswith(("freebsd", "openbsd")):
+- shell = "/bin/sh"
+- else:
+- shell = "/bin/bash"
+-
+- ret = self.run_function(
+- "cmd.run_all",
+- ['echo "cheese" 1>&2', "shell={}".format(shell)],
+- python_shell=True,
+- )
+- self.assertTrue("pid" in ret)
+- self.assertTrue("retcode" in ret)
+- self.assertTrue("stdout" in ret)
+- self.assertTrue("stderr" in ret)
+- self.assertTrue(isinstance(ret.get("pid"), int))
+- self.assertTrue(isinstance(ret.get("retcode"), int))
+- self.assertTrue(isinstance(ret.get("stdout"), str))
+- self.assertTrue(isinstance(ret.get("stderr"), str))
+- self.assertEqual(
+- ret.get("stderr").rstrip(),
+- "cheese" if not salt.utils.platform.is_windows() else '"cheese"',
+- )
+-
+- @pytest.mark.slow_test
+- def test_retcode(self):
+- """
+- cmd.retcode
+- """
+- self.assertEqual(
+- self.run_function("cmd.retcode", ["exit 0"], python_shell=True), 0
+- )
+- self.assertEqual(
+- self.run_function("cmd.retcode", ["exit 1"], python_shell=True), 1
+- )
+-
+- @pytest.mark.slow_test
+- def test_run_all_with_success_retcodes(self):
+- """
+- cmd.run with success_retcodes
+- """
+- ret = self.run_function(
+- "cmd.run_all", ["exit 42"], success_retcodes=[42], python_shell=True
+- )
+-
+- self.assertTrue("retcode" in ret)
+- self.assertEqual(ret.get("retcode"), 0)
+-
+- @pytest.mark.slow_test
+- def test_retcode_with_success_retcodes(self):
+- """
+- cmd.run with success_retcodes
+- """
+- ret = self.run_function(
+- "cmd.retcode", ["exit 42"], success_retcodes=[42], python_shell=True
+- )
+-
+- self.assertEqual(ret, 0)
+-
+- @pytest.mark.slow_test
+- def test_run_all_with_success_stderr(self):
+- """
+- cmd.run with success_retcodes
+- """
+- random_file = "{}{}{}".format(
+- RUNTIME_VARS.TMP_ROOT_DIR, os.path.sep, random.random()
+- )
+-
+- if salt.utils.platform.is_windows():
+- func = "type"
+- expected_stderr = "cannot find the file specified"
+- else:
+- func = "cat"
+- expected_stderr = "No such file or directory"
+- ret = self.run_function(
+- "cmd.run_all",
+- ["{} {}".format(func, random_file)],
+- success_stderr=[expected_stderr],
+- python_shell=True,
+- )
+-
+- self.assertTrue("retcode" in ret)
+- self.assertEqual(ret.get("retcode"), 0)
+-
+- @pytest.mark.slow_test
+- def test_blacklist_glob(self):
+- """
+- cmd_blacklist_glob
+- """
+- self.assertEqual(
+- self.run_function("cmd.run", ["bad_command --foo"]).rstrip(),
+- 'ERROR: The shell command "bad_command --foo" is not permitted',
+- )
+-
+- @pytest.mark.slow_test
+- def test_script(self):
+- """
+- cmd.script
+- """
+- args = "saltines crackers biscuits=yes"
+- script = "salt://script.py"
+- ret = self.run_function("cmd.script", [script, args], saltenv="base")
+- self.assertEqual(ret["stdout"], args)
+-
+- @pytest.mark.slow_test
+- def test_script_query_string(self):
+- """
+- cmd.script
+- """
+- args = "saltines crackers biscuits=yes"
+- script = "salt://script.py?saltenv=base"
+- ret = self.run_function("cmd.script", [script, args], saltenv="base")
+- self.assertEqual(ret["stdout"], args)
+-
+- @pytest.mark.slow_test
+- def test_script_retcode(self):
+- """
+- cmd.script_retcode
+- """
+- script = "salt://script.py"
+- ret = self.run_function("cmd.script_retcode", [script], saltenv="base")
+- self.assertEqual(ret, 0)
+-
+- @pytest.mark.slow_test
+- def test_script_cwd(self):
+- """
+- cmd.script with cwd
+- """
+- tmp_cwd = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
+- args = "saltines crackers biscuits=yes"
+- script = "salt://script.py"
+- ret = self.run_function(
+- "cmd.script", [script, args], cwd=tmp_cwd, saltenv="base"
+- )
+- self.assertEqual(ret["stdout"], args)
+-
+- @pytest.mark.slow_test
+- def test_script_cwd_with_space(self):
+- """
+- cmd.script with cwd
+- """
+- tmp_cwd = "{}{}test 2".format(
+- tempfile.mkdtemp(dir=RUNTIME_VARS.TMP), os.path.sep
+- )
+- os.mkdir(tmp_cwd)
+-
+- args = "saltines crackers biscuits=yes"
+- script = "salt://script.py"
+- ret = self.run_function(
+- "cmd.script", [script, args], cwd=tmp_cwd, saltenv="base"
+- )
+- self.assertEqual(ret["stdout"], args)
+-
+- @pytest.mark.destructive_test
+- def test_tty(self):
+- """
+- cmd.tty
+- """
+- for tty in ("tty0", "pts3"):
+- if os.path.exists(os.path.join("/dev", tty)):
+- ret = self.run_function("cmd.tty", [tty, "apply salt liberally"])
+- self.assertTrue("Success" in ret)
+-
+- @pytest.mark.skip_on_windows
+- @pytest.mark.skip_if_binaries_missing("which")
+- def test_which(self):
+- """
+- cmd.which
+- """
+- cmd_which = self.run_function("cmd.which", ["cat"])
+- self.assertIsInstance(cmd_which, str)
+- cmd_run = self.run_function("cmd.run", ["which cat"])
+- self.assertIsInstance(cmd_run, str)
+- self.assertEqual(cmd_which.rstrip(), cmd_run.rstrip())
+-
+- @pytest.mark.skip_on_windows
+- @pytest.mark.skip_if_binaries_missing("which")
+- def test_which_bin(self):
+- """
+- cmd.which_bin
+- """
+- cmds = ["pip3", "pip2", "pip", "pip-python"]
+- ret = self.run_function("cmd.which_bin", [cmds])
+- self.assertTrue(os.path.split(ret)[1] in cmds)
+-
+- @pytest.mark.slow_test
+- def test_has_exec(self):
+- """
+- cmd.has_exec
+- """
+- self.assertTrue(
+- self.run_function("cmd.has_exec", [AVAILABLE_PYTHON_EXECUTABLE])
+- )
+- self.assertFalse(
+- self.run_function("cmd.has_exec", ["alllfsdfnwieulrrh9123857ygf"])
+- )
+-
+- @pytest.mark.slow_test
+- def test_exec_code(self):
+- """
+- cmd.exec_code
+- """
+- code = dedent(
+- """
+- import sys
+- sys.stdout.write('cheese')
+- """
+- )
+- self.assertEqual(
+- self.run_function(
+- "cmd.exec_code", [AVAILABLE_PYTHON_EXECUTABLE, code]
+- ).rstrip(),
+- "cheese",
+- )
+-
+- @pytest.mark.slow_test
+- def test_exec_code_with_single_arg(self):
+- """
+- cmd.exec_code
+- """
+- code = dedent(
+- """
+- import sys
+- sys.stdout.write(sys.argv[1])
+- """
+- )
+- arg = "cheese"
+- self.assertEqual(
+- self.run_function(
+- "cmd.exec_code", [AVAILABLE_PYTHON_EXECUTABLE, code], args=arg
+- ).rstrip(),
+- arg,
+- )
+-
+- @pytest.mark.slow_test
+- def test_exec_code_with_multiple_args(self):
+- """
+- cmd.exec_code
+- """
+- code = dedent(
+- """
+- import sys
+- sys.stdout.write(sys.argv[1])
+- """
+- )
+- arg = "cheese"
+- self.assertEqual(
+- self.run_function(
+- "cmd.exec_code", [AVAILABLE_PYTHON_EXECUTABLE, code], args=[arg, "test"]
+- ).rstrip(),
+- arg,
+- )
+-
+- @pytest.mark.slow_test
+- def test_quotes(self):
+- """
+- cmd.run with quoted command
+- """
+- cmd = """echo 'SELECT * FROM foo WHERE bar="baz"' """
+- expected_result = 'SELECT * FROM foo WHERE bar="baz"'
+- if salt.utils.platform.is_windows():
+- expected_result = "'SELECT * FROM foo WHERE bar=\"baz\"'"
+- result = self.run_function("cmd.run_stdout", [cmd]).strip()
+- self.assertEqual(result, expected_result)
+-
+- @pytest.mark.skip_if_not_root
+- @pytest.mark.skip_on_windows(reason="Skip on Windows, requires password")
+- def test_quotes_runas(self):
+- """
+- cmd.run with quoted command
+- """
+- cmd = """echo 'SELECT * FROM foo WHERE bar="baz"' """
+- expected_result = 'SELECT * FROM foo WHERE bar="baz"'
+- result = self.run_function(
+- "cmd.run_all", [cmd], runas=RUNTIME_VARS.RUNNING_TESTS_USER
+- )
+- errmsg = "The command returned: {}".format(result)
+- self.assertEqual(result["retcode"], 0, errmsg)
+- self.assertEqual(result["stdout"], expected_result, errmsg)
+-
+- @pytest.mark.destructive_test
+- @pytest.mark.skip_if_not_root
+- @pytest.mark.skip_on_windows(reason="Skip on Windows, uses unix commands")
+- @pytest.mark.slow_test
+- def test_avoid_injecting_shell_code_as_root(self):
+- """
+- cmd.run should execute the whole command as the "runas" user, not
+- running substitutions as root.
+- """
+- cmd = "echo $(id -u)"
+-
+- root_id = self.run_function("cmd.run_stdout", [cmd])
+- runas_root_id = self.run_function(
+- "cmd.run_stdout", [cmd], runas=RUNTIME_VARS.RUNNING_TESTS_USER
+- )
+- with self._ensure_user_exists(self.runas_usr):
+- user_id = self.run_function("cmd.run_stdout", [cmd], runas=self.runas_usr)
+-
+- self.assertNotEqual(user_id, root_id)
+- self.assertNotEqual(user_id, runas_root_id)
+- self.assertEqual(root_id, runas_root_id)
+-
+- @pytest.mark.destructive_test
+- @pytest.mark.skip_if_not_root
+- @pytest.mark.skip_on_windows(reason="Skip on Windows, uses unix commands")
+- @pytest.mark.slow_test
+- def test_cwd_runas(self):
+- """
+- cmd.run should be able to change working directory correctly, whether
+- or not runas is in use.
+- """
+- cmd = "pwd"
+- tmp_cwd = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
+- os.chmod(tmp_cwd, 0o711)
+-
+- cwd_normal = self.run_function("cmd.run_stdout", [cmd], cwd=tmp_cwd).rstrip(
+- "\n"
+- )
+- self.assertEqual(tmp_cwd, cwd_normal)
+-
+- with self._ensure_user_exists(self.runas_usr):
+- cwd_runas = self.run_function(
+- "cmd.run_stdout", [cmd], cwd=tmp_cwd, runas=self.runas_usr
+- ).rstrip("\n")
+- self.assertEqual(tmp_cwd, cwd_runas)
+-
+- @pytest.mark.destructive_test
+- @pytest.mark.skip_if_not_root
+- @pytest.mark.skip_unless_on_darwin(reason="Applicable to MacOS only")
+- @pytest.mark.slow_test
+- def test_runas_env(self):
+- """
+- cmd.run should be able to change working directory correctly, whether
+- or not runas is in use.
+- """
+- with self._ensure_user_exists(self.runas_usr):
+- user_path = self.run_function(
+- "cmd.run_stdout", ['printf %s "$PATH"'], runas=self.runas_usr
+- )
+- # XXX: Not sure of a better way. Environment starts out with
+- # /bin:/usr/bin and should be populated by path helper and the bash
+- # profile.
+- self.assertNotEqual("/bin:/usr/bin", user_path)
+-
+- @pytest.mark.destructive_test
+- @pytest.mark.skip_if_not_root
+- @pytest.mark.skip_unless_on_darwin(reason="Applicable to MacOS only")
+- @pytest.mark.slow_test
+- def test_runas_complex_command_bad_cwd(self):
+- """
+- cmd.run should not accidentally run parts of a complex command when
+- given a cwd which cannot be used by the user the command is run as.
+-
+- Due to the need to use `su -l` to login to another user on MacOS, we
+- cannot cd into directories that the target user themselves does not
+- have execute permission for. To an extent, this test is testing that
+- buggy behaviour, but its purpose is to ensure that the greater bug of
+- running commands after failing to cd does not occur.
+- """
+- tmp_cwd = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
+- os.chmod(tmp_cwd, 0o700)
+-
+- with self._ensure_user_exists(self.runas_usr):
+- cmd_result = self.run_function(
+- "cmd.run_all",
+- ['pwd; pwd; : $(echo "You have failed the test" >&2)'],
+- cwd=tmp_cwd,
+- runas=self.runas_usr,
+- )
+-
+- self.assertEqual("", cmd_result["stdout"])
+- self.assertNotIn("You have failed the test", cmd_result["stderr"])
+- self.assertNotEqual(0, cmd_result["retcode"])
+-
+- @SKIP_INITIAL_PHOTONOS_FAILURES
+- @pytest.mark.skip_on_windows
+- @pytest.mark.skip_if_not_root
+- @pytest.mark.destructive_test
+- @pytest.mark.slow_test
+- def test_runas(self):
+- """
+- Ensure that the env is the runas user's
+- """
+- with self._ensure_user_exists(self.runas_usr):
+- out = self.run_function(
+- "cmd.run", ["env"], runas=self.runas_usr
+- ).splitlines()
+- self.assertIn("USER={}".format(self.runas_usr), out)
+-
+- @pytest.mark.skip_if_binaries_missing("sleep", reason="sleep cmd not installed")
+- def test_timeout(self):
+- """
+- cmd.run trigger timeout
+- """
+- out = self.run_function(
+- "cmd.run", ["sleep 2 && echo hello"], f_timeout=1, python_shell=True
+- )
+- self.assertTrue("Timed out" in out)
+-
+- @pytest.mark.skip_if_binaries_missing("sleep", reason="sleep cmd not installed")
+- def test_timeout_success(self):
+- """
+- cmd.run sufficient timeout to succeed
+- """
+- out = self.run_function(
+- "cmd.run", ["sleep 1 && echo hello"], f_timeout=2, python_shell=True
+- )
+- self.assertEqual(out, "hello")
+-
+- @pytest.mark.slow_test
+- def test_hide_output(self):
+- """
+- Test the hide_output argument
+- """
+- ls_command = (
+- ["ls", "/"] if not salt.utils.platform.is_windows() else ["dir", "c:\\"]
+- )
+-
+- error_command = ["thiscommanddoesnotexist"]
+-
+- # cmd.run
+- out = self.run_function("cmd.run", ls_command, hide_output=True)
+- self.assertEqual(out, "")
+-
+- # cmd.shell
+- out = self.run_function("cmd.shell", ls_command, hide_output=True)
+- self.assertEqual(out, "")
+-
+- # cmd.run_stdout
+- out = self.run_function("cmd.run_stdout", ls_command, hide_output=True)
+- self.assertEqual(out, "")
+-
+- # cmd.run_stderr
+- out = self.run_function("cmd.shell", error_command, hide_output=True)
+- self.assertEqual(out, "")
+-
+- # cmd.run_all (command should have produced stdout)
+- out = self.run_function("cmd.run_all", ls_command, hide_output=True)
+- self.assertEqual(out["stdout"], "")
+- self.assertEqual(out["stderr"], "")
+-
+- # cmd.run_all (command should have produced stderr)
+- out = self.run_function("cmd.run_all", error_command, hide_output=True)
+- self.assertEqual(out["stdout"], "")
+- self.assertEqual(out["stderr"], "")
+-
+- @pytest.mark.slow_test
+- def test_cmd_run_whoami(self):
+- """
+- test return of whoami
+- """
+- if not salt.utils.platform.is_windows():
+- user = RUNTIME_VARS.RUNTIME_CONFIGS["master"]["user"]
+- else:
+- user = salt.utils.user.get_specific_user()
+- if user.startswith("sudo_"):
+- user = user.replace("sudo_", "")
+- cmd = self.run_function("cmd.run", ["whoami"])
+- try:
+- self.assertEqual(user.lower(), cmd.lower())
+- except AssertionError as exc:
+- if not salt.utils.platform.is_windows():
+- raise exc from None
+- if "\\" in user:
+- user = user.split("\\")[-1]
+- self.assertEqual(user.lower(), cmd.lower())
+-
+- @pytest.mark.skip_unless_on_windows(reason="Minion is not Windows")
+- @pytest.mark.slow_test
+- def test_windows_env_handling(self):
+- """
+- Ensure that nt.environ is used properly with cmd.run*
+- """
+- out = self.run_function(
+- "cmd.run", ["set"], env={"abc": "123", "ABC": "456"}
+- ).splitlines()
+- self.assertIn("abc=123", out)
+- self.assertIn("ABC=456", out)
+-
+- @pytest.mark.slow_test
+- @pytest.mark.skip_unless_on_windows(reason="Minion is not Windows")
+- def test_windows_powershell_script_args(self):
+- """
+- Ensure that powershell processes inline script in args
+- """
+- val = "i like cheese"
+- args = (
+- '-SecureString (ConvertTo-SecureString -String "{}" -AsPlainText -Force)'
+- " -ErrorAction Stop".format(val)
+- )
+- script = "salt://issue-56195/test.ps1"
+- ret = self.run_function(
+- "cmd.script", [script], args=args, shell="powershell", saltenv="base"
+- )
+- self.assertEqual(ret["stdout"], val)
+-
+- @pytest.mark.slow_test
+- @pytest.mark.skip_unless_on_windows(reason="Minion is not Windows")
+- @pytest.mark.skip_if_binaries_missing("pwsh")
+- def test_windows_powershell_script_args_pwsh(self):
+- """
+- Ensure that powershell processes inline script in args with powershell
+- core
+- """
+- val = "i like cheese"
+- args = (
+- '-SecureString (ConvertTo-SecureString -String "{}" -AsPlainText -Force)'
+- " -ErrorAction Stop".format(val)
+- )
+- script = "salt://issue-56195/test.ps1"
+- ret = self.run_function(
+- "cmd.script", [script], args=args, shell="pwsh", saltenv="base"
+- )
+- self.assertEqual(ret["stdout"], val)
+diff --git a/tests/integration/modules/test_cp.py b/tests/integration/modules/test_cp.py
+index ad7538b4ba8..cd3e4c2f5ad 100644
+--- a/tests/integration/modules/test_cp.py
++++ b/tests/integration/modules/test_cp.py
+@@ -234,9 +234,9 @@ class CPModuleTest(ModuleCase):
+ self.run_function("cp.get_url", ["https://repo.saltproject.io/index.html", tgt])
+ with salt.utils.files.fopen(tgt, "r") as instructions:
+ data = salt.utils.stringutils.to_unicode(instructions.read())
+- self.assertIn("Bootstrap", data)
+- self.assertIn("Debian", data)
+- self.assertIn("Windows", data)
++ self.assertIn("Salt Project", data)
++ self.assertIn("Package", data)
++ self.assertIn("Repo", data)
+ self.assertNotIn("AYBABTU", data)
+
+ @pytest.mark.slow_test
+@@ -250,9 +250,9 @@ class CPModuleTest(ModuleCase):
+
+ with salt.utils.files.fopen(ret, "r") as instructions:
+ data = salt.utils.stringutils.to_unicode(instructions.read())
+- self.assertIn("Bootstrap", data)
+- self.assertIn("Debian", data)
+- self.assertIn("Windows", data)
++ self.assertIn("Salt Project", data)
++ self.assertIn("Package", data)
++ self.assertIn("Repo", data)
+ self.assertNotIn("AYBABTU", data)
+
+ @pytest.mark.slow_test
+@@ -273,9 +273,9 @@ class CPModuleTest(ModuleCase):
+ time.sleep(sleep)
+ if ret.find("HTTP 599") != -1:
+ raise Exception("https://repo.saltproject.io/index.html returned 599 error")
+- self.assertIn("Bootstrap", ret)
+- self.assertIn("Debian", ret)
+- self.assertIn("Windows", ret)
++ self.assertIn("Salt Project", ret)
++ self.assertIn("Package", ret)
++ self.assertIn("Repo", ret)
+ self.assertNotIn("AYBABTU", ret)
+
+ @pytest.mark.slow_test
+@@ -346,9 +346,9 @@ class CPModuleTest(ModuleCase):
+ """
+ src = "https://repo.saltproject.io/index.html"
+ ret = self.run_function("cp.get_file_str", [src])
+- self.assertIn("Bootstrap", ret)
+- self.assertIn("Debian", ret)
+- self.assertIn("Windows", ret)
++ self.assertIn("Salt Project", ret)
++ self.assertIn("Package", ret)
++ self.assertIn("Repo", ret)
+ self.assertNotIn("AYBABTU", ret)
+
+ @pytest.mark.slow_test
+diff --git a/tests/integration/modules/test_timezone.py b/tests/integration/modules/test_timezone.py
+index 8d7180cbd13..c1dc8a7b73d 100644
+--- a/tests/integration/modules/test_timezone.py
++++ b/tests/integration/modules/test_timezone.py
+@@ -4,6 +4,7 @@ Integration tests for timezone module
+ Linux and Solaris are supported
+ """
+ import pytest
++import os
+
+ from tests.support.case import ModuleCase
+
+@@ -15,6 +16,8 @@ except ImportError:
+ HAS_TZLOCAL = False
+
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++@pytest.mark.skipif(INSIDE_CONTAINER, reason="No hwclock in a container")
+ class TimezoneLinuxModuleTest(ModuleCase):
+ def setUp(self):
+ """
+diff --git a/tests/integration/pillar/test_git_pillar.py b/tests/integration/pillar/test_git_pillar.py
+index 68c14daaa15..5b4cbda95c9 100644
+--- a/tests/integration/pillar/test_git_pillar.py
++++ b/tests/integration/pillar/test_git_pillar.py
+@@ -63,6 +63,7 @@ https://github.com/git/git/commit/6bc0cb5
+ https://github.com/unbit/uwsgi/commit/ac1e354
+ """
+
++import os
+ import random
+ import string
+ import sys
+@@ -100,9 +101,11 @@ try:
+ except Exception: # pylint: disable=broad-except
+ HAS_PYGIT2 = False
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
+ pytestmark = [
+ SKIP_INITIAL_PHOTONOS_FAILURES,
+ pytest.mark.skip_on_platforms(windows=True, darwin=True),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Communication problems between containers."),
+ ]
+
+
+diff --git a/tests/integration/ssh/test_state.py b/tests/integration/ssh/test_state.py
+index a9fd3e7f2d3..69245454e85 100644
+--- a/tests/integration/ssh/test_state.py
++++ b/tests/integration/ssh/test_state.py
+@@ -283,53 +283,6 @@ class SSHStateTest(SSHCase):
+ check_file = self.run_function("file.file_exists", [SSH_SLS_FILE], wipe=False)
+ self.assertTrue(check_file)
+
+- @pytest.mark.slow_test
+- def test_state_running(self):
+- """
+- test state.running with salt-ssh
+- """
+-
+- retval = []
+-
+- def _run_in_background():
+- retval.append(self.run_function("state.sls", ["running"], wipe=False))
+-
+- bg_thread = threading.Thread(target=_run_in_background)
+- bg_thread.start()
+-
+- expected = 'The function "state.pkg" is running as'
+- state_ret = []
+- for _ in range(30):
+- if not bg_thread.is_alive():
+- continue
+- get_sls = self.run_function("state.running", wipe=False)
+- state_ret.append(get_sls)
+- if expected in " ".join(get_sls):
+- # We found the expected return
+- break
+- time.sleep(1)
+- else:
+- if not bg_thread.is_alive():
+- bg_failed_msg = "Failed to return clean data"
+- if retval and bg_failed_msg in retval.pop().get("_error", ""):
+- pytest.skip("Background state run failed, skipping")
+- self.fail(
+- "Did not find '{}' in state.running return: {}".format(
+- expected, state_ret
+- )
+- )
+-
+- # make sure we wait until the earlier state is complete
+- future = time.time() + 120
+- while True:
+- if expected not in " ".join(self.run_function("state.running", wipe=False)):
+- break
+- if time.time() > future:
+- self.fail(
+- "state.pkg is still running overtime. Test did not clean up"
+- " correctly."
+- )
+-
+ def tearDown(self):
+ """
+ make sure to clean up any old ssh directories
+diff --git a/tests/pytests/functional/cache/test_consul.py b/tests/pytests/functional/cache/test_consul.py
+index 3a38e495a93..c6e16d2588e 100644
+--- a/tests/pytests/functional/cache/test_consul.py
++++ b/tests/pytests/functional/cache/test_consul.py
+@@ -1,4 +1,5 @@
+ import logging
++import os
+ import socket
+ import time
+
+@@ -13,9 +14,12 @@ docker = pytest.importorskip("docker")
+
+ log = logging.getLogger(__name__)
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++
+ pytestmark = [
+ pytest.mark.slow_test,
+ pytest.mark.skip_if_binaries_missing("dockerd"),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"),
+ ]
+
+
+diff --git a/tests/pytests/functional/cache/test_mysql.py b/tests/pytests/functional/cache/test_mysql.py
+index c283872c08c..e15fc732a4a 100644
+--- a/tests/pytests/functional/cache/test_mysql.py
++++ b/tests/pytests/functional/cache/test_mysql.py
+@@ -1,4 +1,5 @@
+ import logging
++import os
+
+ import pytest
+
+@@ -11,9 +12,12 @@ docker = pytest.importorskip("docker")
+
+ log = logging.getLogger(__name__)
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++
+ pytestmark = [
+ pytest.mark.slow_test,
+ pytest.mark.skip_if_binaries_missing("dockerd"),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"),
+ ]
+
+
+diff --git a/tests/pytests/functional/fileserver/hgfs/test_hgfs.py b/tests/pytests/functional/fileserver/hgfs/test_hgfs.py
+index 571fe75e403..bfd927fd0fe 100644
+--- a/tests/pytests/functional/fileserver/hgfs/test_hgfs.py
++++ b/tests/pytests/functional/fileserver/hgfs/test_hgfs.py
+@@ -16,6 +16,8 @@ try:
+ except ImportError:
+ HAS_HG = False
+
++pytestmark = [pytest.mark.skipif(not HAS_HG, reason="missing hglib library")]
++
+
+ @pytest.fixture(scope="module")
+ def configure_loader_modules(master_opts):
+diff --git a/tests/pytests/functional/modules/test_cmdmod.py b/tests/pytests/functional/modules/test_cmdmod.py
+new file mode 100644
+index 00000000000..d30b474c6d2
+--- /dev/null
++++ b/tests/pytests/functional/modules/test_cmdmod.py
+@@ -0,0 +1,561 @@
++import os
++import random
++import sys
++from contextlib import contextmanager
++
++import pytest
++
++import salt.config
++import salt.utils.path
++import salt.utils.platform
++import salt.utils.user
++from tests.support.helpers import SKIP_INITIAL_PHOTONOS_FAILURES, dedent
++
++pytestmark = [pytest.mark.windows_whitelisted]
++
++
++@pytest.fixture(scope="module")
++def cmdmod(modules):
++ return modules.cmd
++
++
++@pytest.fixture(scope="module")
++def usermod(modules):
++ return modules.user
++
++
++@pytest.fixture(scope="module")
++def available_python_executable():
++ yield salt.utils.path.which_bin(["python", "python3"])
++
++
++@pytest.fixture
++def runas_usr():
++ runas_usr = "nobody"
++ if salt.utils.platform.is_darwin():
++ runas_usr = "macsalttest"
++ yield runas_usr
++
++
++@pytest.fixture
++def running_username():
++ """
++ Return the username that is running the code.
++ """
++ return salt.utils.user.get_user()
++
++
++@pytest.fixture
++def script_contents(state_tree):
++ _contents = """
++ #!/usr/bin/env python3
++ import sys
++ print(" ".join(sys.argv[1:]))
++ """
++
++ with pytest.helpers.temp_file("script.py", _contents, state_tree):
++ yield
++
++
++@pytest.fixture
++def issue_56195_test_ps1(state_tree):
++ _contents = """
++ [CmdLetBinding()]
++ Param(
++ [SecureString] $SecureString
++ )
++ $Credential = New-Object System.Net.NetworkCredential("DummyId", $SecureString)
++ $Credential.Password
++ """
++
++ with pytest.helpers.temp_file("issue_56195_test.ps1", _contents, state_tree):
++ yield
++
++
++@contextmanager
++def _ensure_user_exists(name, usermod):
++ if name in usermod.info(name).values():
++ # User already exists; don't touch
++ yield
++ else:
++ # Need to create user for test
++ usermod.add(name)
++ try:
++ yield
++ finally:
++ usermod.delete(name, remove=True)
++
++
++@pytest.mark.slow_test
++def test_run(cmdmod):
++ """
++ cmd.run
++ """
++ shell = os.environ.get("SHELL")
++ if shell is None:
++ # Failed to get the SHELL var, don't run
++ pytest.skip("Unable to get the SHELL environment variable")
++
++ assert cmdmod.run("echo $SHELL")
++ assert cmdmod.run("echo $SHELL", shell=shell, python_shell=True).rstrip() == shell
++ assert cmdmod.run("ls / | grep etc", python_shell=True) == "etc"
++ assert (
++ cmdmod.run(
++ 'echo {{grains.id}} | awk "{print $1}"',
++ template="jinja",
++ python_shell=True,
++ )
++ == "func-tests-minion"
++ )
++ assert cmdmod.run("grep f", stdin="one\ntwo\nthree\nfour\nfive\n") == "four\nfive"
++ assert cmdmod.run('echo "a=b" | sed -e s/=/:/g', python_shell=True) == "a:b"
++
++
++@pytest.mark.slow_test
++def test_stdout(cmdmod):
++ """
++ cmd.run_stdout
++ """
++ assert (
++ cmdmod.run_stdout('echo "cheese"').rstrip() == "cheese"
++ if not salt.utils.platform.is_windows()
++ else '"cheese"'
++ )
++
++
++@pytest.mark.slow_test
++def test_stderr(cmdmod):
++ """
++ cmd.run_stderr
++ """
++ if sys.platform.startswith(("freebsd", "openbsd")):
++ shell = "/bin/sh"
++ else:
++ shell = "/bin/bash"
++
++ assert (
++ cmdmod.run_stderr(
++ 'echo "cheese" 1>&2',
++ shell=shell,
++ python_shell=True,
++ ).rstrip()
++ == "cheese"
++ if not salt.utils.platform.is_windows()
++ else '"cheese"'
++ )
++
++
++@pytest.mark.slow_test
++def test_run_all(cmdmod):
++ """
++ cmd.run_all
++ """
++ if sys.platform.startswith(("freebsd", "openbsd")):
++ shell = "/bin/sh"
++ else:
++ shell = "/bin/bash"
++
++ ret = cmdmod.run_all(
++ 'echo "cheese" 1>&2',
++ shell=shell,
++ python_shell=True,
++ )
++ assert "pid" in ret
++ assert "retcode" in ret
++ assert "stdout" in ret
++ assert "stderr" in ret
++ assert isinstance(ret.get("pid"), int)
++ assert isinstance(ret.get("retcode"), int)
++ assert isinstance(ret.get("stdout"), str)
++ assert isinstance(ret.get("stderr"), str)
++ assert (
++ ret.get("stderr").rstrip() == "cheese"
++ if not salt.utils.platform.is_windows()
++ else '"cheese"'
++ )
++
++
++@pytest.mark.slow_test
++def test_retcode(cmdmod):
++ """
++ cmd.retcode
++ """
++ assert cmdmod.retcode("exit 0", python_shell=True) == 0
++ assert cmdmod.retcode("exit 1", python_shell=True) == 1
++
++
++@pytest.mark.slow_test
++def test_run_all_with_success_retcodes(cmdmod):
++ """
++ cmd.run with success_retcodes
++ """
++ ret = cmdmod.run_all("exit 42", success_retcodes=[42], python_shell=True)
++
++ assert "retcode" in ret
++ assert ret.get("retcode") == 0
++
++
++@pytest.mark.slow_test
++def test_retcode_with_success_retcodes(cmdmod):
++ """
++ cmd.run with success_retcodes
++ """
++ ret = cmdmod.retcode("exit 42", success_retcodes=[42], python_shell=True)
++
++ assert ret == 0
++
++
++@pytest.mark.slow_test
++def test_run_all_with_success_stderr(cmdmod, tmp_path):
++ """
++ cmd.run with success_retcodes
++ """
++ random_file = str(tmp_path / f"{random.random()}")
++
++ if salt.utils.platform.is_windows():
++ func = "type"
++ expected_stderr = "cannot find the file specified"
++ else:
++ func = "cat"
++ expected_stderr = "No such file or directory"
++ ret = cmdmod.run_all(
++ f"{func} {random_file}",
++ success_stderr=[expected_stderr],
++ python_shell=True,
++ )
++
++ assert "retcode" in ret
++ assert ret.get("retcode") == 0
++
++
++@pytest.mark.slow_test
++def test_script(cmdmod, script_contents):
++ """
++ cmd.script
++ """
++ args = "saltines crackers biscuits=yes"
++ script = "salt://script.py"
++ ret = cmdmod.script(script, args, saltenv="base")
++ assert ret["stdout"] == args
++
++
++@pytest.mark.slow_test
++def test_script_query_string(cmdmod, script_contents):
++ """
++ cmd.script
++ """
++ args = "saltines crackers biscuits=yes"
++ script = "salt://script.py?saltenv=base"
++ ret = cmdmod.script(script, args, saltenv="base")
++ assert ret["stdout"] == args
++
++
++@pytest.mark.slow_test
++def test_script_retcode(cmdmod, script_contents):
++ """
++ cmd.script_retcode
++ """
++ script = "salt://script.py"
++ ret = cmdmod.script_retcode(script, saltenv="base")
++ assert ret == 0
++
++
++@pytest.mark.slow_test
++def test_script_cwd(cmdmod, script_contents, tmp_path):
++ """
++ cmd.script with cwd
++ """
++ tmp_cwd = str(tmp_path)
++ args = "saltines crackers biscuits=yes"
++ script = "salt://script.py"
++ ret = cmdmod.script(script, args, cwd=tmp_cwd, saltenv="base")
++ assert ret["stdout"] == args
++
++
++@pytest.mark.slow_test
++def test_script_cwd_with_space(cmdmod, script_contents, tmp_path):
++ """
++ cmd.script with cwd
++ """
++ tmp_cwd = str(tmp_path / "test 2")
++ os.mkdir(tmp_cwd)
++
++ args = "saltines crackers biscuits=yes"
++ script = "salt://script.py"
++ ret = cmdmod.script(script, args, cwd=tmp_cwd, saltenv="base")
++ assert ret["stdout"] == args
++
++
++@pytest.mark.destructive_test
++def test_tty(cmdmod):
++ """
++ cmd.tty
++ """
++ for tty in ("tty0", "pts3"):
++ if os.path.exists(os.path.join("/dev", tty)):
++ ret = cmdmod.tty(tty, "apply salt liberally")
++ assert "Success" in ret
++
++
++@pytest.mark.skip_on_windows
++@pytest.mark.skip_if_binaries_missing("which")
++def test_which(cmdmod):
++ """
++ cmd.which
++ """
++ cmd_which = cmdmod.which("cat")
++ assert isinstance(cmd_which, str)
++ cmd_run = cmdmod.run("which cat")
++ assert isinstance(cmd_run, str)
++ assert cmd_which.rstrip() == cmd_run.rstrip()
++
++
++@pytest.mark.skip_on_windows
++@pytest.mark.skip_if_binaries_missing("which")
++def test_which_bin(cmdmod):
++ """
++ cmd.which_bin
++ """
++ cmds = ["pip3", "pip2", "pip", "pip-python"]
++ ret = cmdmod.which_bin(cmds)
++ assert os.path.split(ret)[1] in cmds
++
++
++@pytest.mark.slow_test
++def test_has_exec(cmdmod, available_python_executable):
++ """
++ cmd.has_exec
++ """
++ assert cmdmod.has_exec(available_python_executable)
++ assert not cmdmod.has_exec("alllfsdfnwieulrrh9123857ygf")
++
++
++@pytest.mark.slow_test
++def test_exec_code(cmdmod, available_python_executable):
++ """
++ cmd.exec_code
++ """
++ code = dedent(
++ """
++ import sys
++ sys.stdout.write('cheese')
++ """
++ )
++ assert cmdmod.exec_code(available_python_executable, code).rstrip() == "cheese"
++
++
++@pytest.mark.slow_test
++def test_exec_code_with_single_arg(cmdmod, available_python_executable):
++ """
++ cmd.exec_code
++ """
++ code = dedent(
++ """
++ import sys
++ sys.stdout.write(sys.argv[1])
++ """
++ )
++ arg = "cheese"
++ assert cmdmod.exec_code(available_python_executable, code, args=arg).rstrip() == arg
++
++
++@pytest.mark.slow_test
++def test_exec_code_with_multiple_args(cmdmod, available_python_executable):
++ """
++ cmd.exec_code
++ """
++ code = dedent(
++ """
++ import sys
++ sys.stdout.write(sys.argv[1])
++ """
++ )
++ arg = "cheese"
++ assert (
++ cmdmod.exec_code(available_python_executable, code, args=[arg, "test"]).rstrip()
++ == arg
++ )
++
++
++@pytest.mark.slow_test
++def test_quotes(cmdmod):
++ """
++ cmd.run with quoted command
++ """
++ cmd = """echo 'SELECT * FROM foo WHERE bar="baz"' """
++ expected_result = 'SELECT * FROM foo WHERE bar="baz"'
++ result = cmdmod.run_stdout(cmd).strip()
++ assert result == expected_result
++
++
++@pytest.mark.skip_if_not_root
++@pytest.mark.skip_on_windows(reason="Skip on Windows, requires password")
++def test_quotes_runas(cmdmod, running_username):
++ """
++ cmd.run with quoted command
++ """
++ cmd = """echo 'SELECT * FROM foo WHERE bar="baz"' """
++ expected_result = 'SELECT * FROM foo WHERE bar="baz"'
++ result = cmdmod.run_all(cmd, runas=running_username)
++ errmsg = f"The command returned: {result}"
++ assert result["retcode"] == 0, errmsg
++ assert result["stdout"] == expected_result, errmsg
++
++
++@pytest.mark.destructive_test
++@pytest.mark.skip_if_not_root
++@pytest.mark.skip_on_windows(reason="Skip on Windows, uses unix commands")
++@pytest.mark.slow_test
++def test_cwd_runas(cmdmod, usermod, runas_usr, tmp_path):
++ """
++ cmd.run should be able to change working directory correctly, whether
++ or not runas is in use.
++ """
++ cmd = "pwd"
++ tmp_cwd = str(tmp_path)
++ os.chmod(tmp_cwd, 0o711)
++
++ cwd_normal = cmdmod.run_stdout(cmd, cwd=tmp_cwd).rstrip("\n")
++ assert tmp_cwd == cwd_normal
++
++ with _ensure_user_exists(runas_usr, usermod):
++ cwd_runas = cmdmod.run_stdout(cmd, cwd=tmp_cwd, runas=runas_usr).rstrip("\n")
++ assert tmp_cwd == cwd_runas
++
++
++@pytest.mark.destructive_test
++@pytest.mark.skip_if_not_root
++@pytest.mark.skip_unless_on_darwin(reason="Applicable to MacOS only")
++@pytest.mark.slow_test
++def test_runas_env(cmdmod, usermod, runas_usr):
++ """
++ cmd.run should be able to change working directory correctly, whether
++ or not runas is in use.
++ """
++ with _ensure_user_exists(runas_usr, usermod):
++ user_path = cmdmod.run_stdout('printf %s "$PATH"', runas=runas_usr)
++ # XXX: Not sure of a better way. Environment starts out with
++ # /bin:/usr/bin and should be populated by path helper and the bash
++ # profile.
++ assert "/bin:/usr/bin" != user_path
++
++
++@pytest.mark.destructive_test
++@pytest.mark.skip_if_not_root
++@pytest.mark.skip_unless_on_darwin(reason="Applicable to MacOS only")
++@pytest.mark.slow_test
++def test_runas_complex_command_bad_cwd(cmdmod, usermod, runas_usr, tmp_path):
++ """
++ cmd.run should not accidentally run parts of a complex command when
++ given a cwd which cannot be used by the user the command is run as.
++ Due to the need to use `su -l` to login to another user on MacOS, we
++ cannot cd into directories that the target user themselves does not
++ have execute permission for. To an extent, this test is testing that
++ buggy behaviour, but its purpose is to ensure that the greater bug of
++ running commands after failing to cd does not occur.
++ """
++ tmp_cwd = str(tmp_path)
++ os.chmod(tmp_cwd, 0o700)
++
++ with _ensure_user_exists(runas_usr, usermod):
++ cmd_result = cmdmod.run_all(
++ 'pwd; pwd; : $(echo "You have failed the test" >&2)',
++ cwd=tmp_cwd,
++ runas=runas_usr,
++ )
++
++ assert "" == cmd_result["stdout"]
++ assert "You have failed the test" not in cmd_result["stderr"]
++ assert 0 != cmd_result["retcode"]
++
++
++@SKIP_INITIAL_PHOTONOS_FAILURES
++@pytest.mark.skip_on_windows
++@pytest.mark.skip_if_not_root
++@pytest.mark.destructive_test
++@pytest.mark.slow_test
++def test_runas(cmdmod, usermod, runas_usr):
++ """
++ Ensure that the env is the runas user's
++ """
++ with _ensure_user_exists(runas_usr, usermod):
++ out = cmdmod.run("env", runas=runas_usr).splitlines()
++ assert f"USER={runas_usr}" in out
++
++
++@pytest.mark.skip_if_binaries_missing("sleep", reason="sleep cmd not installed")
++def test_timeout(cmdmod):
++ """
++ cmd.run trigger timeout
++ """
++ out = cmdmod.run("sleep 2 && echo hello", timeout=1, python_shell=True)
++ assert "Timed out" in out
++
++
++@pytest.mark.skip_if_binaries_missing("sleep", reason="sleep cmd not installed")
++def test_timeout_success(cmdmod):
++ """
++ cmd.run sufficient timeout to succeed
++ """
++ out = cmdmod.run("sleep 1 && echo hello", timeout=2, python_shell=True)
++ assert out == "hello"
++
++
++@pytest.mark.slow_test
++def test_cmd_run_whoami(cmdmod, running_username):
++ """
++ test return of whoami
++ """
++ if not salt.utils.platform.is_windows():
++ user = running_username
++ else:
++ user = salt.utils.user.get_specific_user()
++ if user.startswith("sudo_"):
++ user = user.replace("sudo_", "")
++ cmd = cmdmod.run("whoami")
++ assert user.lower() == cmd.lower()
++
++
++@pytest.mark.skip_unless_on_windows(reason="Minion is not Windows")
++@pytest.mark.slow_test
++def test_windows_env_handling(cmdmod):
++ """
++ Ensure that nt.environ is used properly with cmd.run*
++ """
++ out = cmdmod.run("set", env={"abc": "123", "ABC": "456"}).splitlines()
++ assert "abc=123" in out
++ assert "ABC=456" in out
++
++
++@pytest.mark.slow_test
++@pytest.mark.skip_unless_on_windows(reason="Minion is not Windows")
++def test_windows_powershell_script_args(cmdmod, issue_56195_test_ps1):
++ """
++ Ensure that powershell processes inline script in args
++ """
++ val = "i like cheese"
++ args = (
++ '-SecureString (ConvertTo-SecureString -String "{}" -AsPlainText -Force)'
++ " -ErrorAction Stop".format(val)
++ )
++ script = "salt://issue_56195_test.ps1"
++ ret = cmdmod.script(script, args=args, shell="powershell", saltenv="base")
++ assert ret["stdout"] == val
++
++
++@pytest.mark.slow_test
++@pytest.mark.skip_unless_on_windows(reason="Minion is not Windows")
++@pytest.mark.skip_if_binaries_missing("pwsh")
++def test_windows_powershell_script_args_pwsh(cmdmod, issue_56195_test_ps1):
++ """
++ Ensure that powershell processes inline script in args with powershell
++ core
++ """
++ val = "i like cheese"
++ args = (
++ '-SecureString (ConvertTo-SecureString -String "{}" -AsPlainText -Force)'
++ " -ErrorAction Stop".format(val)
++ )
++ script = "salt://issue_56195_test.ps1"
++ ret = cmdmod.script(script, args=args, shell="pwsh", saltenv="base")
++ assert ret["stdout"] == val
+diff --git a/tests/pytests/functional/modules/test_dockermod.py b/tests/pytests/functional/modules/test_dockermod.py
+index 3c7bb25e461..a5b40869352 100644
+--- a/tests/pytests/functional/modules/test_dockermod.py
++++ b/tests/pytests/functional/modules/test_dockermod.py
+@@ -2,6 +2,7 @@
+ Integration tests for the docker_container states
+ """
+ import logging
++import os
+
+ import pytest
+ from saltfactories.utils import random_string
+@@ -11,9 +12,12 @@ pytest.importorskip("docker")
+
+ log = logging.getLogger(__name__)
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++
+ pytestmark = [
+ pytest.mark.slow_test,
+ pytest.mark.skip_if_binaries_missing("docker", "dockerd", check_all=False),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run inside a container"),
+ ]
+
+
+diff --git a/tests/pytests/functional/modules/test_swarm.py b/tests/pytests/functional/modules/test_swarm.py
+index 8c0ce8cbd93..9dc70f5b3dc 100644
+--- a/tests/pytests/functional/modules/test_swarm.py
++++ b/tests/pytests/functional/modules/test_swarm.py
+@@ -1,10 +1,15 @@
++import os
++
+ import pytest
+
+ import salt.utils.versions
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++
+ pytestmark = [
+ pytest.mark.slow_test,
+ pytest.mark.skip_if_binaries_missing("dockerd"),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="No hwclock in a container"),
+ ]
+
+ # The swarm module need the docker-py library installed
+diff --git a/tests/pytests/functional/modules/test_system.py b/tests/pytests/functional/modules/test_system.py
+index 2dabaaebfad..3b669c46afd 100644
+--- a/tests/pytests/functional/modules/test_system.py
++++ b/tests/pytests/functional/modules/test_system.py
+@@ -9,9 +9,12 @@ import pytest
+
+ import salt.utils.files
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++
+ pytestmark = [
+ pytest.mark.skip_unless_on_linux,
+ pytest.mark.slow_test,
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="No systemd in container."),
+ ]
+
+ log = logging.getLogger(__name__)
+diff --git a/tests/pytests/functional/pillar/hg_pillar/test_hg_pillar.py b/tests/pytests/functional/pillar/hg_pillar/test_hg_pillar.py
+index 183b002d8b2..44603d96f1d 100644
+--- a/tests/pytests/functional/pillar/hg_pillar/test_hg_pillar.py
++++ b/tests/pytests/functional/pillar/hg_pillar/test_hg_pillar.py
+@@ -60,6 +60,7 @@ def hg_setup_and_teardown():
+ @pytest.mark.skip_on_windows(
+ reason="just testing if this or hgfs causes the issue with total crash"
+ )
++@pytest.mark.skipif(not HAS_HG, reason="missing hglib library")
+ def test_ext_pillar(hg_setup_and_teardown):
+ data = hg_pillar.ext_pillar("*", None, hg_setup_and_teardown)
+ assert data == {"testinfo": "info", "testinfo2": "info"}
+diff --git a/tests/pytests/functional/states/rabbitmq/test_cluster.py b/tests/pytests/functional/states/rabbitmq/test_cluster.py
+index f8b4bdc225e..210b22a2360 100644
+--- a/tests/pytests/functional/states/rabbitmq/test_cluster.py
++++ b/tests/pytests/functional/states/rabbitmq/test_cluster.py
+@@ -3,6 +3,7 @@ Integration tests for the rabbitmq_cluster states
+ """
+
+ import logging
++import os
+
+ import pytest
+
+@@ -13,11 +14,14 @@ pytest.importorskip("docker")
+
+ log = logging.getLogger(__name__)
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++
+ pytestmark = [
+ pytest.mark.slow_test,
+ pytest.mark.skip_if_binaries_missing(
+ "docker", "dockerd", reason="Docker not installed"
+ ),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"),
+ ]
+
+
+diff --git a/tests/pytests/functional/states/rabbitmq/test_plugin.py b/tests/pytests/functional/states/rabbitmq/test_plugin.py
+index e1b686e3365..f1191490536 100644
+--- a/tests/pytests/functional/states/rabbitmq/test_plugin.py
++++ b/tests/pytests/functional/states/rabbitmq/test_plugin.py
+@@ -3,6 +3,7 @@ Integration tests for the rabbitmq_plugin states
+ """
+
+ import logging
++import os
+
+ import pytest
+
+@@ -14,11 +15,14 @@ log = logging.getLogger(__name__)
+
+ pytest.importorskip("docker")
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++
+ pytestmark = [
+ pytest.mark.slow_test,
+ pytest.mark.skip_if_binaries_missing(
+ "docker", "dockerd", reason="Docker not installed"
+ ),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"),
+ ]
+
+
+diff --git a/tests/pytests/functional/states/rabbitmq/test_policy.py b/tests/pytests/functional/states/rabbitmq/test_policy.py
+index e5cee97cbc8..7ccf6a522e0 100644
+--- a/tests/pytests/functional/states/rabbitmq/test_policy.py
++++ b/tests/pytests/functional/states/rabbitmq/test_policy.py
+@@ -3,6 +3,7 @@ Integration tests for the rabbitmq_policy states
+ """
+
+ import logging
++import os
+
+ import pytest
+
+@@ -14,11 +15,14 @@ log = logging.getLogger(__name__)
+
+ pytest.importorskip("docker")
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++
+ pytestmark = [
+ pytest.mark.slow_test,
+ pytest.mark.skip_if_binaries_missing(
+ "docker", "dockerd", reason="Docker not installed"
+ ),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"),
+ ]
+
+
+diff --git a/tests/pytests/functional/states/rabbitmq/test_upstream.py b/tests/pytests/functional/states/rabbitmq/test_upstream.py
+index cfdad35aba6..c7bcf3b0d44 100644
+--- a/tests/pytests/functional/states/rabbitmq/test_upstream.py
++++ b/tests/pytests/functional/states/rabbitmq/test_upstream.py
+@@ -3,6 +3,7 @@ Integration tests for the rabbitmq_user states
+ """
+
+ import logging
++import os
+
+ import pytest
+
+@@ -13,11 +14,14 @@ log = logging.getLogger(__name__)
+
+ pytest.importorskip("docker")
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++
+ pytestmark = [
+ pytest.mark.slow_test,
+ pytest.mark.skip_if_binaries_missing(
+ "docker", "dockerd", reason="Docker not installed"
+ ),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"),
+ ]
+
+
+diff --git a/tests/pytests/functional/states/rabbitmq/test_user.py b/tests/pytests/functional/states/rabbitmq/test_user.py
+index 2f9b22d28d2..31723df7be8 100644
+--- a/tests/pytests/functional/states/rabbitmq/test_user.py
++++ b/tests/pytests/functional/states/rabbitmq/test_user.py
+@@ -3,6 +3,7 @@ Integration tests for the rabbitmq_user states
+ """
+
+ import logging
++import os
+
+ import pytest
+
+@@ -13,11 +14,14 @@ log = logging.getLogger(__name__)
+
+ pytest.importorskip("docker")
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++
+ pytestmark = [
+ pytest.mark.slow_test,
+ pytest.mark.skip_if_binaries_missing(
+ "docker", "dockerd", reason="Docker not installed"
+ ),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"),
+ ]
+
+
+diff --git a/tests/pytests/functional/states/rabbitmq/test_vhost.py b/tests/pytests/functional/states/rabbitmq/test_vhost.py
+index a648d41854f..d6ac6901a25 100644
+--- a/tests/pytests/functional/states/rabbitmq/test_vhost.py
++++ b/tests/pytests/functional/states/rabbitmq/test_vhost.py
+@@ -3,6 +3,7 @@ Integration tests for the rabbitmq_user states
+ """
+
+ import logging
++import os
+
+ import pytest
+
+@@ -13,11 +14,14 @@ log = logging.getLogger(__name__)
+
+ pytest.importorskip("docker")
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++
+ pytestmark = [
+ pytest.mark.slow_test,
+ pytest.mark.skip_if_binaries_missing(
+ "docker", "dockerd", reason="Docker not installed"
+ ),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"),
+ ]
+
+
+diff --git a/tests/pytests/functional/states/test_docker_network.py b/tests/pytests/functional/states/test_docker_network.py
+index 16a78b13a4a..0da01ed8bac 100644
+--- a/tests/pytests/functional/states/test_docker_network.py
++++ b/tests/pytests/functional/states/test_docker_network.py
+@@ -1,5 +1,6 @@
+ import functools
+ import logging
++import os
+ import random
+
+ import pytest
+@@ -15,9 +16,13 @@ pytest.importorskip("docker")
+
+ log = logging.getLogger(__name__)
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++
++
+ pytestmark = [
+ pytest.mark.slow_test,
+ pytest.mark.skip_if_binaries_missing("docker", "dockerd", check_all=False),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"),
+ ]
+
+
+diff --git a/tests/pytests/functional/states/test_pkg.py b/tests/pytests/functional/states/test_pkg.py
+index 0e82dc608ba..12318c996d1 100644
+--- a/tests/pytests/functional/states/test_pkg.py
++++ b/tests/pytests/functional/states/test_pkg.py
+@@ -64,7 +64,7 @@ def PKG_CAP_TARGETS(grains):
+ _PKG_CAP_TARGETS = []
+ if grains["os_family"] == "Suse":
+ if grains["os"] == "SUSE":
+- _PKG_CAP_TARGETS = [("perl(ZNC)", "znc-perl")]
++ _PKG_CAP_TARGETS = [("perl(Error)", "perl-Error")]
+ if not _PKG_CAP_TARGETS:
+ pytest.skip("Capability not provided")
+ return _PKG_CAP_TARGETS
+@@ -856,8 +856,8 @@ def test_pkg_cap_003_installed_multipkg_with_version(
+ This is a destructive test as it installs and then removes two packages
+ """
+ target, realpkg = PKG_CAP_TARGETS[0]
+- version = latest_version(target)
+- realver = latest_version(realpkg)
++ version = modules.pkg.version(target)
++ realver = modules.pkg.version(realpkg)
+
+ # If this condition is False, we need to find new targets.
+ # This needs to be able to test successful installation of packages.
+diff --git a/tests/pytests/integration/cli/test_syndic_eauth.py b/tests/pytests/integration/cli/test_syndic_eauth.py
+index 57e9c0a467a..218022b9e3c 100644
+--- a/tests/pytests/integration/cli/test_syndic_eauth.py
++++ b/tests/pytests/integration/cli/test_syndic_eauth.py
+@@ -1,4 +1,5 @@
+ import json
++import os
+ import pathlib
+ import tempfile
+ import time
+@@ -7,9 +8,11 @@ import pytest
+
+ docker = pytest.importorskip("docker")
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
+
+ pytestmark = [
+ pytest.mark.core_test,
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"),
+ ]
+
+
+diff --git a/tests/pytests/integration/daemons/test_memory_leak.py b/tests/pytests/integration/daemons/test_memory_leak.py
+index 1b782760418..8157091c44e 100644
+--- a/tests/pytests/integration/daemons/test_memory_leak.py
++++ b/tests/pytests/integration/daemons/test_memory_leak.py
+@@ -1,3 +1,4 @@
++import os
+ import time
+ from multiprocessing import Manager, Process
+
+@@ -8,6 +9,8 @@ pytestmark = [
+ pytest.mark.slow_test,
+ ]
+
++GITHUB_ACTIONS = bool(os.getenv("GITHUB_ACTIONS", False))
++
+
+ @pytest.fixture
+ def testfile_path(tmp_path):
+@@ -45,6 +48,7 @@ def file_add_delete_sls(testfile_path, base_env_state_tree_root_dir):
+
+
+ @pytest.mark.skip_on_darwin(reason="MacOS is a spawning platform, won't work")
++@pytest.mark.skipif(GITHUB_ACTIONS, reason="Test is failing in GitHub Actions")
+ @pytest.mark.flaky(max_runs=4)
+ def test_memory_leak(salt_cli, salt_minion, file_add_delete_sls):
+ max_usg = None
+diff --git a/tests/pytests/integration/modules/test_cmdmod.py b/tests/pytests/integration/modules/test_cmdmod.py
+index 4e8ce5824ee..d9c326c3f0a 100644
+--- a/tests/pytests/integration/modules/test_cmdmod.py
++++ b/tests/pytests/integration/modules/test_cmdmod.py
+@@ -1,5 +1,11 @@
++import logging
++
+ import pytest
+
++import salt.utils.user
++
++log = logging.getLogger(__name__)
++
+
+ @pytest.fixture(scope="module")
+ def non_root_account():
+@@ -7,6 +13,14 @@ def non_root_account():
+ yield account
+
+
++@pytest.fixture
++def running_username():
++ """
++ Return the username that is running the code.
++ """
++ return salt.utils.user.get_user()
++
++
+ @pytest.mark.skip_if_not_root
+ def test_exec_code_all(salt_call_cli, non_root_account):
+ ret = salt_call_cli.run(
+@@ -22,3 +36,82 @@ def test_long_stdout(salt_cli, salt_minion):
+ )
+ assert ret.returncode == 0
+ assert len(ret.data.strip()) == len(echo_str)
++
++
++@pytest.mark.skip_if_not_root
++@pytest.mark.skip_on_windows(reason="Skip on Windows, uses unix commands")
++def test_avoid_injecting_shell_code_as_root(
++ salt_call_cli, non_root_account, running_username
++):
++ """
++ cmd.run should execute the whole command as the "runas" user, not
++ running substitutions as root.
++ """
++ cmd = "echo $(id -u)"
++
++ ret = salt_call_cli.run("cmd.run_stdout", cmd)
++ root_id = ret.json
++ ret = salt_call_cli.run("cmd.run_stdout", cmd, runas=running_username)
++ runas_root_id = ret.json
++
++ ret = salt_call_cli.run("cmd.run_stdout", cmd, runas=non_root_account.username)
++ user_id = ret.json
++
++ assert user_id != root_id
++ assert user_id != runas_root_id
++ assert root_id == runas_root_id
++
++
++@pytest.mark.slow_test
++def test_blacklist_glob(salt_call_cli):
++ """
++ cmd_blacklist_glob
++ """
++ cmd = "bad_command --foo"
++ ret = salt_call_cli.run(
++ "cmd.run",
++ cmd,
++ )
++
++ assert (
++ ret.stderr.rstrip()
++ == "Error running 'cmd.run': The shell command \"bad_command --foo\" is not permitted"
++ )
++
++
++@pytest.mark.slow_test
++def test_hide_output(salt_call_cli):
++ """
++ Test the hide_output argument
++ """
++ ls_command = (
++ ["ls", "/"] if not salt.utils.platform.is_windows() else ["dir", "c:\\"]
++ )
++
++ error_command = ["thiscommanddoesnotexist"]
++
++ # cmd.run
++ ret = salt_call_cli.run("cmd.run", ls_command, hide_output=True)
++ assert ret.data == ""
++
++ # cmd.shell
++ ret = salt_call_cli.run("cmd.shell", ls_command, hide_output=True)
++ assert ret.data == ""
++
++ # cmd.run_stdout
++ ret = salt_call_cli.run("cmd.run_stdout", ls_command, hide_output=True)
++ assert ret.data == ""
++
++ # cmd.run_stderr
++ ret = salt_call_cli.run("cmd.shell", error_command, hide_output=True)
++ assert ret.data == ""
++
++ # cmd.run_all (command should have produced stdout)
++ ret = salt_call_cli.run("cmd.run_all", ls_command, hide_output=True)
++ assert ret.data["stdout"] == ""
++ assert ret.data["stderr"] == ""
++
++ # cmd.run_all (command should have produced stderr)
++ ret = salt_call_cli.run("cmd.run_all", error_command, hide_output=True)
++ assert ret.data["stdout"] == ""
++ assert ret.data["stderr"] == ""
+diff --git a/tests/pytests/integration/modules/test_virt.py b/tests/pytests/integration/modules/test_virt.py
+index 57ec239c4e9..1b7f30154a7 100644
+--- a/tests/pytests/integration/modules/test_virt.py
++++ b/tests/pytests/integration/modules/test_virt.py
+@@ -2,6 +2,7 @@
+ Validate the virt module
+ """
+ import logging
++import os
+ from numbers import Number
+ from xml.etree import ElementTree
+
+@@ -14,9 +15,12 @@ docker = pytest.importorskip("docker")
+
+ log = logging.getLogger(__name__)
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++
+ pytestmark = [
+ pytest.mark.slow_test,
+ pytest.mark.skip_if_binaries_missing("docker"),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"),
+ ]
+
+
+diff --git a/tests/pytests/integration/ssh/test_log.py b/tests/pytests/integration/ssh/test_log.py
+index e87c4a8581f..683feb8bd91 100644
+--- a/tests/pytests/integration/ssh/test_log.py
++++ b/tests/pytests/integration/ssh/test_log.py
+@@ -2,6 +2,7 @@
+ Integration tests for salt-ssh logging
+ """
+ import logging
++import os
+ import time
+
+ import pytest
+@@ -11,12 +12,14 @@ from tests.support.helpers import Keys
+
+ pytest.importorskip("docker")
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
+
+ log = logging.getLogger(__name__)
+
+ pytestmark = [
+ pytest.mark.slow_test,
+ pytest.mark.skip_if_binaries_missing("dockerd"),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"),
+ ]
+
+
+diff --git a/tests/pytests/integration/ssh/test_master.py b/tests/pytests/integration/ssh/test_master.py
+index 31e318870cb..0c2f482cf9f 100644
+--- a/tests/pytests/integration/ssh/test_master.py
++++ b/tests/pytests/integration/ssh/test_master.py
+@@ -2,6 +2,8 @@
+ Simple Smoke Tests for Connected SSH minions
+ """
+
++import os
++
+ import pytest
+ from saltfactories.utils.functional import StateResult
+
+@@ -10,7 +12,10 @@ pytestmark = [
+ pytest.mark.skip_on_windows(reason="salt-ssh not available on Windows"),
+ ]
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++
+
++@pytest.mark.skipif(INSIDE_CONTAINER, reason="No systemd in container.")
+ @pytest.mark.skip_if_not_root
+ def test_service(salt_ssh_cli, grains):
+ service = "cron"
+diff --git a/tests/pytests/integration/ssh/test_py_versions.py b/tests/pytests/integration/ssh/test_py_versions.py
+index 52ab819e808..71d4cfaa94e 100644
+--- a/tests/pytests/integration/ssh/test_py_versions.py
++++ b/tests/pytests/integration/ssh/test_py_versions.py
+@@ -2,6 +2,7 @@
+ Integration tests for salt-ssh py_versions
+ """
+ import logging
++import os
+ import socket
+ import time
+
+@@ -12,12 +13,14 @@ from tests.support.helpers import Keys
+
+ pytest.importorskip("docker")
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
+
+ log = logging.getLogger(__name__)
+
+ pytestmark = [
+ pytest.mark.slow_test,
+ pytest.mark.skip_if_binaries_missing("dockerd"),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"),
+ ]
+
+
+diff --git a/tests/pytests/integration/ssh/test_ssh_setup.py b/tests/pytests/integration/ssh/test_ssh_setup.py
+index eddf31caccd..79b55ad90a5 100644
+--- a/tests/pytests/integration/ssh/test_ssh_setup.py
++++ b/tests/pytests/integration/ssh/test_ssh_setup.py
+@@ -17,12 +17,14 @@ from tests.support.helpers import Keys
+
+ pytest.importorskip("docker")
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
+
+ log = logging.getLogger(__name__)
+
+ pytestmark = [
+ pytest.mark.slow_test,
+ pytest.mark.skip_if_binaries_missing("dockerd"),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"),
+ ]
+
+
+diff --git a/tests/pytests/scenarios/compat/test_with_versions.py b/tests/pytests/scenarios/compat/test_with_versions.py
+index 75a2b87f24c..498dd6a60de 100644
+--- a/tests/pytests/scenarios/compat/test_with_versions.py
++++ b/tests/pytests/scenarios/compat/test_with_versions.py
+@@ -5,6 +5,7 @@
+ Test current salt master with older salt minions
+ """
+ import logging
++import os
+ import pathlib
+
+ import pytest
+@@ -18,6 +19,8 @@ docker = pytest.importorskip("docker")
+
+ log = logging.getLogger(__name__)
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++
+
+ pytestmark = [
+ pytest.mark.slow_test,
+@@ -25,6 +28,7 @@ pytestmark = [
+ pytest.mark.skipif(
+ salt.utils.platform.is_photonos() is True, reason="Skip on PhotonOS"
+ ),
++ pytest.mark.skipif(INSIDE_CONTAINER, reason="Cannot run in a container"),
+ ]
+
+
+diff --git a/tests/pytests/scenarios/failover/multimaster/test_failover_master.py b/tests/pytests/scenarios/failover/multimaster/test_failover_master.py
+index 6efecfb8334..9f6251a4d6f 100644
+--- a/tests/pytests/scenarios/failover/multimaster/test_failover_master.py
++++ b/tests/pytests/scenarios/failover/multimaster/test_failover_master.py
+@@ -12,7 +12,10 @@ pytestmark = [
+
+ log = logging.getLogger(__name__)
+
++GITHUB_ACTIONS = bool(os.getenv("GITHUB_ACTIONS", False))
+
++
++@pytest.mark.skipif(GITHUB_ACTIONS, reason="Test is failing in GitHub Actions")
+ def test_pki(salt_mm_failover_master_1, salt_mm_failover_master_2, caplog):
+ """
+ Verify https://docs.saltproject.io/en/latest/topics/tutorials/multimaster_pki.html
+diff --git a/tests/pytests/scenarios/setup/test_install.py b/tests/pytests/scenarios/setup/test_install.py
+index 48f1d5889f6..7664fda804e 100644
+--- a/tests/pytests/scenarios/setup/test_install.py
++++ b/tests/pytests/scenarios/setup/test_install.py
+@@ -3,6 +3,7 @@ Tests for building and installing salt
+ """
+ import json
+ import logging
++import os
+ import pathlib
+ import re
+ import sys
+@@ -16,11 +17,16 @@ from salt.modules.virtualenv_mod import KNOWN_BINARY_NAMES
+
+ log = logging.getLogger(__name__)
+
++INSIDE_CONTAINER = os.getenv("HOSTNAME", "") == "salt-test-container"
++
+ pytestmark = [
+ pytest.mark.core_test,
+ pytest.mark.windows_whitelisted,
+ pytest.mark.skip_initial_onedir_failure,
+ pytest.mark.skip_if_binaries_missing(*KNOWN_BINARY_NAMES, check_all=False),
++ pytest.mark.skipif(
++ INSIDE_CONTAINER, reason="No gcc and python3-devel in container."
++ ),
+ ]
+
+
+diff --git a/tests/pytests/unit/modules/test_aptpkg.py b/tests/pytests/unit/modules/test_aptpkg.py
+index eb72447c3aa..6f0b905ef73 100644
+--- a/tests/pytests/unit/modules/test_aptpkg.py
++++ b/tests/pytests/unit/modules/test_aptpkg.py
+@@ -1360,17 +1360,17 @@ def test_call_apt_dpkg_lock():
+ ]
+
+ cmd_mock = MagicMock(side_effect=cmd_side_effect)
+- cmd_call = (
++ cmd_call = [
+ call(
+ ["dpkg", "-l", "python"],
+- env={},
+- ignore_retcode=False,
+ output_loglevel="quiet",
+ python_shell=True,
++ env={},
++ ignore_retcode=False,
+ username="Darth Vader",
+ ),
+- )
+- expected_calls = [cmd_call * 5]
++ ]
++ expected_calls = cmd_call * 5
+
+ with patch.dict(
+ aptpkg.__salt__,
+@@ -1390,7 +1390,7 @@ def test_call_apt_dpkg_lock():
+
+ # We should attempt to call the cmd 5 times
+ assert cmd_mock.call_count == 5
+- cmd_mock.has_calls(expected_calls)
++ cmd_mock.assert_has_calls(expected_calls)
+
+
+ def test_services_need_restart_checkrestart_missing():
+diff --git a/tests/pytests/unit/modules/test_linux_sysctl.py b/tests/pytests/unit/modules/test_linux_sysctl.py
+index 0bdd24039d7..6b0875bc460 100644
+--- a/tests/pytests/unit/modules/test_linux_sysctl.py
++++ b/tests/pytests/unit/modules/test_linux_sysctl.py
+@@ -215,7 +215,7 @@ def test_persist_no_conf_failure():
+ ):
+ with pytest.raises(CommandExecutionError):
+ linux_sysctl.persist("net.ipv4.ip_forward", 42, config=None)
+- fopen_mock.called_once()
++ fopen_mock.assert_called_once()
+
+
+ def test_persist_no_conf_success():
+@@ -353,7 +353,7 @@ def test_persist_value_with_spaces_already_set(tmp_path):
+ """
+ config = str(tmp_path / "existing_sysctl_with_spaces.conf")
+ value = "|/usr/share/kdump-tools/dump-core %p %s %t %e"
+- config_file_content = "kernel.core_pattern = {}\n".format(value)
++ config_file_content = f"kernel.core_pattern = {value}\n"
+ with fopen(config, "w", encoding="utf-8") as config_file:
+ config_file.write(config_file_content)
+ mock_run = MagicMock(return_value=value)
+@@ -383,7 +383,7 @@ def test_persist_value_with_spaces_already_configured(tmp_path):
+ """
+ config = str(tmp_path / "existing_sysctl_with_spaces.conf")
+ value = "|/usr/share/kdump-tools/dump-core %p %s %t %e"
+- config_file_content = "kernel.core_pattern = {}\n".format(value)
++ config_file_content = f"kernel.core_pattern = {value}\n"
+ with fopen(config, "w", encoding="utf-8") as config_file:
+ config_file.write(config_file_content)
+ mock_run = MagicMock(return_value="")
+@@ -451,7 +451,7 @@ def test_persist_value_with_spaces_update_config(tmp_path):
+ assert os.path.isfile(config)
+ with fopen(config, encoding="utf-8") as config_file:
+ written = config_file.read()
+- assert written == "kernel.core_pattern = {}\n".format(value)
++ assert written == f"kernel.core_pattern = {value}\n"
+
+
+ def test_persist_value_with_spaces_new_file(tmp_path):
+diff --git a/tests/pytests/unit/modules/test_win_ip.py b/tests/pytests/unit/modules/test_win_ip.py
+index 38eb6b1ac5f..94a3fe7ca93 100644
+--- a/tests/pytests/unit/modules/test_win_ip.py
++++ b/tests/pytests/unit/modules/test_win_ip.py
+@@ -151,7 +151,7 @@ def test_enable():
+ ):
+ assert win_ip.enable("Ethernet")
+
+- mock_cmd.called_once_with(
++ mock_cmd.assert_called_once_with(
+ [
+ "netsh",
+ "interface",
+@@ -180,7 +180,7 @@ def test_disable():
+ ):
+ assert win_ip.disable("Ethernet")
+
+- mock_cmd.called_once_with(
++ mock_cmd.assert_called_once_with(
+ [
+ "netsh",
+ "interface",
+diff --git a/tests/pytests/unit/test_master.py b/tests/pytests/unit/test_master.py
+index d338307d1f8..679229066d4 100644
+--- a/tests/pytests/unit/test_master.py
++++ b/tests/pytests/unit/test_master.py
+@@ -61,7 +61,7 @@ def test_fileserver_duration():
+ end = time.time()
+ # Interval is equal to timeout so the _do_update method will be called
+ # one time.
+- update.called_once()
++ update.assert_called_once()
+ # Timeout is 1 second
+ duration = end - start
+ if duration > 2 and salt.utils.platform.spawning_platform():
+diff --git a/tests/pytests/unit/test_minion.py b/tests/pytests/unit/test_minion.py
+index 740743194e4..a9e91742a2d 100644
+--- a/tests/pytests/unit/test_minion.py
++++ b/tests/pytests/unit/test_minion.py
+@@ -655,7 +655,9 @@ def test_gen_modules_executors(minion_opts):
+ with patch("salt.pillar.get_pillar", return_value=MockPillarCompiler()):
+ with patch("salt.loader.executors") as execmock:
+ minion.gen_modules()
+- assert execmock.called_with(minion.opts, minion.functions)
++ execmock.assert_called_with(
++ minion.opts, functions=minion.functions, proxy=minion.proxy, context={}
++ )
+ finally:
+ minion.destroy()
+
+diff --git a/tests/pytests/unit/utils/event/test_event.py b/tests/pytests/unit/utils/event/test_event.py
+index e289e72dad0..f4b6c159996 100644
+--- a/tests/pytests/unit/utils/event/test_event.py
++++ b/tests/pytests/unit/utils/event/test_event.py
+@@ -38,7 +38,7 @@ def sock_dir(tmp_path):
+ def _assert_got_event(evt, data, msg=None, expected_failure=False):
+ assert evt is not None, msg
+ for key in data:
+- assert key in evt, "{}: Key {} missing".format(msg, key)
++ assert key in evt, f"{msg}: Key {key} missing"
+ assertMsg = "{0}: Key {1} value mismatch, {2} != {3}"
+ assertMsg = assertMsg.format(msg, key, data[key], evt[key])
+ if not expected_failure:
+@@ -59,8 +59,8 @@ def test_minion_event(sock_dir):
+ :10
+ ]
+ with salt.utils.event.MinionEvent(opts, listen=False) as me:
+- assert me.puburi == str(sock_dir / "minion_event_{}_pub.ipc".format(id_hash))
+- assert me.pulluri == str(sock_dir / "minion_event_{}_pull.ipc".format(id_hash))
++ assert me.puburi == str(sock_dir / f"minion_event_{id_hash}_pub.ipc")
++ assert me.pulluri == str(sock_dir / f"minion_event_{id_hash}_pull.ipc")
+
+
+ def test_minion_event_tcp_ipc_mode():
+@@ -73,8 +73,8 @@ def test_minion_event_tcp_ipc_mode():
+ def test_minion_event_no_id(sock_dir):
+ with salt.utils.event.MinionEvent(dict(sock_dir=str(sock_dir)), listen=False) as me:
+ id_hash = hashlib.sha256(salt.utils.stringutils.to_bytes("")).hexdigest()[:10]
+- assert me.puburi == str(sock_dir / "minion_event_{}_pub.ipc".format(id_hash))
+- assert me.pulluri == str(sock_dir / "minion_event_{}_pull.ipc".format(id_hash))
++ assert me.puburi == str(sock_dir / f"minion_event_{id_hash}_pub.ipc")
++ assert me.pulluri == str(sock_dir / f"minion_event_{id_hash}_pull.ipc")
+
+
+ @pytest.mark.slow_test
+@@ -256,9 +256,9 @@ def test_event_many(sock_dir):
+ with eventpublisher_process(str(sock_dir)):
+ with salt.utils.event.MasterEvent(str(sock_dir), listen=True) as me:
+ for i in range(500):
+- me.fire_event({"data": "{}".format(i)}, "testevents")
++ me.fire_event({"data": f"{i}"}, "testevents")
+ evt = me.get_event(tag="testevents")
+- _assert_got_event(evt, {"data": "{}".format(i)}, "Event {}".format(i))
++ _assert_got_event(evt, {"data": f"{i}"}, f"Event {i}")
+
+
+ @pytest.mark.slow_test
+@@ -268,10 +268,10 @@ def test_event_many_backlog(sock_dir):
+ with salt.utils.event.MasterEvent(str(sock_dir), listen=True) as me:
+ # Must not exceed zmq HWM
+ for i in range(500):
+- me.fire_event({"data": "{}".format(i)}, "testevents")
++ me.fire_event({"data": f"{i}"}, "testevents")
+ for i in range(500):
+ evt = me.get_event(tag="testevents")
+- _assert_got_event(evt, {"data": "{}".format(i)}, "Event {}".format(i))
++ _assert_got_event(evt, {"data": f"{i}"}, f"Event {i}")
+
+
+ # Test the fire_master function. As it wraps the underlying fire_event,
+@@ -300,7 +300,7 @@ def test_connect_pull_should_debug_log_on_StreamClosedError():
+ event = SaltEvent(node=None)
+ with patch.object(event, "pusher") as mock_pusher:
+ with patch.object(
+- salt.utils.event.log, "debug", auto_spec=True
++ salt.utils.event.log, "debug", autospec=True
+ ) as mock_log_debug:
+ mock_pusher.connect.side_effect = (
+ salt.ext.tornado.iostream.StreamClosedError
+@@ -317,10 +317,10 @@ def test_connect_pull_should_error_log_on_other_errors(error):
+ event = SaltEvent(node=None)
+ with patch.object(event, "pusher") as mock_pusher:
+ with patch.object(
+- salt.utils.event.log, "debug", auto_spec=True
++ salt.utils.event.log, "debug", autospec=True
+ ) as mock_log_debug:
+ with patch.object(
+- salt.utils.event.log, "error", auto_spec=True
++ salt.utils.event.log, "error", autospec=True
+ ) as mock_log_error:
+ mock_pusher.connect.side_effect = error
+ event.connect_pull()
+diff --git a/tests/unit/modules/test_boto_apigateway.py b/tests/unit/modules/test_boto_apigateway.py
+index 5f3d2a49822..ebf50679bd8 100644
+--- a/tests/unit/modules/test_boto_apigateway.py
++++ b/tests/unit/modules/test_boto_apigateway.py
+@@ -15,6 +15,7 @@ from tests.support.unit import TestCase
+
+ # pylint: disable=import-error,no-name-in-module
+ try:
++ import boto
+ import boto3
+ import botocore
+ from botocore.exceptions import ClientError
+diff --git a/tests/unit/modules/test_boto_cognitoidentity.py b/tests/unit/modules/test_boto_cognitoidentity.py
+index 1e213a169ac..974832f9ff9 100644
+--- a/tests/unit/modules/test_boto_cognitoidentity.py
++++ b/tests/unit/modules/test_boto_cognitoidentity.py
+@@ -14,6 +14,7 @@ from tests.support.unit import TestCase
+
+ # pylint: disable=import-error,no-name-in-module
+ try:
++ import boto
+ import boto3
+ from botocore.exceptions import ClientError
+
+diff --git a/tests/unit/modules/test_boto_elasticsearch_domain.py b/tests/unit/modules/test_boto_elasticsearch_domain.py
+index 5c5845aa25b..0578a81e8ef 100644
+--- a/tests/unit/modules/test_boto_elasticsearch_domain.py
++++ b/tests/unit/modules/test_boto_elasticsearch_domain.py
+@@ -14,6 +14,7 @@ from tests.support.unit import TestCase
+
+ # pylint: disable=import-error,no-name-in-module
+ try:
++ import boto
+ import boto3
+ from botocore.exceptions import ClientError
+
+diff --git a/tests/unit/modules/test_boto_lambda.py b/tests/unit/modules/test_boto_lambda.py
+index d32dc9345b6..ecaa532f1ff 100644
+--- a/tests/unit/modules/test_boto_lambda.py
++++ b/tests/unit/modules/test_boto_lambda.py
+@@ -18,6 +18,7 @@ from tests.support.unit import TestCase
+
+ # pylint: disable=import-error,no-name-in-module
+ try:
++ import boto
+ import boto3
+ from botocore import __version__ as found_botocore_version
+ from botocore.exceptions import ClientError
+diff --git a/tests/unit/modules/test_network.py b/tests/unit/modules/test_network.py
+index 34b06250fc6..9eef9a02f58 100644
+--- a/tests/unit/modules/test_network.py
++++ b/tests/unit/modules/test_network.py
+@@ -153,9 +153,11 @@ class NetworkTestCase(TestCase, LoaderModuleMockMixin):
+ """
+ Test for Performs a DNS lookup with dig
+ """
+- with patch("salt.utils.path.which", MagicMock(return_value="dig")), patch.dict(
++ with patch.dict(
+ network.__utils__, {"network.sanitize_host": MagicMock(return_value="A")}
+- ), patch.dict(network.__salt__, {"cmd.run": MagicMock(return_value="A")}):
++ ), patch("salt.utils.path.which", MagicMock(return_value="dig")), patch.dict(
++ network.__salt__, {"cmd.run": MagicMock(return_value="A")}
++ ):
+ self.assertEqual(network.dig("host"), "A")
+
+ def test_arp(self):
+diff --git a/tests/unit/modules/test_nilrt_ip.py b/tests/unit/modules/test_nilrt_ip.py
+index 1261473edb4..50dc13b20b8 100644
+--- a/tests/unit/modules/test_nilrt_ip.py
++++ b/tests/unit/modules/test_nilrt_ip.py
+@@ -28,7 +28,7 @@ class NilrtIPTestCase(TestCase, LoaderModuleMockMixin):
+ "salt.modules.nilrt_ip._change_dhcp_config", return_value=True
+ ) as change_dhcp_config_mock:
+ assert nilrt_ip._change_state("test_interface", "down")
+- assert change_dhcp_config_mock.called_with("test_interface", False)
++ change_dhcp_config_mock.assert_called_with("test_interface", False)
+
+ def test_change_state_up_state(self):
+ """
+@@ -42,7 +42,7 @@ class NilrtIPTestCase(TestCase, LoaderModuleMockMixin):
+ "salt.modules.nilrt_ip._change_dhcp_config", return_value=True
+ ) as change_dhcp_config_mock:
+ assert nilrt_ip._change_state("test_interface", "up")
+- assert change_dhcp_config_mock.called_with("test_interface")
++ change_dhcp_config_mock.assert_called_with("test_interface")
+
+ def test_set_static_all_with_dns(self):
+ """
+diff --git a/tests/unit/modules/test_zcbuildout.py b/tests/unit/modules/test_zcbuildout.py
+index f793e3fc3f8..5a5996e110e 100644
+--- a/tests/unit/modules/test_zcbuildout.py
++++ b/tests/unit/modules/test_zcbuildout.py
+@@ -451,6 +451,7 @@ class BuildoutOnlineTestCase(Base):
+ )
+
+ @pytest.mark.slow_test
++ @pytest.mark.skip(reason="TODO this test should probably be fixed")
+ def test_run_buildout(self):
+ if salt.modules.virtualenv_mod.virtualenv_ver(self.ppy_st) >= (20, 0, 0):
+ self.skipTest(
+@@ -467,6 +468,7 @@ class BuildoutOnlineTestCase(Base):
+ self.assertTrue("Installing b" in out)
+
+ @pytest.mark.slow_test
++ @pytest.mark.skip(reason="TODO this test should probably be fixed")
+ def test_buildout(self):
+ if salt.modules.virtualenv_mod.virtualenv_ver(self.ppy_st) >= (20, 0, 0):
+ self.skipTest(
+diff --git a/tests/unit/netapi/rest_tornado/test_saltnado.py b/tests/unit/netapi/rest_tornado/test_saltnado.py
+index 7b63a65d4f3..c4758e700ab 100644
+--- a/tests/unit/netapi/rest_tornado/test_saltnado.py
++++ b/tests/unit/netapi/rest_tornado/test_saltnado.py
+@@ -647,7 +647,6 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase):
+ with patch.object(
+ self.handler.application.event_listener,
+ "get_event",
+- autospec=True,
+ side_effect=fancy_get_event,
+ ), patch.dict(
+ self.handler.application.opts,
+@@ -698,7 +697,6 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase):
+ with patch.object(
+ self.handler.application.event_listener,
+ "get_event",
+- autospec=True,
+ side_effect=fancy_get_event,
+ ), patch.object(
+ self.handler,
+@@ -729,8 +727,8 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase):
+ {
+ "tag": "fnord",
+ "data": {
+- "return": "return from fnord {}".format(i),
+- "id": "fnord {}".format(i),
++ "return": f"return from fnord {i}",
++ "id": f"fnord {i}",
+ },
+ }
+ )
+@@ -760,7 +758,6 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase):
+ with patch.object(
+ self.handler.application.event_listener,
+ "get_event",
+- autospec=True,
+ side_effect=fancy_get_event,
+ ), patch.object(
+ self.handler,
+@@ -794,8 +791,8 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase):
+ {
+ "tag": "fnord",
+ "data": {
+- "return": "return from fnord {}".format(i),
+- "id": "fnord {}".format(i),
++ "return": f"return from fnord {i}",
++ "id": f"fnord {i}",
+ },
+ }
+ )
+@@ -820,7 +817,6 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase):
+ with patch.object(
+ self.handler.application.event_listener,
+ "get_event",
+- autospec=True,
+ side_effect=fancy_get_event,
+ ), patch.dict(
+ self.handler.application.opts,
+@@ -843,12 +839,12 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase):
+ completed_events = [salt.ext.tornado.gen.Future() for _ in range(10)]
+ events_by_id = {}
+ for i, event in enumerate(completed_events):
+- id_ = "fnord {}".format(i)
++ id_ = f"fnord {i}"
+ events_by_id[id_] = event
+ event.set_result(
+ {
+ "tag": "fnord",
+- "data": {"return": "return from {}".format(id_), "id": id_},
++ "data": {"return": f"return from {id_}", "id": id_},
+ }
+ )
+ expected_result = {
+@@ -878,7 +874,6 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase):
+ with patch.object(
+ self.handler.application.event_listener,
+ "get_event",
+- autospec=True,
+ side_effect=fancy_get_event,
+ ), patch.dict(
+ self.handler.application.opts,
+@@ -904,12 +899,12 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase):
+ events_by_id = {}
+ # Setup some real-enough looking return data
+ for i, event in enumerate(completed_events):
+- id_ = "fnord {}".format(i)
++ id_ = f"fnord {i}"
+ events_by_id[id_] = event
+ event.set_result(
+ {
+ "tag": "fnord",
+- "data": {"return": "return from {}".format(id_), "id": id_},
++ "data": {"return": f"return from {id_}", "id": id_},
+ }
+ )
+ # Hard coded instead of dynamic to avoid potentially writing a test
+@@ -971,7 +966,6 @@ class TestDisbatchLocal(salt.ext.tornado.testing.AsyncTestCase):
+ with patch.object(
+ self.handler.application.event_listener,
+ "get_event",
+- autospec=True,
+ side_effect=fancy_get_event,
+ ), patch.object(
+ self.handler,
+diff --git a/tests/unit/states/test_boto_apigateway.py b/tests/unit/states/test_boto_apigateway.py
+index 51c85d6058a..1edde8d303c 100644
+--- a/tests/unit/states/test_boto_apigateway.py
++++ b/tests/unit/states/test_boto_apigateway.py
+@@ -20,6 +20,7 @@ from tests.support.unit import TestCase
+ from tests.unit.modules.test_boto_apigateway import BotoApiGatewayTestCaseMixin
+
+ try:
++ import boto
+ import boto3
+ import botocore
+ from botocore.exceptions import ClientError
+diff --git a/tests/unit/states/test_boto_cognitoidentity.py b/tests/unit/states/test_boto_cognitoidentity.py
+index 4354df0546f..479477ac800 100644
+--- a/tests/unit/states/test_boto_cognitoidentity.py
++++ b/tests/unit/states/test_boto_cognitoidentity.py
+@@ -18,6 +18,7 @@ from tests.unit.modules.test_boto_cognitoidentity import (
+ )
+
+ try:
++ import boto
+ import boto3
+ from botocore.exceptions import ClientError
+
+diff --git a/tests/unit/states/test_zcbuildout.py b/tests/unit/states/test_zcbuildout.py
+index db6013076d1..0abaadeb4be 100644
+--- a/tests/unit/states/test_zcbuildout.py
++++ b/tests/unit/states/test_zcbuildout.py
+@@ -48,6 +48,7 @@ class BuildoutTestCase(Base):
+ self.assertFalse(ret["result"])
+
+ @pytest.mark.slow_test
++ @pytest.mark.skip(reason="TODO this test should probably be fixed")
+ def test_installed(self):
+ if salt.modules.virtualenv_mod.virtualenv_ver(self.ppy_st) >= (20, 0, 0):
+ self.skipTest(
+--
+2.43.0
+
+
diff --git a/salt.changes b/salt.changes
index 8ff75b9..8b24f82 100644
--- a/salt.changes
+++ b/salt.changes
@@ -1,3 +1,37 @@
+-------------------------------------------------------------------
+Mon Feb 26 10:43:37 UTC 2024 - Pablo Suárez Hernández
+
+- Fix problematic tests and allow smooth tests executions on containers
+
+- Added:
+ * fix-problematic-tests-and-allow-smooth-tests-executi.patch
+
+-------------------------------------------------------------------
+Wed Feb 21 12:21:03 UTC 2024 - Pablo Suárez Hernández
+
+- Discover Ansible playbook files as "*.yml" or "*.yaml" files (bsc#1211888)
+
+- Added:
+ * discover-both-.yml-and-.yaml-playbooks-bsc-1211888.patch
+
+-------------------------------------------------------------------
+Tue Feb 20 12:58:58 UTC 2024 - Pablo Suárez Hernández
+
+- Extend dependencies for python3-salt-testsuite and python3-salt packages
+- Improve Salt and testsuite packages multibuild
+
+-------------------------------------------------------------------
+Thu Feb 8 12:17:39 UTC 2024 - Yeray Gutiérrez Cedrés
+
+- Enable multibuilld and create test flavor
+- Additionally we require python-mock just for older Python versions.
+
+-------------------------------------------------------------------
+Mon Feb 5 09:55:33 UTC 2024 - Pablo Suárez Hernández
+
+- Remove python-boto dependency for the python3-salt-testsuite package for Tumbleweed
+- Rename salt-tests to python3-salt-testsuite
+
-------------------------------------------------------------------
Thu Feb 1 12:19:06 UTC 2024 - Pablo Suárez Hernández
diff --git a/salt.spec b/salt.spec
index eaef8ff..85170a5 100644
--- a/salt.spec
+++ b/salt.spec
@@ -16,6 +16,13 @@
#
%global debug_package %{nil}
+%global flavor @BUILD_FLAVOR@%{nil}
+%if "%{flavor}" == "testsuite"
+%define psuffix -test
+%else
+%define psuffix %{nil}
+%endif
+
%if 0%{?suse_version} > 1210 || 0%{?rhel} >= 7 || 0%{?fedora} >=28
%bcond_without systemd
%else
@@ -31,11 +38,10 @@
%bcond_with fish_completion
%bcond_with zsh_completion
%endif
-%bcond_with test
%bcond_without docs
%bcond_with builddocs
-Name: salt
+Name: salt%{psuffix}
Version: 3006.0
Release: 0
Summary: A parallel remote execution system
@@ -343,6 +349,10 @@ Patch97: fixed-keyerror-in-logs-when-running-a-state-that-fai.patch
Patch98: improve-pip-target-override-condition-with-venv_pip_.patch
# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/65819
Patch99: allow-kwargs-for-fileserver-roots-update-bsc-1218482.patch
+# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/66048
+Patch100: discover-both-.yml-and-.yaml-playbooks-bsc-1211888.patch
+# PATCH-FIX_UPSTREAM https://github.com/saltstack/salt/pull/66130
+Patch101: fix-problematic-tests-and-allow-smooth-tests-executi.patch
### IMPORTANT: The line below is used as a snippet marker. Do not touch it.
@@ -424,6 +434,8 @@ malleable. Salt accomplishes this via its ability to handle larger loads of
information, and not just dozens, but hundreds or even thousands of individual
servers, handle them quickly and through a simple and manageable interface.
+%if "%{flavor}" != "testsuite"
+
%package -n python3-salt
Summary: python3 library for salt
Group: System/Management
@@ -464,7 +476,7 @@ BuildRequires: python3-packaging
# requirements/zeromq.txt
%if %{with test}
BuildRequires: python3-boto >= 2.32.1
-BuildRequires: python3-mock
+BuildRequires: %{python3-mock if %python-base < 3.8}
BuildRequires: python3-moto >= 0.3.6
BuildRequires: python3-pip
BuildRequires: python3-salt-testing >= 2015.2.16
@@ -533,6 +545,12 @@ Recommends: python3-netaddr
Recommends: python3-pyinotify
%endif
+# Required by Salt modules
+Requires: iputils
+Requires: sudo
+Requires: file
+Requires: man
+
Provides: bundled(python3-tornado) = 4.5.3
%description -n python3-salt
@@ -701,13 +719,6 @@ Requires(pre): %fillup_prereq
Salt ssh is a master running without zmq.
it enables the management of minions over a ssh connection.
-%package tests
-Summary: Unit and integration tests for Salt
-Requires: %{name} = %{version}-%{release}
-
-%description tests
-Collections of unit and integration tests for Salt
-
%if %{with bash_completion}
%package bash-completion
Summary: Bash Completion for %{name}
@@ -774,6 +785,51 @@ For transactional systems, like MicroOS, Salt can operate
transparently if the executor "transactional-update" is registered in
list of active executors. This package add the configuration file.
+%endif
+
+%if "%{flavor}" == "testsuite"
+
+%package -n python3-salt-testsuite
+Summary: Unit and integration tests for Salt
+
+%if 0%{?rhel} == 8
+BuildRequires: platform-python
+%else
+BuildRequires: python3
+%endif
+BuildRequires: python3-devel
+BuildRequires: python3-setuptools
+
+Requires: salt = %{version}
+Requires: python3-CherryPy
+Requires: python3-Genshi
+Requires: python3-Mako
+%if !0%{?suse_version} > 1600 || 0%{?centos}
+Requires: python3-boto
+%endif
+Requires: python3-boto3
+Requires: python3-docker
+%if 0%{?suse_version} < 1600
+Requires: python3-mock
+%endif
+Requires: python3-pygit2
+Requires: python3-pytest >= 7.0.1
+Requires: python3-pytest-httpserver
+Requires: python3-pytest-salt-factories >= 1.0.0~rc21
+Requires: python3-pytest-subtests
+Requires: python3-testinfra
+Requires: python3-yamllint
+Requires: python3-pip
+Requires: docker
+Requires: openssh
+Requires: git
+
+Obsoletes: %{name}-tests
+
+%description -n python3-salt-testsuite
+Collection of unit, functional, and integration tests for %{name}.
+
+%endif
%prep
%setup -q -n salt-%{version}-suse
@@ -783,6 +839,8 @@ cp %{S:6} .
%autopatch -p1
%build
+%if "%{flavor}" != "testsuite"
+
# Putting /usr/bin at the front of $PATH is needed for RHEL/RES 7. Without this
# change, the RPM will require /bin/python, which is not provided by any package
# on RHEL/RES 7.
@@ -805,7 +863,11 @@ popd
cd doc && make html && rm _build/html/.buildinfo && rm _build/html/_images/proxy_minions.png && cd _build/html && chmod -R -x+X *
%endif
+%endif
+
%install
+%if "%{flavor}" != "testsuite"
+
mv _build.python3 build
python3 setup.py --salt-transport=both install --prefix=%{_prefix} --root=%{buildroot}
mv build _build.python3
@@ -853,11 +915,19 @@ install -Dd -m 0755 %{buildroot}%{_sysconfdir}/logrotate.d/
# Install salt-support profiles
install -Dpm 0644 salt/cli/support/profiles/* %{buildroot}%{python3_sitelib}/salt/cli/support/profiles
+%endif
+
+%if "%{flavor}" == "testsuite"
# Install Salt tests
-install -Dd -m 0750 %{buildroot}%{_datadir}/salt
-install -Dd -m 0750 %{buildroot}%{_datadir}/salt/tests
-cp -a tests/* %{buildroot}%{_datadir}/salt/tests/
-sed -i '1s=^#!/usr/bin/\(python\|env python\)[0-9.]*=#!/usr/bin/python3=' %{buildroot}%{_datadir}/salt/tests/runtests.py
+install -Dd %{buildroot}%{python3_sitelib}/salt-testsuite
+cp -a tests %{buildroot}%{python3_sitelib}/salt-testsuite/
+# Remove runtests.py which is not used as deprecated method of running the tests
+rm %{buildroot}%{python3_sitelib}/salt-testsuite/tests/runtests.py
+# Copy conf files to the testsuite as they are used by the tests
+cp -a conf %{buildroot}%{python3_sitelib}/salt-testsuite/
+%endif
+
+%if "%{flavor}" != "testsuite"
## Install Zypper plugins only on SUSE machines
%if 0%{?suse_version}
@@ -968,11 +1038,10 @@ install -Dpm 0640 conf/suse/standalone-formulas-configuration.conf %{buildroot}%
%fdupes %{buildroot}%{python3_sitelib}
%endif
-%check
-%if %{with test}
-python3 setup.py test --runtests-opts=-u
%endif
+%if "%{flavor}" != "testsuite"
+
%pre
S_HOME="/var/lib/salt"
S_PHOME="/srv/salt"
@@ -1434,7 +1503,10 @@ rm -f %{_localstatedir}/cache/salt/minion/thin/version
%files -n python3-salt
%defattr(-,root,root,-)
-%{python3_sitelib}/*
+%dir %{python3_sitelib}/salt
+%dir %{python3_sitelib}/salt-*.egg-info
+%{python3_sitelib}/salt/*
+%{python3_sitelib}/salt-*.egg-info/*
%exclude %{python3_sitelib}/salt/cloud/deploy/*.sh
%if %{with docs}
@@ -1443,11 +1515,6 @@ rm -f %{_localstatedir}/cache/salt/minion/thin/version
%doc doc/_build/html
%endif
-%files tests
-%dir %{_datadir}/salt/
-%dir %{_datadir}/salt/tests/
-%{_datadir}/salt/tests/*
-
%if %{with bash_completion}
%files bash-completion
%defattr(-,root,root)
@@ -1484,6 +1551,12 @@ rm -f %{_localstatedir}/cache/salt/minion/thin/version
%defattr(-,root,root)
%config(noreplace) %attr(0640, root, root) %{_sysconfdir}/salt/minion.d/transactional_update.conf
+%endif
+
+%if "%{flavor}" == "testsuite"
+%files -n python3-salt-testsuite
+%{python3_sitelib}/salt-testsuite
+%endif
%changelog