123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839 |
- # -*- coding: utf-8 -*-
- """
- Tests for the state runner
- """
- # Import Python Libs
- from __future__ import absolute_import, print_function, unicode_literals
- import errno
- import logging
- import os
- import shutil
- import signal
- import tempfile
- import textwrap
- import threading
- import time
- # Import Salt Libs
- import salt.exceptions
- import salt.utils.event
- import salt.utils.files
- import salt.utils.json
- import salt.utils.platform
- import salt.utils.stringutils
- import salt.utils.yaml
- # Import 3rd-party libs
- from salt.ext import six
- from salt.ext.six.moves import queue
- from tests.support.case import ShellCase
- from tests.support.helpers import expensiveTest, flaky
- from tests.support.mock import MagicMock, patch
- # Import Salt Testing Libs
- from tests.support.runtests import RUNTIME_VARS
- from tests.support.unit import skipIf
- log = logging.getLogger(__name__)
- @flaky
- class StateRunnerTest(ShellCase):
- """
- Test the state runner.
- """
- def add_to_queue(self, q, cmd):
- """
- helper method to add salt-run
- return data to a queue
- """
- ret = self.run_run(cmd)
- q.put(ret)
- q.task_done()
- @skipIf(True, "SLOWTEST skip")
- def test_orchestrate_output(self):
- """
- Ensure the orchestrate runner outputs useful state data.
- In Issue #31330, the output only contains ['outputter:', ' highstate'],
- and not the full stateful return. This tests ensures we don't regress in that
- manner again.
- Also test against some sample "good" output that would be included in a correct
- orchestrate run.
- """
- ret_output = self.run_run("state.orchestrate orch.simple")
- bad_out = ["outputter:", " highstate"]
- good_out = [
- " Function: salt.state",
- " Result: True",
- "Succeeded: 1 (changed=1)",
- "Failed: 0",
- "Total states run: 1",
- ]
- # First, check that we don't have the "bad" output that was displaying in
- # Issue #31330 where only the highstate outputter was listed
- assert bad_out != ret_output
- # Now test that some expected good sample output is present in the return.
- for item in good_out:
- assert item in ret_output
- @skipIf(True, "SLOWTEST skip")
- def test_orchestrate_nested(self):
- """
- test salt-run state.orchestrate and failhard with nested orchestration
- """
- if os.path.exists("/tmp/ewu-2016-12-13"):
- os.remove("/tmp/ewu-2016-12-13")
- _, code = self.run_run("state.orchestrate nested-orch.outer", with_retcode=True)
- assert os.path.exists("/tmp/ewu-2016-12-13") is False
- assert code != 0
- @skipIf(True, "SLOWTEST skip")
- def test_orchestrate_with_mine(self):
- """
- test salt-run state.orchestrate with mine.get call in sls
- """
- fail_time = time.time() + 120
- self.run_run('mine.update "*"')
- exp_ret = "Succeeded: 1 (changed=1)"
- while True:
- ret = self.run_run("state.orchestrate orch.mine")
- try:
- assert exp_ret in ret
- break
- except AssertionError:
- if time.time() > fail_time:
- self.fail(
- '"{0}" was not found in the orchestration call'.format(exp_ret)
- )
- @skipIf(True, "SLOWTEST skip")
- def test_orchestrate_state_and_function_failure(self):
- """
- Ensure that returns from failed minions are in the changes dict where
- they belong, so they can be programatically analyzed.
- See https://github.com/saltstack/salt/issues/43204
- """
- self.run_run("saltutil.sync_modules")
- ret = salt.utils.json.loads(
- "\n".join(self.run_run("state.orchestrate orch.issue43204 --out=json"))
- )
- # Drill down to the changes dict
- state_ret = ret["data"]["master"]["salt_|-Step01_|-Step01_|-state"]["changes"]
- func_ret = ret["data"]["master"][
- "salt_|-Step02_|-runtests_helpers.nonzero_retcode_return_false_|-function"
- ]["changes"]
- # Remove duration and start time from the results, since they would
- # vary with each run and that would make it impossible to test.
- for item in ("duration", "start_time"):
- state_ret["ret"]["minion"][
- "test_|-test fail with changes_|-test fail with changes_|-fail_with_changes"
- ].pop(item)
- self.assertEqual(
- state_ret,
- {
- "out": "highstate",
- "ret": {
- "minion": {
- "test_|-test fail with changes_|-test fail with changes_|-fail_with_changes": {
- "__id__": "test fail with changes",
- "__run_num__": 0,
- "__sls__": "orch.issue43204.fail_with_changes",
- "changes": {
- "testing": {
- "new": "Something pretended to change",
- "old": "Unchanged",
- }
- },
- "comment": "Failure!",
- "name": "test fail with changes",
- "result": False,
- }
- }
- },
- },
- )
- self.assertEqual(func_ret, {"out": "highstate", "ret": {"minion": False}})
- @skipIf(True, "SLOWTEST skip")
- def test_orchestrate_target_exists(self):
- """
- test orchestration when target exists
- while using multiple states
- """
- ret = self.run_run("state.orchestrate orch.target-exists")
- first = [" ID: core", " Function: salt.state", " Result: True"]
- second = [
- " ID: test-state",
- " Function: salt.state",
- " Result: True",
- ]
- third = [
- " ID: cmd.run",
- " Function: salt.function",
- " Result: True",
- ]
- ret_out = [first, second, third]
- for out in ret_out:
- for item in out:
- assert item in ret
- @skipIf(True, "SLOWTEST skip")
- def test_orchestrate_retcode(self):
- """
- Test orchestration with nonzero retcode set in __context__
- """
- self.run_run("saltutil.sync_runners")
- self.run_run("saltutil.sync_wheel")
- ret = "\n".join(self.run_run("state.orchestrate orch.retcode"))
- for result in (
- " ID: test_runner_success\n"
- " Function: salt.runner\n"
- " Name: runtests_helpers.success\n"
- " Result: True",
- " ID: test_runner_failure\n"
- " Function: salt.runner\n"
- " Name: runtests_helpers.failure\n"
- " Result: False",
- " ID: test_wheel_success\n"
- " Function: salt.wheel\n"
- " Name: runtests_helpers.success\n"
- " Result: True",
- " ID: test_wheel_failure\n"
- " Function: salt.wheel\n"
- " Name: runtests_helpers.failure\n"
- " Result: False",
- ):
- self.assertIn(result, ret)
- @skipIf(True, "SLOWTEST skip")
- def test_orchestrate_target_does_not_exist(self):
- """
- test orchestration when target doesn't exist
- while using multiple states
- """
- ret = self.run_run("state.orchestrate orch.target-does-not-exist")
- first = [
- "No minions matched the target. No command was sent, no jid was assigned.",
- " ID: core",
- " Function: salt.state",
- " Result: False",
- ]
- second = [
- " ID: test-state",
- " Function: salt.state",
- " Result: True",
- ]
- third = [
- " ID: cmd.run",
- " Function: salt.function",
- " Result: True",
- ]
- ret_out = [first, second, third]
- for out in ret_out:
- for item in out:
- assert item in ret
- @skipIf(True, "SLOWTEST skip")
- def test_orchestrate_batch_with_failhard_error(self):
- """
- test orchestration properly stops with failhard and batch.
- """
- ret = self.run_run("state.orchestrate orch.batch --out=json -l critical")
- ret_json = salt.utils.json.loads("\n".join(ret))
- retcode = ret_json["retcode"]
- result = ret_json["data"]["master"][
- "salt_|-call_fail_state_|-call_fail_state_|-state"
- ]["result"]
- changes = ret_json["data"]["master"][
- "salt_|-call_fail_state_|-call_fail_state_|-state"
- ]["changes"]
- # Looks like there is a platform differences in execution.
- # I see empty changes dict in MacOS for some reason. Maybe it's a bug?
- if changes:
- changes_ret = changes["ret"]
- # Debug
- print("Retcode: {}".format(retcode))
- print("Changes: {}".format(changes))
- print("Result: {}".format(result))
- assert retcode != 0
- assert result is False
- if changes:
- # The execution should stop after first error, so return dict should contain only one minion
- assert len(changes_ret) == 1
- @skipIf(True, "SLOWTEST skip")
- def test_state_event(self):
- """
- test to ensure state.event
- runner returns correct data
- """
- q = queue.Queue(maxsize=0)
- cmd = "state.event salt/job/*/new count=1"
- expect = '"minions": ["minion"]'
- server_thread = threading.Thread(target=self.add_to_queue, args=(q, cmd))
- server_thread.setDaemon(True)
- server_thread.start()
- while q.empty():
- self.run_salt("minion test.ping --static")
- out = q.get()
- assert expect in six.text_type(out)
- server_thread.join()
- @skipIf(True, "SLOWTEST skip")
- def test_orchestrate_subset(self):
- """
- test orchestration state using subset
- """
- ret = self.run_run("state.orchestrate orch.subset", timeout=500)
- def count(thing, listobj):
- return sum([obj.strip() == thing for obj in listobj])
- assert count("ID: test subset", ret) == 1
- assert count("Succeeded: 1", ret) == 1
- assert count("Failed: 0", ret) == 1
- @skipIf(True, "SLOWTEST skip")
- def test_orchestrate_salt_function_return_false_failure(self):
- """
- Ensure that functions that only return False in the return
- are flagged as failed when run as orchestrations.
- See https://github.com/saltstack/salt/issues/30367
- """
- self.run_run("saltutil.sync_modules")
- ret = salt.utils.json.loads(
- "\n".join(self.run_run("state.orchestrate orch.issue30367 --out=json"))
- )
- # Drill down to the changes dict
- state_result = ret["data"]["master"][
- "salt_|-deploy_check_|-test.false_|-function"
- ]["result"]
- func_ret = ret["data"]["master"]["salt_|-deploy_check_|-test.false_|-function"][
- "changes"
- ]
- assert state_result is False
- self.assertEqual(func_ret, {"out": "highstate", "ret": {"minion": False}})
- @skipIf(salt.utils.platform.is_windows(), "*NIX-only test")
- @flaky
- class OrchEventTest(ShellCase):
- """
- Tests for orchestration events
- """
- def setUp(self):
- self.timeout = 60
- self.master_d_dir = os.path.join(RUNTIME_VARS.TMP_CONF_DIR, "master.d")
- try:
- os.makedirs(self.master_d_dir)
- except OSError as exc:
- if exc.errno != errno.EEXIST:
- raise
- self.conf = tempfile.NamedTemporaryFile(
- mode="w", suffix=".conf", dir=self.master_d_dir, delete=True,
- )
- self.base_env = tempfile.mkdtemp(dir=RUNTIME_VARS.TMP)
- self.addCleanup(shutil.rmtree, self.base_env)
- self.addCleanup(self.conf.close)
- for attr in ("timeout", "master_d_dir", "conf", "base_env"):
- self.addCleanup(delattr, self, attr)
- # Force a reload of the configuration now that our temp config file has
- # been removed.
- self.addCleanup(self.run_run_plus, "test.arg", __reload_config=True)
- def alarm_handler(self, signal, frame):
- raise Exception("Timeout of {0} seconds reached".format(self.timeout))
- def write_conf(self, data):
- """
- Dump the config dict to the conf file
- """
- self.conf.write(salt.utils.yaml.safe_dump(data, default_flow_style=False))
- self.conf.flush()
- @expensiveTest
- def test_jid_in_ret_event(self):
- """
- Test to confirm that the ret event for the orchestration contains the
- jid for the jobs spawned.
- """
- self.write_conf(
- {"fileserver_backend": ["roots"], "file_roots": {"base": [self.base_env]}}
- )
- state_sls = os.path.join(self.base_env, "test_state.sls")
- with salt.utils.files.fopen(state_sls, "w") as fp_:
- fp_.write(
- salt.utils.stringutils.to_str(
- textwrap.dedent(
- """
- date:
- cmd.run
- """
- )
- )
- )
- orch_sls = os.path.join(self.base_env, "test_orch.sls")
- with salt.utils.files.fopen(orch_sls, "w") as fp_:
- fp_.write(
- salt.utils.stringutils.to_str(
- textwrap.dedent(
- """
- date_cmd:
- salt.state:
- - tgt: minion
- - sls: test_state
- ping_minion:
- salt.function:
- - name: test.ping
- - tgt: minion
- fileserver.file_list:
- salt.runner
- config.values:
- salt.wheel
- """
- )
- )
- )
- listener = salt.utils.event.get_event(
- "master",
- sock_dir=self.master_opts["sock_dir"],
- transport=self.master_opts["transport"],
- opts=self.master_opts,
- )
- jid = self.run_run_plus(
- "state.orchestrate", "test_orch", __reload_config=True
- ).get("jid")
- if jid is None:
- raise Exception("jid missing from run_run_plus output")
- signal.signal(signal.SIGALRM, self.alarm_handler)
- signal.alarm(self.timeout)
- try:
- while True:
- event = listener.get_event(full=True)
- if event is None:
- continue
- if event["tag"] == "salt/run/{0}/ret".format(jid):
- # Don't wrap this in a try/except. We want to know if the
- # data structure is different from what we expect!
- ret = event["data"]["return"]["data"]["master"]
- for job in ret:
- self.assertTrue("__jid__" in ret[job])
- break
- finally:
- del listener
- signal.alarm(0)
- @expensiveTest
- def test_parallel_orchestrations(self):
- """
- Test to confirm that the parallel state requisite works in orch
- we do this by running 10 test.sleep's of 10 seconds, and insure it only takes roughly 10s
- """
- self.write_conf(
- {"fileserver_backend": ["roots"], "file_roots": {"base": [self.base_env]}}
- )
- orch_sls = os.path.join(self.base_env, "test_par_orch.sls")
- with salt.utils.files.fopen(orch_sls, "w") as fp_:
- fp_.write(
- textwrap.dedent(
- """
- {% for count in range(1, 20) %}
- sleep {{ count }}:
- module.run:
- - name: test.sleep
- - length: 10
- - parallel: True
- {% endfor %}
- sleep 21:
- module.run:
- - name: test.sleep
- - length: 10
- - parallel: True
- - require:
- - module: sleep 1
- """
- )
- )
- orch_sls = os.path.join(self.base_env, "test_par_orch.sls")
- listener = salt.utils.event.get_event(
- "master",
- sock_dir=self.master_opts["sock_dir"],
- transport=self.master_opts["transport"],
- opts=self.master_opts,
- )
- start_time = time.time()
- jid = self.run_run_plus(
- "state.orchestrate", "test_par_orch", __reload_config=True
- ).get("jid")
- if jid is None:
- raise Exception("jid missing from run_run_plus output")
- signal.signal(signal.SIGALRM, self.alarm_handler)
- signal.alarm(self.timeout)
- received = False
- try:
- while True:
- event = listener.get_event(full=True)
- if event is None:
- continue
- # if we receive the ret for this job before self.timeout (60),
- # the test is implicitly successful; if it were happening in serial it would be
- # atleast 110 seconds.
- if event["tag"] == "salt/run/{0}/ret".format(jid):
- received = True
- # Don't wrap this in a try/except. We want to know if the
- # data structure is different from what we expect!
- ret = event["data"]["return"]["data"]["master"]
- for state in ret:
- data = ret[state]
- # we expect each duration to be greater than 10s
- self.assertTrue(data["duration"] > 10000)
- break
- # self confirm that the total runtime is roughly 30s (left 10s for buffer)
- self.assertTrue((time.time() - start_time) < 40)
- finally:
- self.assertTrue(received)
- del listener
- signal.alarm(0)
- @expensiveTest
- def test_orchestration_soft_kill(self):
- """
- Test to confirm that the parallel state requisite works in orch
- we do this by running 10 test.sleep's of 10 seconds, and insure it only takes roughly 10s
- """
- self.write_conf(
- {"fileserver_backend": ["roots"], "file_roots": {"base": [self.base_env]}}
- )
- orch_sls = os.path.join(self.base_env, "two_stage_orch_kill.sls")
- with salt.utils.files.fopen(orch_sls, "w") as fp_:
- fp_.write(
- textwrap.dedent(
- """
- stage_one:
- test.succeed_without_changes
- stage_two:
- test.fail_without_changes
- """
- )
- )
- listener = salt.utils.event.get_event(
- "master",
- sock_dir=self.master_opts["sock_dir"],
- transport=self.master_opts["transport"],
- opts=self.master_opts,
- )
- mock_jid = "20131219120000000000"
- self.run_run("state.soft_kill {0} stage_two".format(mock_jid))
- with patch("salt.utils.jid.gen_jid", MagicMock(return_value=mock_jid)):
- jid = self.run_run_plus(
- "state.orchestrate", "two_stage_orch_kill", __reload_config=True
- ).get("jid")
- if jid is None:
- raise Exception("jid missing from run_run_plus output")
- signal.signal(signal.SIGALRM, self.alarm_handler)
- signal.alarm(self.timeout)
- received = False
- try:
- while True:
- event = listener.get_event(full=True)
- if event is None:
- continue
- # Ensure that stage_two of the state does not run
- if event["tag"] == "salt/run/{0}/ret".format(jid):
- received = True
- # Don't wrap this in a try/except. We want to know if the
- # data structure is different from what we expect!
- ret = event["data"]["return"]["data"]["master"]
- self.assertNotIn(
- "test_|-stage_two_|-stage_two_|-fail_without_changes", ret
- )
- break
- finally:
- self.assertTrue(received)
- del listener
- signal.alarm(0)
- @skipIf(True, "SLOWTEST skip")
- def test_orchestration_with_pillar_dot_items(self):
- """
- Test to confirm when using a state file that includes other state file, if
- one of those state files includes pillar related functions that will not
- be pulling from the pillar cache that all the state files are available and
- the file_roots has been preserved. See issues #48277 and #46986.
- """
- self.write_conf(
- {"fileserver_backend": ["roots"], "file_roots": {"base": [self.base_env]}}
- )
- orch_sls = os.path.join(self.base_env, "main.sls")
- with salt.utils.files.fopen(orch_sls, "w") as fp_:
- fp_.write(
- textwrap.dedent(
- """
- include:
- - one
- - two
- - three
- """
- )
- )
- orch_sls = os.path.join(self.base_env, "one.sls")
- with salt.utils.files.fopen(orch_sls, "w") as fp_:
- fp_.write(
- textwrap.dedent(
- """
- {%- set foo = salt['saltutil.runner']('pillar.show_pillar') %}
- placeholder_one:
- test.succeed_without_changes
- """
- )
- )
- orch_sls = os.path.join(self.base_env, "two.sls")
- with salt.utils.files.fopen(orch_sls, "w") as fp_:
- fp_.write(
- textwrap.dedent(
- """
- placeholder_two:
- test.succeed_without_changes
- """
- )
- )
- orch_sls = os.path.join(self.base_env, "three.sls")
- with salt.utils.files.fopen(orch_sls, "w") as fp_:
- fp_.write(
- textwrap.dedent(
- """
- placeholder_three:
- test.succeed_without_changes
- """
- )
- )
- orch_sls = os.path.join(self.base_env, "main.sls")
- listener = salt.utils.event.get_event(
- "master",
- sock_dir=self.master_opts["sock_dir"],
- transport=self.master_opts["transport"],
- opts=self.master_opts,
- )
- jid = self.run_run_plus("state.orchestrate", "main", __reload_config=True).get(
- "jid"
- )
- if jid is None:
- raise salt.exceptions.SaltInvocationError(
- "jid missing from run_run_plus output"
- )
- signal.signal(signal.SIGALRM, self.alarm_handler)
- signal.alarm(self.timeout)
- received = False
- try:
- while True:
- event = listener.get_event(full=True)
- if event is None:
- continue
- if event.get("tag", "") == "salt/run/{0}/ret".format(jid):
- received = True
- # Don't wrap this in a try/except. We want to know if the
- # data structure is different from what we expect!
- ret = event["data"]["return"]["data"]["master"]
- for state in ret:
- data = ret[state]
- # Each state should be successful
- self.assertEqual(data["comment"], "Success!")
- break
- finally:
- self.assertTrue(received)
- del listener
- signal.alarm(0)
- @skipIf(True, "SLOWTEST skip")
- def test_orchestration_onchanges_and_prereq(self):
- """
- Test to confirm that the parallel state requisite works in orch
- we do this by running 10 test.sleep's of 10 seconds, and insure it only takes roughly 10s
- """
- self.write_conf(
- {"fileserver_backend": ["roots"], "file_roots": {"base": [self.base_env]}}
- )
- orch_sls = os.path.join(self.base_env, "orch.sls")
- with salt.utils.files.fopen(orch_sls, "w") as fp_:
- fp_.write(
- textwrap.dedent(
- """
- manage_a_file:
- salt.state:
- - tgt: minion
- - sls:
- - orch.req_test
- do_onchanges:
- salt.function:
- - tgt: minion
- - name: test.ping
- - onchanges:
- - salt: manage_a_file
- do_prereq:
- salt.function:
- - tgt: minion
- - name: test.ping
- - prereq:
- - salt: manage_a_file
- """
- )
- )
- listener = salt.utils.event.get_event(
- "master",
- sock_dir=self.master_opts["sock_dir"],
- transport=self.master_opts["transport"],
- opts=self.master_opts,
- )
- try:
- jid1 = self.run_run_plus(
- "state.orchestrate", "orch", test=True, __reload_config=True
- ).get("jid")
- # Run for real to create the file
- self.run_run_plus("state.orchestrate", "orch", __reload_config=True).get(
- "jid"
- )
- # Run again in test mode. Since there were no changes, the
- # requisites should not fire.
- jid2 = self.run_run_plus(
- "state.orchestrate", "orch", test=True, __reload_config=True
- ).get("jid")
- finally:
- try:
- os.remove(os.path.join(RUNTIME_VARS.TMP, "orch.req_test"))
- except OSError:
- pass
- assert jid1 is not None
- assert jid2 is not None
- tags = {"salt/run/{0}/ret".format(x): x for x in (jid1, jid2)}
- ret = {}
- signal.signal(signal.SIGALRM, self.alarm_handler)
- signal.alarm(self.timeout)
- try:
- while True:
- event = listener.get_event(full=True)
- if event is None:
- continue
- if event["tag"] in tags:
- ret[tags.pop(event["tag"])] = self.repack_state_returns(
- event["data"]["return"]["data"]["master"]
- )
- if not tags:
- # If tags is empty, we've grabbed all the returns we
- # wanted, so let's stop listening to the event bus.
- break
- finally:
- del listener
- signal.alarm(0)
- for sls_id in ("manage_a_file", "do_onchanges", "do_prereq"):
- # The first time through, all three states should have a None
- # result, while the second time through, they should all have a
- # True result.
- assert (
- ret[jid1][sls_id]["result"] is None
- ), "result of {0} ({1}) is not None".format(
- sls_id, ret[jid1][sls_id]["result"]
- )
- assert (
- ret[jid2][sls_id]["result"] is True
- ), "result of {0} ({1}) is not True".format(
- sls_id, ret[jid2][sls_id]["result"]
- )
- # The file.managed state should have shown changes in the test mode
- # return data.
- assert ret[jid1]["manage_a_file"]["changes"]
- # After the file was created, running again in test mode should have
- # shown no changes.
- assert not ret[jid2]["manage_a_file"]["changes"], ret[jid2]["manage_a_file"][
- "changes"
- ]
|