1import logging 2import os 3import textwrap 4import threading 5import time 6 7import pytest 8import salt.loader 9import salt.utils.atomicfile 10import salt.utils.files 11import salt.utils.path 12import salt.utils.platform 13import salt.utils.stringutils 14 15log = logging.getLogger(__name__) 16 17 18pytestmark = [ 19 pytest.mark.windows_whitelisted, 20] 21 22 23def test_show_highstate(state, state_testfile_dest_path): 24 """ 25 state.show_highstate 26 """ 27 high = state.show_highstate() 28 assert isinstance(high, dict) 29 assert str(state_testfile_dest_path) in high 30 assert high[str(state_testfile_dest_path)]["__env__"] == "base" 31 32 33def test_show_lowstate(state): 34 """ 35 state.show_lowstate 36 """ 37 low = state.show_lowstate() 38 assert isinstance(low, list) 39 for entry in low: 40 assert isinstance(entry, dict) 41 42 43def test_show_states(state): 44 """ 45 state.show_states 46 """ 47 states = state.show_states() 48 assert isinstance(states, list) 49 for entry in states: 50 assert isinstance(entry, str) 51 assert states == ["core"] 52 53 54def test_show_states_missing_sls(state, state_tree): 55 """ 56 Test state.show_states with a sls file 57 defined in a top file is missing 58 """ 59 top_sls_contents = """ 60 base: 61 '*': 62 - core 63 - does-not-exist 64 """ 65 with pytest.helpers.temp_file("top.sls", top_sls_contents, state_tree): 66 states = state.show_states() 67 assert isinstance(states, list) 68 assert states == ["No matching sls found for 'does-not-exist' in env 'base'"] 69 70 71def test_catch_recurse(state, state_tree): 72 """ 73 state.show_sls used to catch a recursive ref 74 """ 75 sls_contents = """ 76 mysql: 77 service: 78 - running 79 - require: 80 - file: /etc/mysql/my.cnf 81 82 /etc/mysql/my.cnf: 83 file: 84 - managed 85 - source: salt://master.cnf 86 - require: 87 - service: mysql 88 """ 89 with pytest.helpers.temp_file("recurse-fail.sls", sls_contents, state_tree): 90 ret = state.sls("recurse-fail") 91 assert ret.failed 92 assert ( 93 'A recursive requisite was found, SLS "recurse-fail" ID "/etc/mysql/my.cnf" ID "mysql"' 94 in ret.errors 95 ) 96 97 98RECURSE_SLS_ONE = """ 99snmpd: 100 pkg: 101 - installed 102 service: 103 - running 104 - require: 105 - pkg: snmpd 106 - watch: 107 - file: /etc/snmp/snmpd.conf 108 109/etc/snmp/snmpd.conf: 110 file: 111 - managed 112 - source: salt://snmpd/snmpd.conf.jinja 113 - template: jinja 114 - user: root 115 - group: root 116 - mode: "0600" 117 - require: 118 - pkg: snmpd 119""" 120RECURSE_SLS_TWO = """ 121nagios-nrpe-server: 122 pkg: 123 - installed 124 service: 125 - running 126 - watch: 127 - file: /etc/nagios/nrpe.cfg 128 129/etc/nagios/nrpe.cfg: 130 file: 131 - managed 132 - source: salt://baseserver/nrpe.cfg 133 - require: 134 - pkg: nagios-nrpe-server 135""" 136 137 138@pytest.mark.parametrize( 139 "sls_contents, expected_in_output", 140 [(RECURSE_SLS_ONE, "snmpd"), (RECURSE_SLS_TWO, "/etc/nagios/nrpe.cfg")], 141 ids=("recurse-scenario-1", "recurse-scenario-2"), 142) 143def test_no_recurse(state, state_tree, sls_contents, expected_in_output): 144 """ 145 verify that a sls structure is NOT a recursive ref 146 """ 147 with pytest.helpers.temp_file("recurse-ok.sls", sls_contents, state_tree): 148 ret = state.show_sls("recurse-ok") 149 assert expected_in_output in ret 150 151 152def test_running_dictionary_consistency(state): 153 """ 154 Test the structure of the running dictionary so we don't change it 155 without deprecating/documenting the change 156 """ 157 running_dict_fields = { 158 "__id__", 159 "__run_num__", 160 "__sls__", 161 "changes", 162 "comment", 163 "duration", 164 "name", 165 "result", 166 "start_time", 167 } 168 169 sls = state.single("test.succeed_without_changes", name="gndn") 170 ret_values_set = set(sls.full_return.keys()) 171 assert running_dict_fields.issubset(ret_values_set) 172 173 174def test_running_dictionary_key_sls(state, state_tree): 175 """ 176 Ensure the __sls__ key is either null or a string 177 """ 178 sls1 = state.single("test.succeed_with_changes", name="gndn") 179 assert "__sls__" in sls1.full_return 180 assert sls1.full_return["__sls__"] is None 181 182 sls_contents = """ 183 gndn: 184 test.succeed_with_changes 185 """ 186 with pytest.helpers.temp_file("gndn.sls", sls_contents, state_tree): 187 sls2 = state.sls(mods="gndn") 188 189 for state_return in sls2: 190 assert "__sls__" in state_return.full_return 191 assert isinstance(state_return.full_return["__sls__"], str) 192 193 194@pytest.fixture 195def requested_sls_key(minion_opts, state_tree): 196 if not salt.utils.platform.is_windows(): 197 sls_contents = """ 198 count_root_dir_contents: 199 cmd.run: 200 - name: 'ls -a / | wc -l' 201 """ 202 sls_key = "cmd_|-count_root_dir_contents_|-ls -a / | wc -l_|-run" 203 else: 204 sls_contents = r""" 205 count_root_dir_contents: 206 cmd.run: 207 - name: 'Get-ChildItem C:\ | Measure-Object | %{$_.Count}' 208 - shell: powershell 209 """ 210 sls_key = ( 211 r"cmd_|-count_root_dir_contents_|-Get-ChildItem C:\ | Measure-Object |" 212 r" %{$_.Count}_|-run" 213 ) 214 try: 215 with pytest.helpers.temp_file( 216 "requested.sls", sls_contents, state_tree 217 ) as sls_path: 218 yield sls_key 219 finally: 220 cache_file = os.path.join(minion_opts["cachedir"], "req_state.p") 221 if os.path.exists(cache_file): 222 os.remove(cache_file) 223 224 225def test_request(state, requested_sls_key): 226 """ 227 verify sending a state request to the minion(s) 228 """ 229 ret = state.request("requested") 230 assert ret[requested_sls_key]["result"] is None 231 232 233def test_check_request(state, requested_sls_key): 234 """ 235 verify checking a state request sent to the minion(s) 236 """ 237 ret = state.request("requested") 238 assert ret[requested_sls_key]["result"] is None 239 240 ret = state.check_request() 241 assert ret["default"]["test_run"][requested_sls_key]["result"] is None 242 243 244def test_clear_request(state, requested_sls_key): 245 """ 246 verify clearing a state request sent to the minion(s) 247 """ 248 ret = state.request("requested") 249 assert ret[requested_sls_key]["result"] is None 250 251 ret = state.clear_request() 252 assert ret is True 253 254 255def test_run_request_succeeded(state, requested_sls_key): 256 """ 257 verify running a state request sent to the minion(s) 258 """ 259 ret = state.request("requested") 260 assert ret[requested_sls_key]["result"] is None 261 262 ret = state.run_request() 263 assert ret[requested_sls_key]["result"] is True 264 265 266def test_run_request_failed_no_request_staged(state, requested_sls_key): 267 """ 268 verify not running a state request sent to the minion(s) 269 """ 270 ret = state.request("requested") 271 assert ret[requested_sls_key]["result"] is None 272 273 ret = state.clear_request() 274 assert ret is True 275 276 ret = state.run_request() 277 assert ret == {} 278 279 280def test_issue_1876_syntax_error(state, state_tree, tmp_path): 281 """ 282 verify that we catch the following syntax error:: 283 284 /tmp/salttest/issue-1876: 285 286 file: 287 - managed 288 - source: salt://testfile 289 290 file.append: 291 - text: foo 292 293 """ 294 testfile = tmp_path / "issue-1876.txt" 295 sls_contents = """ 296 {}: 297 file: 298 - managed 299 - source: salt://testfile 300 301 file.append: 302 - text: foo 303 """.format( 304 testfile 305 ) 306 with pytest.helpers.temp_file("issue-1876.sls", sls_contents, state_tree): 307 ret = state.sls("issue-1876") 308 assert ret.failed 309 errmsg = ( 310 "ID '{}' in SLS 'issue-1876' contains multiple state declarations of the" 311 " same type".format(testfile) 312 ) 313 assert errmsg in ret.errors 314 315 316def test_issue_1879_too_simple_contains_check(state, state_tree, tmp_path): 317 testfile = tmp_path / "issue-1979.txt" 318 init_sls_contents = """ 319 {}: 320 file: 321 - touch 322 """.format( 323 testfile 324 ) 325 step1_sls_contents = """ 326 {}: 327 file.append: 328 - text: | 329 # set variable identifying the chroot you work in (used in the prompt below) 330 if [ -z "$debian_chroot" ] && [ -r /etc/debian_chroot ]; then 331 debian_chroot=$(cat /etc/debian_chroot) 332 fi 333 334 """.format( 335 testfile 336 ) 337 step2_sls_contents = """ 338 {}: 339 file.append: 340 - text: | 341 # enable bash completion in interactive shells 342 if [ -f /etc/bash_completion ] && ! shopt -oq posix; then 343 . /etc/bash_completion 344 fi 345 346 """.format( 347 testfile 348 ) 349 350 expected = textwrap.dedent( 351 """\ 352 # set variable identifying the chroot you work in (used in the prompt below) 353 if [ -z "$debian_chroot" ] && [ -r /etc/debian_chroot ]; then 354 debian_chroot=$(cat /etc/debian_chroot) 355 fi 356 # enable bash completion in interactive shells 357 if [ -f /etc/bash_completion ] && ! shopt -oq posix; then 358 . /etc/bash_completion 359 fi 360 """ 361 ) 362 363 issue_1879_dir = state_tree / "issue-1879" 364 with pytest.helpers.temp_file( 365 "init.sls", init_sls_contents, issue_1879_dir 366 ), pytest.helpers.temp_file( 367 "step-1.sls", step1_sls_contents, issue_1879_dir 368 ), pytest.helpers.temp_file( 369 "step-2.sls", step2_sls_contents, issue_1879_dir 370 ): 371 # Create the file 372 ret = state.sls("issue-1879") 373 for staterun in ret: 374 assert staterun.result is True 375 376 # The first append 377 ret = state.sls("issue-1879.step-1") 378 for staterun in ret: 379 assert staterun.result is True 380 381 # The second append 382 ret = state.sls("issue-1879.step-2") 383 for staterun in ret: 384 assert staterun.result is True 385 386 # Does it match? 387 contents = testfile.read_text() 388 assert contents == expected 389 390 # Make sure we don't re-append existing text 391 ret = state.sls("issue-1879.step-1") 392 for staterun in ret: 393 assert staterun.result is True 394 395 ret = state.sls("issue-1879.step-2") 396 for staterun in ret: 397 assert staterun.result is True 398 399 # Does it match? 400 contents = testfile.read_text() 401 assert contents == expected 402 403 404def test_include(state, state_tree, tmp_path): 405 testfile_path = tmp_path / "testfile" 406 testfile_path.write_text("foo") 407 include_test_path = tmp_path / "include-test.txt" 408 to_include_test_path = tmp_path / "to-include-test.txt" 409 exclude_test_path = tmp_path / "exclude-test.txt" 410 to_include_sls_contents = """ 411 {}: 412 file.managed: 413 - source: salt://testfile 414 """.format( 415 to_include_test_path 416 ) 417 include_sls_contents = """ 418 include: 419 - to-include-test 420 421 {}: 422 file.managed: 423 - source: salt://testfile 424 """.format( 425 include_test_path 426 ) 427 with pytest.helpers.temp_file( 428 "testfile", "foo", state_tree 429 ), pytest.helpers.temp_file( 430 "to-include-test.sls", to_include_sls_contents, state_tree 431 ), pytest.helpers.temp_file( 432 "include-test.sls", include_sls_contents, state_tree 433 ): 434 ret = state.sls("include-test") 435 for staterun in ret: 436 assert staterun.result is True 437 438 assert include_test_path.exists() 439 assert to_include_test_path.exists() 440 assert exclude_test_path.exists() is False 441 442 443def test_exclude(state, state_tree, tmp_path): 444 testfile_path = tmp_path / "testfile" 445 testfile_path.write_text("foo") 446 include_test_path = tmp_path / "include-test.txt" 447 to_include_test_path = tmp_path / "to-include-test.txt" 448 exclude_test_path = tmp_path / "exclude-test.txt" 449 to_include_sls_contents = """ 450 {}: 451 file.managed: 452 - source: salt://testfile 453 """.format( 454 to_include_test_path 455 ) 456 include_sls_contents = """ 457 include: 458 - to-include-test 459 460 {}: 461 file.managed: 462 - source: salt://testfile 463 """.format( 464 include_test_path 465 ) 466 exclude_sls_contents = """ 467 exclude: 468 - to-include-test 469 470 include: 471 - include-test 472 473 {}: 474 file.managed: 475 - source: salt://testfile 476 """.format( 477 exclude_test_path 478 ) 479 with pytest.helpers.temp_file( 480 "testfile", "foo", state_tree 481 ), pytest.helpers.temp_file( 482 "to-include-test.sls", to_include_sls_contents, state_tree 483 ), pytest.helpers.temp_file( 484 "include-test.sls", include_sls_contents, state_tree 485 ), pytest.helpers.temp_file( 486 "exclude-test.sls", exclude_sls_contents, state_tree 487 ): 488 ret = state.sls("exclude-test") 489 for staterun in ret: 490 assert staterun.result is True 491 492 assert include_test_path.exists() 493 assert exclude_test_path.exists() 494 assert to_include_test_path.exists() is False 495 496 497def test_issue_2068_template_str(state, state_tree): 498 template_str_no_dot_sls_contents = """ 499 required_state: 500 test: 501 - succeed_without_changes 502 503 requiring_state: 504 test: 505 - succeed_without_changes 506 - require: 507 - test: required_state 508 """ 509 template_str_sls_contents = """ 510 required_state: test.succeed_without_changes 511 512 requiring_state: 513 test.succeed_without_changes: 514 - require: 515 - test: required_state 516 """ 517 with pytest.helpers.temp_file( 518 "issue-2068-no-dot.sls", template_str_no_dot_sls_contents, state_tree 519 ) as template_str_no_dot_path, pytest.helpers.temp_file( 520 "issue-2068.sls", template_str_sls_contents, state_tree 521 ) as template_str_path: 522 # If running this state with state.sls works, so should using state.template_str 523 ret = state.sls("issue-2068-no-dot") 524 for staterun in ret: 525 assert staterun.result is True 526 527 template_str_no_dot_contents = template_str_no_dot_path.read_text() 528 ret = state.template_str(template_str_no_dot_contents) 529 for staterun in ret: 530 assert staterun.result is True 531 532 # Now using state.template 533 ret = state.template(str(template_str_no_dot_path)) 534 for staterun in ret: 535 assert staterun.result is True 536 537 # Now the problematic #2068 including dot's 538 ret = state.sls("issue-2068") 539 for staterun in ret: 540 assert staterun.result is True 541 542 template_str_contents = template_str_path.read_text() 543 ret = state.template_str(template_str_contents) 544 for staterun in ret: 545 assert staterun.result is True 546 547 # Now using state.template 548 ret = state.template(str(template_str_path)) 549 for staterun in ret: 550 assert staterun.result is True 551 552 553@pytest.mark.parametrize("item", ("include", "exclude", "extends")) 554def test_template_str_invalid_items(state, item): 555 TEMPLATE = textwrap.dedent( 556 """\ 557 {}: 558 - existing-state 559 560 /tmp/test-template-invalid-items: 561 file: 562 - managed 563 - source: salt://testfile 564 """.format( 565 item 566 ) 567 ) 568 569 ret = state.template_str(TEMPLATE.format(item)) 570 assert ret.failed 571 errmsg = ( 572 "The '{}' declaration found on '<template-str>' is invalid when " 573 "rendering single templates".format(item) 574 ) 575 assert errmsg in ret.errors 576 577 578@pytest.mark.skip_on_windows( 579 reason=( 580 "Functional testing this on windows raises unicode errors. " 581 "Tested in tests/pytests/integration/modules/state/test_state.py" 582 ) 583) 584def test_pydsl(state, state_tree, tmp_path): 585 """ 586 Test the basics of the pydsl 587 """ 588 testfile = tmp_path / "testfile" 589 sls_contents = """ 590 #!pydsl 591 592 state("{}").file("touch") 593 """.format( 594 testfile 595 ) 596 with pytest.helpers.temp_file("pydsl.sls", sls_contents, state_tree): 597 ret = state.sls("pydsl") 598 for staterun in ret: 599 assert staterun.result is True 600 assert testfile.exists() 601 602 603def test_issues_7905_and_8174_sls_syntax_error(state, state_tree): 604 """ 605 Call sls file with yaml syntax error. 606 607 Ensure theses errors are detected and presented to the user without 608 stack traces. 609 """ 610 badlist_1_sls_contents = """ 611 # Missing " " between "-" and "foo" or "name" 612 A: 613 cmd.run: 614 -name: echo foo 615 -foo: 616 - bar 617 """ 618 badlist_2_sls_contents = """ 619 # C should fail with bad list error message 620 B: 621 # ok 622 file.exist: 623 - name: /foo/bar/foobar 624 # ok 625 /foo/bar/foobar: 626 file.exist 627 628 # nok 629 C: 630 /foo/bar/foobar: 631 file.exist 632 """ 633 with pytest.helpers.temp_file( 634 "badlist1.sls", badlist_1_sls_contents, state_tree 635 ), pytest.helpers.temp_file("badlist2.sls", badlist_2_sls_contents, state_tree): 636 ret = state.sls("badlist1") 637 assert ret.failed 638 assert ret.errors == ["State 'A' in SLS 'badlist1' is not formed as a list"] 639 640 ret = state.sls("badlist2") 641 assert ret.failed 642 assert ret.errors == ["State 'C' in SLS 'badlist2' is not formed as a list"] 643 644 645@pytest.mark.slow_test 646def test_retry_option(state, state_tree): 647 """ 648 test the retry option on a simple state with defaults 649 ensure comment is as expected 650 ensure state duration is greater than configured the passed (interval * attempts) 651 """ 652 sls_contents = """ 653 file_test: 654 file.exists: 655 - name: /path/to/a/non-existent/file.txt 656 - retry: 657 until: True 658 attempts: 3 659 interval: 1 660 splay: 0 661 """ 662 expected_comment = ( 663 'Attempt 1: Returned a result of "False", with the following ' 664 'comment: "Specified path /path/to/a/non-existent/file.txt does not exist"' 665 ) 666 with pytest.helpers.temp_file("retry.sls", sls_contents, state_tree): 667 ret = state.sls("retry") 668 for state_return in ret: 669 assert state_return.result is False 670 assert expected_comment in state_return.comment 671 assert state_return.full_return["duration"] >= 3 672 673 674def test_retry_option_success(state, state_tree, tmp_path): 675 """ 676 test a state with the retry option that should return True immediately (i.e. no retries) 677 """ 678 testfile = tmp_path / "testfile" 679 testfile.touch() 680 sls_contents = """ 681 file_test: 682 file.exists: 683 - name: {} 684 - retry: 685 until: True 686 attempts: 5 687 interval: 2 688 splay: 0 689 """.format( 690 testfile 691 ) 692 with pytest.helpers.temp_file("retry.sls", sls_contents, state_tree): 693 ret = state.sls("retry") 694 for state_return in ret: 695 assert state_return.result is True 696 assert state_return.full_return["duration"] < 4 697 # It should not take 2 attempts 698 assert "Attempt 2" not in state_return.comment 699 700 701@pytest.mark.slow_test 702def test_retry_option_eventual_success(state, state_tree, tmp_path): 703 """ 704 test a state with the retry option that should return True, eventually 705 """ 706 testfile1 = tmp_path / "testfile-1" 707 testfile2 = tmp_path / "testfile-2" 708 709 def create_testfile(testfile1, testfile2): 710 while True: 711 if testfile1.exists(): 712 break 713 time.sleep(2) 714 testfile2.touch() 715 716 thread = threading.Thread(target=create_testfile, args=(testfile1, testfile2)) 717 sls_contents = """ 718 file_test_a: 719 file.managed: 720 - name: {} 721 - content: 'a' 722 723 file_test: 724 file.exists: 725 - name: {} 726 - retry: 727 until: True 728 attempts: 5 729 interval: 2 730 splay: 0 731 - require: 732 - file_test_a 733 """.format( 734 testfile1, testfile2 735 ) 736 with pytest.helpers.temp_file("retry.sls", sls_contents, state_tree): 737 thread.start() 738 ret = state.sls("retry") 739 for state_return in ret: 740 assert state_return.result is True 741 assert state_return.full_return["duration"] > 4 742 # It should not take 5 attempts 743 assert "Attempt 5" not in state_return.comment 744 745 746@pytest.mark.slow_test 747def test_state_non_base_environment(state, state_tree_prod, tmp_path): 748 """ 749 test state.sls with saltenv using a nonbase environment 750 with a salt source 751 """ 752 testfile = tmp_path / "testfile" 753 sls_contents = """ 754 {}: 755 file.managed: 756 - content: foo 757 """.format( 758 testfile 759 ) 760 with pytest.helpers.temp_file("non-base-env.sls", sls_contents, state_tree_prod): 761 ret = state.sls("non-base-env", saltenv="prod") 762 for state_return in ret: 763 assert state_return.result is True 764 assert testfile.exists() 765 766 767@pytest.mark.skip_on_windows( 768 reason="Skipped until parallel states can be fixed on Windows" 769) 770def test_parallel_state_with_long_tag(state, state_tree): 771 """ 772 This tests the case where the state being executed has a long ID dec or 773 name and states are being run in parallel. The filenames used for the 774 parallel state cache were previously based on the tag for each chunk, 775 and longer ID decs or name params can cause the cache file to be longer 776 than the operating system's max file name length. To counter this we 777 instead generate a SHA1 hash of the chunk's tag to use as the cache 778 filename. This test will ensure that long tags don't cause caching 779 failures. 780 781 See https://github.com/saltstack/salt/issues/49738 for more info. 782 """ 783 short_command = "helloworld" 784 long_command = short_command * 25 785 sls_contents = """ 786 test_cmd_short: 787 cmd.run: 788 - name: {} 789 - parallel: True 790 791 test_cmd_long: 792 cmd.run: 793 - name: {} 794 - parallel: True 795 """.format( 796 short_command, long_command 797 ) 798 with pytest.helpers.temp_file("issue-49738.sls", sls_contents, state_tree): 799 ret = state.sls( 800 "issue-49738", 801 __pub_jid="1", # Because these run in parallel we need a fake JID 802 ) 803 804 comments = sorted([x.comment for x in ret]) 805 expected = sorted( 806 ['Command "{}" run'.format(x) for x in (short_command, long_command)] 807 ) 808 assert comments == expected, "{} != {}".format(comments, expected) 809 810 811@pytest.mark.skip_on_darwin(reason="Test is broken on macosx") 812@pytest.mark.skip_on_windows( 813 reason=( 814 "Functional testing this on windows raises unicode errors. " 815 "Tested in tests/pytests/integration/modules/state/test_state.py" 816 ) 817) 818def test_state_sls_unicode_characters(state, state_tree): 819 """ 820 test state.sls when state file contains non-ascii characters 821 """ 822 sls_contents = """ 823 echo1: 824 cmd.run: 825 - name: "echo 'This is Æ test!'" 826 """ 827 with pytest.helpers.temp_file("issue-46672.sls", sls_contents, state_tree): 828 ret = state.sls("issue-46672") 829 expected = "cmd_|-echo1_|-echo 'This is Æ test!'_|-run" 830 assert expected in ret 831 832 833def test_state_sls_integer_name(state, state_tree): 834 """ 835 This tests the case where the state file is named 836 only with integers 837 """ 838 sls_contents = """ 839 always-passes: 840 test.succeed_without_changes 841 """ 842 state_id = "test_|-always-passes_|-always-passes_|-succeed_without_changes" 843 with pytest.helpers.temp_file("12345.sls", sls_contents, state_tree): 844 ret = state.sls("12345") 845 assert state_id in ret 846 for state_return in ret: 847 assert state_return.result is True 848 assert "Success!" in state_return.comment 849 850 ret = state.sls(mods=12345) 851 assert state_id in ret 852 for state_return in ret: 853 assert state_return.result is True 854 assert "Success!" in state_return.comment 855 856 857def test_state_sls_lazyloader_allows_recursion(state, state_tree): 858 """ 859 This tests that referencing dunders like __salt__ work 860 context: https://github.com/saltstack/salt/pull/51499 861 """ 862 sls_contents = """ 863 {% if 'nonexistent_module.function' in salt %} 864 {% do salt.log.warning("Module is available") %} 865 {% endif %} 866 always-passes: 867 test.succeed_without_changes: 868 - name: foo 869 """ 870 state_id = "test_|-always-passes_|-foo_|-succeed_without_changes" 871 with pytest.helpers.temp_file("issue-51499.sls", sls_contents, state_tree): 872 ret = state.sls("issue-51499") 873 assert state_id in ret 874 for state_return in ret: 875 assert state_return.result is True 876 assert "Success!" in state_return.comment 877