Skip to content

Commit 4d25456

Browse files
Daniel A. Wozniakdwoz
authored andcommitted
test(resources): share top.sls + clear master state on minion teardown
Two interacting regressions in the test_dynamic_discovery and test_custom_pillar_key fixtures surfaced when running the resources smoke list end-to-end: 1. ``temp_file`` does not save/restore prior file contents — it just writes on enter and deletes on exit. The new module-scoped fixtures each wrote their own ``top.sls`` via ``temp_file``, which clobbered the package-scoped dummy ``top.sls``. When the module fixture exited, the file was removed and the outer dummy fixture had nothing to fall back on. Subsequent tests saw no pillar. Fix: a new ``_shared_pillar_top`` package-scoped fixture writes ``top.sls`` once with entries for every minion id the test package may use. Per-minion fixtures only contribute their own SLS file. Minions whose fixture isn't requested simply never start, so their top.sls entries are inert. 2. The dynamic and custom-key minions register resources with the master, then shut down at module teardown. Their ``resource_grains`` bank entries persist past shutdown because the registry is only cleaned by re-registration. A later test querying ``-G dummy_grain_1:one`` then sees stale ``dummy:custom-01`` / ``dummy:custom-02`` and waits for dead minions ("Minion did not return"). Fix: ``_clear_minion_resource_registration`` writes an empty pillar SLS, runs ``saltutil.refresh_pillar`` on the dying minion so its ``_register_resources_with_master`` ships an empty dict, then sleeps for the master to flush its bank entries. Wired into the ``salt_minion_dynamic`` and ``salt_minion_custom_pillar_key`` teardown ``finally`` blocks. Separately, fix ``test_pillar_addition_at_runtime_registers_new_resource`` to mutate ``dummy_resources.sls`` in place (read + write) instead of nesting ``temp_file``. The nested temp_file deleted the package fixture's file on inner-exit, leaving the primary minion with no pillar for the subsequent multi-minion test. After these fixes the resources smoke list runs clean: 264 unit passed, 28 skipped; 27 integration passed, 1 skipped.
1 parent fcf3395 commit 4d25456

2 files changed

Lines changed: 126 additions & 75 deletions

File tree

tests/pytests/integration/resources/conftest.py

Lines changed: 96 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,19 @@
2525

2626

2727
@pytest.fixture(scope="package")
28-
def pillar_tree_dummy_resources(salt_master):
28+
def _shared_pillar_top(salt_master):
2929
"""
30-
Pillar declaring ``resources.dummy.resource_ids`` for both potential
31-
test minions. The top file maps ``MINION_ID`` and (optionally)
32-
``MINION_ID_2``; minion 2's pillar entries are inert if
33-
``salt_minion_2`` is not requested by a test.
34-
35-
Resource discovery reads this tree via ``pillar_resources_tree``; the
36-
minion must not rely on a static ``resources:`` key in minion opts.
30+
Single shared ``top.sls`` listing every minion id the test package
31+
may use. Each minion's pillar SLS file is provided by its own
32+
fixture (``pillar_tree_dummy_resources``, ``..._dynamic_resources``,
33+
etc.); if a minion's SLS doesn't exist (because its fixture wasn't
34+
requested in this session) the top.sls entry is inert — the minion
35+
is never started so its pillar is never compiled.
36+
37+
Centralizing top.sls prevents multiple fixtures from clobbering
38+
each other via ``temp_file``: when one module-scoped fixture's
39+
temp_file exits it removes the top.sls; without this central
40+
fixture there is no replay to restore the package-level entries.
3741
"""
3842
top_file = textwrap.dedent(
3943
f"""
@@ -42,8 +46,23 @@ def pillar_tree_dummy_resources(salt_master):
4246
- dummy_resources
4347
'{MINION_ID_2}':
4448
- dummy_resources_2
49+
'{MINION_ID_DYN}':
50+
- dynamic_resources
51+
'{MINION_ID_CUSTOM_KEY}':
52+
- custom_key_resources
4553
"""
4654
)
55+
with salt_master.pillar_tree.base.temp_file("top.sls", top_file):
56+
yield
57+
58+
59+
@pytest.fixture(scope="package")
60+
def pillar_tree_dummy_resources(salt_master, _shared_pillar_top):
61+
"""
62+
Pillar SLS declaring ``resources.dummy.resource_ids`` for the
63+
primary and optional second dummy minions. Top.sls comes from
64+
:func:`_shared_pillar_top`.
65+
"""
4766
pillar_sls = textwrap.dedent(
4867
"""
4968
resources:
@@ -63,14 +82,13 @@ def pillar_tree_dummy_resources(salt_master):
6382
- dummy-05
6483
"""
6584
)
66-
top_tempfile = salt_master.pillar_tree.base.temp_file("top.sls", top_file)
6785
sls_tempfile = salt_master.pillar_tree.base.temp_file(
6886
"dummy_resources.sls", pillar_sls
6987
)
7088
sls_tempfile_2 = salt_master.pillar_tree.base.temp_file(
7189
"dummy_resources_2.sls", pillar_sls_2
7290
)
73-
with top_tempfile, sls_tempfile, sls_tempfile_2:
91+
with sls_tempfile, sls_tempfile_2:
7492
yield
7593

7694

@@ -235,34 +253,49 @@ def sync_resources_and_refresh(salt_call_cli, timeout=120):
235253
time.sleep(3)
236254

237255

238-
@pytest.fixture(scope="module")
239-
def pillar_tree_dynamic_resources(salt_master):
256+
def _clear_minion_resource_registration(
257+
salt_master, salt_call_cli, sls_name, empty_body, timeout=60
258+
):
259+
"""
260+
Force a minion to send an empty resource registration to the master.
261+
262+
Used in fixture teardown so a minion that's about to terminate
263+
doesn't leave stale ``resource_grains`` bank entries that confuse
264+
later tests' targeting. Writes a temp ``sls_name`` with an empty
265+
pillar body, runs ``saltutil.refresh_pillar`` on the minion so
266+
``_discover_resources`` returns nothing, and waits briefly for
267+
``_register_resources_with_master`` to land. Best-effort: failures
268+
are swallowed because the alternative is leaving a noisy traceback
269+
in the teardown of an otherwise green test.
240270
"""
241-
Pillar for the dynamic_test minion: enables the ``dynamic_test``
242-
resource type in the standard ``resources:`` tree (no ids declared
243-
there) and seeds ``_dynamic_test_ids`` at the top level — that's
244-
what ``dynamic_test.discover()`` reads.
271+
try:
272+
with salt_master.pillar_tree.base.temp_file(sls_name, empty_body):
273+
salt_call_cli.run("saltutil.refresh_pillar", wait=True, _timeout=timeout)
274+
time.sleep(3)
275+
except Exception: # pylint: disable=broad-except
276+
pass
245277

246-
Initial id list is empty; tests use ``temp_file`` to replace the
247-
seed SLS on disk and then call ``saltutil.refresh_pillar``.
278+
279+
@pytest.fixture(scope="module")
280+
def pillar_tree_dynamic_resources(salt_master, _shared_pillar_top):
281+
"""
282+
Pillar SLS for the dynamic_test minion: enables the
283+
``dynamic_test`` resource type in the standard ``resources:`` tree
284+
(no ids declared there) and seeds ``_dynamic_test_ids`` at the top
285+
level — that's what ``dynamic_test.discover()`` reads.
286+
287+
Top.sls comes from :func:`_shared_pillar_top`. Initial id list is
288+
empty; tests use ``write_dynamic_ids_pillar`` to rewrite the SLS
289+
and call ``saltutil.refresh_pillar``.
248290
"""
249-
top_file = textwrap.dedent(
250-
f"""
251-
base:
252-
'{MINION_ID_DYN}':
253-
- dynamic_resources
254-
"""
255-
)
256291
pillar_sls = textwrap.dedent(
257292
"""
258293
resources:
259294
dynamic_test: {}
260295
_dynamic_test_ids: []
261296
"""
262297
)
263-
with salt_master.pillar_tree.base.temp_file(
264-
"top.sls", top_file
265-
), salt_master.pillar_tree.base.temp_file("dynamic_resources.sls", pillar_sls):
298+
with salt_master.pillar_tree.base.temp_file("dynamic_resources.sls", pillar_sls):
266299
yield
267300

268301

@@ -287,21 +320,16 @@ def write_dynamic_ids_pillar(salt_master, ids):
287320

288321

289322
@pytest.fixture(scope="module")
290-
def pillar_tree_custom_key_resources(salt_master):
323+
def pillar_tree_custom_key_resources(salt_master, _shared_pillar_top):
291324
"""
292-
Pillar for the custom-key minion: declares the dummy resources
293-
under a NON-default top-level key (``salt_resources``). Also adds an
294-
empty ``resources:`` block (the framework's default key) so we can
295-
assert the minion ignores the default when configured to use the
296-
alternate key.
325+
Pillar SLS for the custom-key minion: declares dummy resources
326+
under a NON-default top-level key (``salt_resources``). Also adds
327+
an empty ``resources:`` block (the framework's default key) so we
328+
can assert the minion ignores the default when configured to use
329+
the alternate key.
330+
331+
Top.sls comes from :func:`_shared_pillar_top`.
297332
"""
298-
top_file = textwrap.dedent(
299-
f"""
300-
base:
301-
'{MINION_ID_CUSTOM_KEY}':
302-
- custom_key_resources
303-
"""
304-
)
305333
pillar_sls = textwrap.dedent(
306334
f"""
307335
resources: {{}}
@@ -312,9 +340,7 @@ def pillar_tree_custom_key_resources(salt_master):
312340
- {CUSTOM_KEY_DUMMY_RESOURCES[1]}
313341
"""
314342
)
315-
with salt_master.pillar_tree.base.temp_file(
316-
"top.sls", top_file
317-
), salt_master.pillar_tree.base.temp_file("custom_key_resources.sls", pillar_sls):
343+
with salt_master.pillar_tree.base.temp_file("custom_key_resources.sls", pillar_sls):
318344
yield
319345

320346

@@ -347,7 +373,20 @@ def salt_minion_custom_pillar_key(salt_master, pillar_tree_custom_key_resources)
347373
ret = salt_call_cli.run("saltutil.sync_all", _timeout=120)
348374
assert ret.returncode == 0, ret
349375
time.sleep(3)
350-
yield factory
376+
try:
377+
yield factory
378+
finally:
379+
# Clear the master's resource_grains bank of this minion's
380+
# entries BEFORE shutdown. Without this, the master keeps
381+
# ``dummy:custom-01`` / ``dummy:custom-02`` registered and
382+
# a later test querying ``-G dummy_grain_1:one`` will wait
383+
# for those (dead) resources to respond.
384+
_clear_minion_resource_registration(
385+
salt_master,
386+
salt_call_cli,
387+
"custom_key_resources.sls",
388+
f"resources: {{}}\n{CUSTOM_PILLAR_KEY}: {{}}\n",
389+
)
351390

352391

353392
@pytest.fixture(scope="module")
@@ -381,7 +420,18 @@ def salt_minion_dynamic(salt_master, pillar_tree_dynamic_resources, dynamic_test
381420
ret = salt_call_cli.run("saltutil.refresh_pillar", wait=True, _timeout=120)
382421
assert ret.returncode == 0, ret
383422
time.sleep(3)
384-
yield factory
423+
try:
424+
yield factory
425+
finally:
426+
# Same as the custom-key minion: clear master state of this
427+
# minion's dynamic_test resources before shutdown so later
428+
# tests don't see stale entries in the resource_grains bank.
429+
_clear_minion_resource_registration(
430+
salt_master,
431+
salt_call_cli,
432+
"dynamic_resources.sls",
433+
"resources: {}\n_dynamic_test_ids: []\n",
434+
)
385435

386436

387437
@pytest.fixture(scope="module")

tests/pytests/integration/resources/test_dummy_resource.py

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -359,16 +359,19 @@ def test_pillar_addition_at_runtime_registers_new_resource(
359359
and running ``saltutil.refresh_pillar`` must cause the master to
360360
pick the id up — without a minion restart.
361361
362-
Replaces the package-scoped ``dummy_resources.sls`` with an expanded
363-
version that adds ``dummy-extra`` to the existing list. The temp
364-
file's context-manager teardown restores the original pillar so the
365-
package-scoped fixture stays consistent for other tests.
362+
Mutates the package-scoped ``dummy_resources.sls`` in place rather
363+
than nesting :py:func:`temp_file` (which would delete the file on
364+
inner-exit and leave the package fixture without its pillar SLS).
365+
Restores the original content in the ``finally`` block.
366366
"""
367367
extra_id = "dummy-extra"
368368
expected = set(DUMMY_RESOURCES) | {extra_id}
369369

370+
sls_path = salt_master.pillar_tree.base.write_path / "dummy_resources.sls"
371+
original_body = sls_path.read_text()
372+
370373
augmented = textwrap.dedent(
371-
f"""
374+
f"""\
372375
resources:
373376
dummy:
374377
resource_ids:
@@ -381,31 +384,29 @@ def test_pillar_addition_at_runtime_registers_new_resource(
381384

382385
salt_run = salt_master.salt_run_cli(timeout=30)
383386
try:
384-
with salt_master.pillar_tree.base.temp_file("dummy_resources.sls", augmented):
385-
ret = salt_call_cli.run("saltutil.refresh_pillar", wait=True, _timeout=120)
386-
assert ret.returncode == 0, ret
387-
388-
deadline = time.monotonic() + 20
389-
seen = set()
390-
while time.monotonic() < deadline:
391-
ret = salt_run.run("resource.list_grains", _timeout=30)
392-
if ret.returncode == 0 and isinstance(ret.data, dict):
393-
seen = {
394-
srn.split(":", 1)[1]
395-
for srn in ret.data
396-
if srn.startswith("dummy:")
397-
}
398-
if seen >= expected:
399-
break
400-
time.sleep(1)
401-
assert seen >= expected, (
402-
f"Master never registered new dummy resource id {extra_id!r}. "
403-
f"Last saw: {seen}"
404-
)
387+
sls_path.write_text(augmented)
388+
ret = salt_call_cli.run("saltutil.refresh_pillar", wait=True, _timeout=120)
389+
assert ret.returncode == 0, ret
390+
391+
deadline = time.monotonic() + 20
392+
seen = set()
393+
while time.monotonic() < deadline:
394+
ret = salt_run.run("resource.list_grains", _timeout=30)
395+
if ret.returncode == 0 and isinstance(ret.data, dict):
396+
seen = {
397+
srn.split(":", 1)[1] for srn in ret.data if srn.startswith("dummy:")
398+
}
399+
if seen >= expected:
400+
break
401+
time.sleep(1)
402+
assert seen >= expected, (
403+
f"Master never registered new dummy resource id {extra_id!r}. "
404+
f"Last saw: {seen}"
405+
)
405406
finally:
406-
# Restore the package-scoped pillar so subsequent test ordering
407-
# is unaffected. The temp_file context already removed our
408-
# augmented version; this final refresh re-renders the original.
407+
# Restore the original SLS content + refresh so the master's view
408+
# converges back on the original 3 dummy ids for subsequent tests.
409+
sls_path.write_text(original_body)
409410
ret = salt_call_cli.run("saltutil.refresh_pillar", wait=True, _timeout=120)
410411
assert ret.returncode == 0, ret
411412
time.sleep(3)

0 commit comments

Comments
 (0)