/
githubmirror
/
salt
Обзор
Документация
Войти
/
githubmirror
/
salt
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
tests/pytests/functional/states/file/test_patch.py
616 строк
22 KB
Daniel A. Wozniak
Apply black formatting to test files
10 июн 2026, 13:34
10 июн 2026, 13:34
250665f
Код
Авторство
О чём код?
import os import pathlib import re import textwrap import types import pytest import salt.utils.hashutils import salt.utils.platform import salt.utils.versions from salt.utils.versions import Version pytestmark = [ pytest.mark.windows_whitelisted, pytest.mark.skip_if_binaries_missing("patch"), ] class Patches: def __init__(self, webserver): self.webserver = webserver self.webserver_root = pathlib.Path(webserver.root) self.numbers_patch_name = "numbers.patch" self.numbers_patch_template_name = self.numbers_patch_name + ".jinja" self.numbers_patch_path = "patches/" + self.numbers_patch_name self.numbers_patch_template_path = "patches/" + self.numbers_patch_template_name self.numbers_patch = "salt://" + self.numbers_patch_path self.numbers_patch_template = "salt://" + self.numbers_patch_template_path self.numbers_patch_http = self.webserver.url(self.numbers_patch_path) self.numbers_patch_template_http = self.webserver.url( self.numbers_patch_template_path ) self.math_patch_name = "math.patch" self.math_patch_template_name = self.math_patch_name + ".jinja" self.math_patch_path = "patches/" + self.math_patch_name self.math_patch_template_path = "patches/" + self.math_patch_template_name self.math_patch = "salt://" + self.math_patch_path self.math_patch_template = "salt://" + self.math_patch_template_path self.math_patch_http = self.webserver.url(self.math_patch_path) self.math_patch_template_http = self.webserver.url( self.math_patch_template_path ) self.all_patch_name = "all.patch" self.all_patch_template_name = self.all_patch_name + ".jinja" self.all_patch_path = "patches/" + self.all_patch_name self.all_patch_template_path = "patches/" + self.all_patch_template_name self.all_patch = "salt://" + self.all_patch_path self.all_patch_template = "salt://" + self.all_patch_template_path self.all_patch_http = self.webserver.url(self.all_patch_path) self.all_patch_template_http = self.webserver.url(self.all_patch_template_path) patches_dir = self.webserver_root / "patches" patches_dir.mkdir() numbers_patch = patches_dir / self.numbers_patch_name numbers_patch.write_text( textwrap.dedent( """\ --- a/foo/numbers.txt 2018-04-09 18:43:58.014272504 -0500 +++ b/foo/numbers.txt 2018-04-09 18:44:46.487905044 -0500 @@ -1,7 +1,7 @@ -one -two three +two +one -1 -2 3 +2 +1 """ ) ) numbers_patch_template = patches_dir / self.numbers_patch_template_name numbers_patch_template.write_text( numbers_patch.read_text().replace("+two", "+{{ two }}") ) math_patch = patches_dir / self.math_patch_name math_patch.write_text( textwrap.dedent( """\ --- a/foo/bar/math.txt 2018-04-09 18:43:52.883205365 -0500 +++ b/foo/bar/math.txt 2018-04-09 18:44:58.525061654 -0500 @@ -1,3 +1,3 @@ -Five plus five is ten +5 + 5 = 10 -Four squared is sixteen +4² = 16 """ ) ) math_patch_template = patches_dir / self.math_patch_template_name math_patch_template.write_text( math_patch.read_text().replace("= 10", "= {{ ten }}") ) all_patch = patches_dir / self.all_patch_name all_patch.write_text(math_patch.read_text() + numbers_patch.read_text()) all_patch_template = patches_dir / self.all_patch_template_name all_patch_template.write_text( math_patch_template.read_text() + numbers_patch_template.read_text() ) self.numbers_patch_hash = salt.utils.hashutils.get_hash( str(patches_dir / self.numbers_patch_name) ) self.numbers_patch_template_hash = salt.utils.hashutils.get_hash( str(patches_dir / self.numbers_patch_template_name) ) self.math_patch_hash = salt.utils.hashutils.get_hash( str(patches_dir / self.math_patch_name) ) self.math_patch_template_hash = salt.utils.hashutils.get_hash( str(patches_dir / self.math_patch_template_name) ) self.all_patch_hash = salt.utils.hashutils.get_hash( str(patches_dir / self.all_patch_name) ) self.all_patch_template_hash = salt.utils.hashutils.get_hash( str(patches_dir / self.all_patch_template_name) ) @pytest.fixture(scope="module") def _check_min_patch_version(shell): min_patch_ver = Version("2.6") ret = shell.run("patch", "--version") assert ret.returncode == 0 version = ret.stdout.strip().splitlines()[0].split()[-1] if Version(version) < min_patch_ver: pytest.xfail( "Minimum version of patch not found, expecting {}, found {}".format( min_patch_ver, version ) ) @pytest.fixture(scope="module") def patches(webserver, _check_min_patch_version): yield Patches(webserver) @pytest.fixture def files(tmp_path): numbers_contents = """ one two three 1 2 3 """ math_contents = """ Five plus five is ten Four squared is sixteen """ numbers_file = pytest.helpers.temp_file( "numbers.txt", directory=tmp_path / "foo", contents=numbers_contents ) math_file = pytest.helpers.temp_file( "math.txt", directory=tmp_path / "foo" / "bar", contents=math_contents ) with numbers_file as numbers_file, math_file as math_file: yield types.SimpleNamespace( numbers=numbers_file, math=math_file, base_dir=tmp_path ) def test_patch_single_file(file, files, patches): """ Test file.patch using a patch applied to a single file """ ret = file.patch(name=str(files.numbers), source=patches.numbers_patch) assert ret.result is True assert ret.changes assert ret.comment == "Patch successfully applied" # Re-run the state, should succeed and there should be a message about # a partially-applied hunk. ret = file.patch(name=str(files.numbers), source=patches.numbers_patch) assert ret.result is True assert not ret.changes assert ret.comment == "Patch was already applied" @pytest.mark.skip_on_freebsd( reason="Previously skipped on FreeBSD. Needs investigation as to why it currently False" ) @pytest.mark.skip_on_darwin( reason="Failing and previously skipped because patch wasn't >= 2.6" ) def test_patch_directory(file, files, patches): """ Test file.patch using a patch applied to a directory, with changes spanning multiple files. """ ret = file.patch(name=str(files.base_dir), source=patches.all_patch, strip=1) assert ret.result is True assert ret.changes assert ret.comment == "Patch successfully applied" # Re-run the state, should succeed and there should be a message about # a partially-applied hunk. ret = file.patch(name=str(files.base_dir), source=patches.all_patch, strip=1) assert ret.result is True assert not ret.changes assert ret.comment == "Patch was already applied" @pytest.mark.skip_on_freebsd( reason="Previously skipped on FreeBSD. Needs investigation as to why it currently False" ) @pytest.mark.skip_on_darwin( reason="Failing and previously skipped because patch wasn't >= 2.6" ) def test_patch_strip_parsing(file, files, patches, subtests): """ Test that we successfuly parse -p/--strip when included in the options """ # Run the state using -p1 with subtests.test("options='-p1'"): ret = file.patch( name=str(files.base_dir), source=patches.all_patch, options="-p1" ) assert ret.result is True assert ret.changes assert ret.comment == "Patch successfully applied" # Re-run the state using --strip=1 with subtests.test("options='--strip=1'"): ret = file.patch( name=str(files.base_dir), source=patches.all_patch, options="--strip=1" ) assert ret.result is True assert not ret.changes assert ret.comment == "Patch was already applied" # Re-run the state using --strip 1 with subtests.test("options='--strip=1'"): ret = file.patch( name=str(files.base_dir), source=patches.all_patch, options="--strip=1" ) assert ret.result is True assert not ret.changes assert ret.comment == "Patch was already applied" def test_patch_saltenv(file, files, patches): """ Test that we attempt to download the patch from a non-base saltenv """ # This state will fail because we don't have a patch file in that # environment, but that is OK, we just want to test that we're looking # in an environment other than base. ret = file.patch(name=str(files.math), source=patches.math_patch, saltenv="prod") assert ret.result is False assert ( ret.comment == "Unable to manage file: Source file {} not found in saltenv 'prod'".format( patches.math_patch ) ) def test_patch_single_file_failure(file, tmp_path, files, patches): """ Test file.patch using a patch applied to a single file. This tests a failed patch. """ # Empty the file to ensure that the patch doesn't apply cleanly files.numbers.write_text("") ret = file.patch(name=str(files.numbers), source=patches.numbers_patch) assert ret.result is False assert "Patch would not apply cleanly" in ret.comment # Test the reject_file option and ensure that the rejects are written # to the path specified. reject_file = tmp_path / "rejected" ret = file.patch( name=str(files.numbers), source=patches.numbers_patch, reject_file=str(reject_file), strip=1, ) assert ret.result is False assert "Patch would not apply cleanly" in ret.comment if salt.utils.platform.is_windows(): assert_fpath = f".*{reject_file.name}" else: assert_fpath = reject_file assert re.search(f"saving rejects to (file )?{assert_fpath}", ret.comment) @pytest.mark.skip_on_freebsd( reason="Previously skipped on FreeBSD. Needs investigation as to why it currently False" ) def test_patch_directory_failure(file, tmp_path, files, patches): """ Test file.patch using a patch applied to a directory, with changes spanning multiple files. """ # Empty the file to ensure that the patch doesn't apply cleanly files.numbers.write_text("") ret = file.patch(name=str(files.base_dir), source=patches.all_patch, strip=1) assert ret.result is False assert "Patch would not apply cleanly" in ret.comment # Test the reject_file option and ensure that the rejects are written # to the path specified. reject_file = tmp_path / "rejected" ret = file.patch( name=str(files.base_dir), source=patches.all_patch, reject_file=str(reject_file), strip=1, ) assert ret.result is False assert "Patch would not apply cleanly" in ret.comment if salt.utils.platform.is_windows(): assert_fpath = f".*{reject_file.name}" else: assert_fpath = reject_file assert re.search(f"saving rejects to (file )?{assert_fpath}", ret.comment) def test_patch_single_file_remote_source(file, files, patches, subtests): """ Test file.patch using a patch applied to a single file, with the patch coming from a remote source. """ # Try without a source_hash and without skip_verify=True, this should # fail with a message about the source_hash with subtests.test("source_hash=None"): ret = file.patch(name=str(files.math), source=patches.math_patch_http) assert ret.result is False assert "Unable to verify upstream hash" in ret.comment # Re-run the state with a source hash, it should now succeed with subtests.test("source_hash!=None"): ret = file.patch( name=str(files.math), source=patches.math_patch_http, source_hash=patches.math_patch_hash, ) assert ret.result is True assert ret.changes assert ret.comment == "Patch successfully applied" # Re-run again, this time with no hash and skip_verify=True to test # skipping hash verification with subtests.test("source_hash=None and skip_verify=True"): ret = file.patch( name=str(files.math), source=patches.math_patch_http, skip_verify=True ) assert ret.result is True assert not ret.changes assert ret.comment == "Patch was already applied" @pytest.mark.skip_on_freebsd( reason="Previously skipped on FreeBSD. Needs investigation as to why it currently False" ) @pytest.mark.skip_on_darwin( reason="Failing and previously skipped because patch wasn't >= 2.6" ) def test_patch_directory_remote_source(file, files, patches, subtests): """ Test file.patch using a patch applied to a directory, with changes spanning multiple files, and the patch file coming from a remote source. """ # Try without a source_hash and without skip_verify=True, this should # fail with a message about the source_hash with subtests.test("source_hash=None"): ret = file.patch( name=str(files.base_dir), source=patches.all_patch_http, strip=1 ) assert ret.result is False assert "Unable to verify upstream hash" in ret.comment # Re-run the state with a source hash, it should now succeed with subtests.test("source_hash!=None"): ret = file.patch( name=str(files.base_dir), source=patches.all_patch_http, source_hash=patches.all_patch_hash, strip=1, ) assert ret.result is True assert ret.changes assert ret.comment == "Patch successfully applied" # Re-run again, this time with no hash and skip_verify=True to test # skipping hash verification with subtests.test("source_hash=None and skip_verify=True"): ret = file.patch( name=str(files.base_dir), source=patches.all_patch_http, skip_verify=True, strip=1, ) assert ret.result is True assert not ret.changes assert ret.comment == "Patch was already applied" def test_patch_single_file_template(file, files, patches): """ Test file.patch using a patch applied to a single file, with jinja templating applied to the patch file. """ ret = file.patch( name=str(files.numbers), source=patches.numbers_patch_template, template="jinja", context={"two": "two", "ten": 10}, ) assert ret.result is True assert ret.comment == "Patch successfully applied" # Re-run the state, should succeed and there should be a message about # a partially-applied hunk. ret = file.patch( name=str(files.numbers), source=patches.numbers_patch_template, template="jinja", context={"two": "two", "ten": 10}, ) assert ret.result is True assert not ret.changes assert ret.comment == "Patch was already applied" @pytest.mark.skip_on_freebsd( reason="Previously skipped on FreeBSD. Needs investigation as to why it currently False" ) @pytest.mark.skip_on_darwin( reason="Failing and previously skipped because patch wasn't >= 2.6" ) def test_patch_directory_template(file, files, patches): """ Test file.patch using a patch applied to a directory, with changes spanning multiple files, and with jinja templating applied to the patch file. """ if salt.utils.platform.is_windows() and os.environ.get("PYTHONUTF8", "0") == "0": pytest.skip("Test will fail if PYTHONUTF8=1 is not set on windows") ret = file.patch( name=str(files.base_dir), source=patches.all_patch_template, template="jinja", context={"two": "two", "ten": 10}, strip=1, ) assert ret.result is True assert ret.changes assert ret.comment == "Patch successfully applied" # Re-run the state, should succeed and there should be a message about # a partially-applied hunk. ret = file.patch( name=str(files.base_dir), source=patches.all_patch_template, template="jinja", context={"two": "two", "ten": 10}, strip=1, ) assert ret.result is True assert not ret.changes assert ret.comment == "Patch was already applied" def test_patch_single_file_remote_source_template(file, files, patches, subtests): """ Test file.patch using a patch applied to a single file, with the patch coming from a remote source. """ if salt.utils.platform.is_windows() and os.environ.get("PYTHONUTF8", "0") == "0": pytest.skip("Test will fail if PYTHONUTF8=1 is not set on windows") # Try without a source_hash and without skip_verify=True, this should # fail with a message about the source_hash with subtests.test("source_hash=None and skip_verify=False"): ret = file.patch( name=str(files.math), source=patches.math_patch_template_http, template="jinja", context={"two": "two", "ten": 10}, ) assert ret.result is False assert not ret.changes assert "Unable to verify upstream hash" in ret.comment # Re-run the state with a source hash, it should now succeed with subtests.test("source_hash!=None"): ret = file.patch( name=str(files.math), source=patches.math_patch_template_http, source_hash=patches.math_patch_template_hash, template="jinja", context={"two": "two", "ten": 10}, ) assert ret.result is True assert ret.changes assert ret.comment == "Patch successfully applied" with subtests.test("source_hash=None and skip_verify=True"): ret = file.patch( name=str(files.math), source=patches.math_patch_template_http, template="jinja", context={"two": "two", "ten": 10}, skip_verify=True, ) assert ret.result is True assert not ret.changes assert ret.comment == "Patch was already applied" @pytest.mark.skip_on_freebsd( reason="Previously skipped on FreeBSD. Needs investigation as to why it currently False" ) @pytest.mark.skip_on_darwin( reason="Failing and previously skipped because patch wasn't >= 2.6" ) def test_patch_directory_remote_source_template(file, files, patches, subtests): """ Test file.patch using a patch applied to a directory, with changes spanning multiple files, and the patch file coming from a remote source. """ if salt.utils.platform.is_windows() and os.environ.get("PYTHONUTF8", "0") == "0": pytest.skip("Test will fail if PYTHONUTF8=1 is not set on windows") # Try without a source_hash and without skip_verify=True, this should # fail with a message about the source_hash with subtests.test("source_hash=None and skip_verify=False"): ret = file.patch( name=str(files.base_dir), source=patches.all_patch_template_http, template="jinja", context={"two": "two", "ten": 10}, strip=1, ) assert ret.result is False assert not ret.changes assert "Unable to verify upstream hash" in ret.comment # Re-run the state with a source hash, it should now succeed with subtests.test("source_hash!=None"): ret = file.patch( name=str(files.base_dir), source=patches.all_patch_template_http, source_hash=patches.all_patch_template_hash, template="jinja", context={"two": "two", "ten": 10}, strip=1, ) assert ret.result is True assert ret.changes assert ret.comment == "Patch successfully applied" with subtests.test("source_hash=None and skip_verify=True"): ret = file.patch( name=str(files.base_dir), source=patches.all_patch_template_http, template="jinja", context={"two": "two", "ten": 10}, skip_verify=True, strip=1, ) assert ret.result is True assert not ret.changes assert ret.comment == "Patch was already applied" def test_patch_test_mode(file, files, patches): """ Test file.patch using test=True """ ret = file.patch(name=str(files.numbers), source=patches.numbers_patch, test=True) assert ret.result is None assert ret.changes assert ret.comment == "The patch would be applied" # Apply the patch for real. We'll then be able to test below that we # exit with a True rather than a None result if test=True is used on an # already-applied patch. ret = file.patch(name=str(files.numbers), source=patches.numbers_patch) assert ret.result is True assert ret.changes assert ret.comment == "Patch successfully applied" # Run again with test=True. Since the pre-check happens before we do # the __opts__['test'] check, we should exit with a True result just # the same as if we try to run this state on an already-patched file # *without* test=True. ret = file.patch(name=str(files.numbers), source=patches.numbers_patch, test=True) assert ret.result is True assert not ret.changes assert ret.comment == "Patch was already applied" # Empty the file to ensure that the patch doesn't apply cleanly files.numbers.write_text("") # Run again with test=True. Similar to the above run, we are testing # that we return before we reach the __opts__['test'] check. In this # case we should return a False result because we should already know # by this point that the patch will not apply cleanly. ret = file.patch(name=str(files.numbers), source=patches.numbers_patch, test=True) assert ret.result is False assert not ret.changes assert "Patch would not apply cleanly" in ret.comment