Conversion of tests/unit/utils/test_reactor.py to pytest

This commit is contained in:
David Murphy 2025-02-25 08:53:44 -07:00 committed by Daniel Wozniak
parent 7fb8eb538b
commit aa981a52ca

View file

@ -0,0 +1,564 @@
import codecs
import glob
import logging
import os
import textwrap
import pytest
import salt.loader
import salt.template
import salt.utils.data
import salt.utils.files
import salt.utils.reactor as reactor
import salt.utils.yaml
from tests.support.mock import MagicMock, Mock, mock_open, patch
log = logging.getLogger(__name__)
REACTOR_CONFIG = """\
reactor:
- old_runner:
- /srv/reactor/old_runner.sls
- old_wheel:
- /srv/reactor/old_wheel.sls
- old_local:
- /srv/reactor/old_local.sls
- old_cmd:
- /srv/reactor/old_cmd.sls
- old_caller:
- /srv/reactor/old_caller.sls
- new_runner:
- /srv/reactor/new_runner.sls
- new_wheel:
- /srv/reactor/new_wheel.sls
- new_local:
- /srv/reactor/new_local.sls
- new_cmd:
- /srv/reactor/new_cmd.sls
- new_caller:
- /srv/reactor/new_caller.sls
"""
REACTOR_DATA = {
"runner": {"data": {"message": "This is an error"}},
"wheel": {"data": {"id": "foo"}},
"local": {"data": {"pkg": "zsh", "repo": "updates"}},
"cmd": {"data": {"pkg": "zsh", "repo": "updates"}},
"caller": {"data": {"path": "/tmp/foo"}},
}
SLS = {
"/srv/reactor/old_runner.sls": textwrap.dedent(
"""\
raise_error:
runner.error.error:
- name: Exception
- message: {{ data['data']['message'] }}
"""
),
"/srv/reactor/old_wheel.sls": textwrap.dedent(
"""\
remove_key:
wheel.key.delete:
- match: {{ data['data']['id'] }}
"""
),
"/srv/reactor/old_local.sls": textwrap.dedent(
"""\
install_zsh:
local.state.single:
- tgt: test
- arg:
- pkg.installed
- {{ data['data']['pkg'] }}
- kwarg:
fromrepo: {{ data['data']['repo'] }}
"""
),
"/srv/reactor/old_cmd.sls": textwrap.dedent(
"""\
install_zsh:
cmd.state.single:
- tgt: test
- arg:
- pkg.installed
- {{ data['data']['pkg'] }}
- kwarg:
fromrepo: {{ data['data']['repo'] }}
"""
),
"/srv/reactor/old_caller.sls": textwrap.dedent(
"""\
touch_file:
caller.file.touch:
- args:
- {{ data['data']['path'] }}
"""
),
"/srv/reactor/new_runner.sls": textwrap.dedent(
"""\
raise_error:
runner.error.error:
- args:
- name: Exception
- message: {{ data['data']['message'] }}
"""
),
"/srv/reactor/new_wheel.sls": textwrap.dedent(
"""\
remove_key:
wheel.key.delete:
- args:
- match: {{ data['data']['id'] }}
"""
),
"/srv/reactor/new_local.sls": textwrap.dedent(
"""\
install_zsh:
local.state.single:
- tgt: test
- args:
- fun: pkg.installed
- name: {{ data['data']['pkg'] }}
- fromrepo: {{ data['data']['repo'] }}
"""
),
"/srv/reactor/new_cmd.sls": textwrap.dedent(
"""\
install_zsh:
cmd.state.single:
- tgt: test
- args:
- fun: pkg.installed
- name: {{ data['data']['pkg'] }}
- fromrepo: {{ data['data']['repo'] }}
"""
),
"/srv/reactor/new_caller.sls": textwrap.dedent(
"""\
touch_file:
caller.file.touch:
- args:
- name: {{ data['data']['path'] }}
"""
),
}
LOW_CHUNKS = {
"old_runner": [
{
"state": "runner",
"__id__": "raise_error",
"__sls__": "/srv/reactor/old_runner.sls",
"order": 1,
"fun": "error.error",
"name": "Exception",
"message": "This is an error",
}
],
"old_wheel": [
{
"state": "wheel",
"__id__": "remove_key",
"name": "remove_key",
"__sls__": "/srv/reactor/old_wheel.sls",
"order": 1,
"fun": "key.delete",
"match": "foo",
}
],
"old_local": [
{
"state": "local",
"__id__": "install_zsh",
"name": "install_zsh",
"__sls__": "/srv/reactor/old_local.sls",
"order": 1,
"tgt": "test",
"fun": "state.single",
"arg": ["pkg.installed", "zsh"],
"kwarg": {"fromrepo": "updates"},
}
],
"old_cmd": [
{
"state": "local",
"__id__": "install_zsh",
"name": "install_zsh",
"__sls__": "/srv/reactor/old_cmd.sls",
"order": 1,
"tgt": "test",
"fun": "state.single",
"arg": ["pkg.installed", "zsh"],
"kwarg": {"fromrepo": "updates"},
}
],
"old_caller": [
{
"state": "caller",
"__id__": "touch_file",
"name": "touch_file",
"__sls__": "/srv/reactor/old_caller.sls",
"order": 1,
"fun": "file.touch",
"args": ["/tmp/foo"],
}
],
"new_runner": [
{
"state": "runner",
"__id__": "raise_error",
"name": "raise_error",
"__sls__": "/srv/reactor/new_runner.sls",
"order": 1,
"fun": "error.error",
"args": [{"name": "Exception"}, {"message": "This is an error"}],
}
],
"new_wheel": [
{
"state": "wheel",
"__id__": "remove_key",
"name": "remove_key",
"__sls__": "/srv/reactor/new_wheel.sls",
"order": 1,
"fun": "key.delete",
"args": [{"match": "foo"}],
}
],
"new_local": [
{
"state": "local",
"__id__": "install_zsh",
"name": "install_zsh",
"__sls__": "/srv/reactor/new_local.sls",
"order": 1,
"tgt": "test",
"fun": "state.single",
"args": [
{"fun": "pkg.installed"},
{"name": "zsh"},
{"fromrepo": "updates"},
],
}
],
"new_cmd": [
{
"state": "local",
"__id__": "install_zsh",
"name": "install_zsh",
"__sls__": "/srv/reactor/new_cmd.sls",
"order": 1,
"tgt": "test",
"fun": "state.single",
"args": [
{"fun": "pkg.installed"},
{"name": "zsh"},
{"fromrepo": "updates"},
],
}
],
"new_caller": [
{
"state": "caller",
"__id__": "touch_file",
"name": "touch_file",
"__sls__": "/srv/reactor/new_caller.sls",
"order": 1,
"fun": "file.touch",
"args": [{"name": "/tmp/foo"}],
}
],
}
WRAPPER_CALLS = {
"old_runner": (
"error.error",
{
"__state__": "runner",
"__id__": "raise_error",
"__sls__": "/srv/reactor/old_runner.sls",
"__user__": "Reactor",
"order": 1,
"arg": [],
"kwarg": {"name": "Exception", "message": "This is an error"},
"name": "Exception",
"message": "This is an error",
},
),
"old_wheel": (
"key.delete",
{
"__state__": "wheel",
"__id__": "remove_key",
"name": "remove_key",
"__sls__": "/srv/reactor/old_wheel.sls",
"order": 1,
"__user__": "Reactor",
"arg": ["foo"],
"kwarg": {},
"match": "foo",
},
),
"old_local": {
"args": ("test", "state.single"),
"kwargs": {
"state": "local",
"__id__": "install_zsh",
"name": "install_zsh",
"__sls__": "/srv/reactor/old_local.sls",
"order": 1,
"arg": ["pkg.installed", "zsh"],
"kwarg": {"fromrepo": "updates"},
},
},
"old_cmd": {
"args": ("test", "state.single"),
"kwargs": {
"state": "local",
"__id__": "install_zsh",
"name": "install_zsh",
"__sls__": "/srv/reactor/old_cmd.sls",
"order": 1,
"arg": ["pkg.installed", "zsh"],
"kwarg": {"fromrepo": "updates"},
},
},
"old_caller": {"args": ("file.touch", "/tmp/foo"), "kwargs": {}},
"new_runner": (
"error.error",
{
"__state__": "runner",
"__id__": "raise_error",
"name": "raise_error",
"__sls__": "/srv/reactor/new_runner.sls",
"__user__": "Reactor",
"order": 1,
"arg": (),
"kwarg": {"name": "Exception", "message": "This is an error"},
},
),
"new_wheel": (
"key.delete",
{
"__state__": "wheel",
"__id__": "remove_key",
"name": "remove_key",
"__sls__": "/srv/reactor/new_wheel.sls",
"order": 1,
"__user__": "Reactor",
"arg": (),
"kwarg": {"match": "foo"},
},
),
"new_local": {
"args": ("test", "state.single"),
"kwargs": {
"state": "local",
"__id__": "install_zsh",
"name": "install_zsh",
"__sls__": "/srv/reactor/new_local.sls",
"order": 1,
"arg": (),
"kwarg": {"fun": "pkg.installed", "name": "zsh", "fromrepo": "updates"},
},
},
"new_cmd": {
"args": ("test", "state.single"),
"kwargs": {
"state": "local",
"__id__": "install_zsh",
"name": "install_zsh",
"__sls__": "/srv/reactor/new_cmd.sls",
"order": 1,
"arg": (),
"kwarg": {"fun": "pkg.installed", "name": "zsh", "fromrepo": "updates"},
},
},
"new_caller": {"args": ("file.touch",), "kwargs": {"name": "/tmp/foo"}},
}
# -----------------------------------------------------------------------------
# FIXTURES
# -----------------------------------------------------------------------------
@pytest.fixture(scope="module")
def master_opts():
"""
In Salt's internal test suite, we often use something like
`AdaptedConfigurationTestCaseMixin.get_temp_config("master")`.
Replace this with however you retrieve your config opts in your own code/tests.
"""
opts = {
# Minimal stand-in for a real master config
"file_roots": {"base": []},
"renderer": "jinja|yaml",
}
# Optionally parse the reactor config for convenience
reactor_config = salt.utils.yaml.safe_load(REACTOR_CONFIG)
opts.update(reactor_config)
return opts
@pytest.fixture(scope="module")
def test_reactor(master_opts):
"""
Create a Reactor instance for testing
"""
return reactor.Reactor(master_opts)
@pytest.fixture(scope="module")
def reaction_map(master_opts):
"""
Reaction map from the configured reactor
"""
return salt.utils.data.repack_dictlist(master_opts["reactor"])
@pytest.fixture(scope="module")
def render_pipe(master_opts):
"""
Render pipeline
"""
renderers = salt.loader.render(master_opts, {})
return [(renderers[x], "") for x in ("jinja", "yaml")]
# -----------------------------------------------------------------------------
# TESTS for Reactor building the low chunks
# -----------------------------------------------------------------------------
@pytest.mark.parametrize("schema", ["old", "new"])
@pytest.mark.parametrize("rtype", list(REACTOR_DATA.keys()))
def test_reactor_reactions(schema, rtype, test_reactor, render_pipe):
"""
Ensure correct reactions are built from the configured SLS files and tag data.
"""
tag = f"{schema}_{rtype}"
reactors_list = test_reactor.list_reactors(tag)
# Patch out globbing since these SLS files don't actually exist on disk
with patch.object(glob, "glob", MagicMock(side_effect=lambda x: [x])):
with patch.object(os.path, "isfile", MagicMock(return_value=True)):
with patch.object(
salt.utils.files, "is_empty", MagicMock(return_value=False)
):
with patch.object(
codecs, "open", mock_open(read_data=SLS[reactors_list[0]])
):
with patch.object(
salt.template,
"template_shebang",
MagicMock(return_value=render_pipe),
):
reactions = test_reactor.reactions(
tag, REACTOR_DATA[rtype], reactors_list
)
assert reactions == LOW_CHUNKS[tag], f"Reactions did not match for tag: {tag}"
def test_list_reactors(test_reactor, reaction_map):
"""
Ensure list_reactors() returns the correct list of reactor SLS files for each tag.
"""
for schema in ("old", "new"):
for rtype in REACTOR_DATA:
tag = f"{schema}_{rtype}"
assert test_reactor.list_reactors(tag) == reaction_map[tag]
# -----------------------------------------------------------------------------
# FIXTURE for Reactor Wrap
# -----------------------------------------------------------------------------
@pytest.fixture(scope="module")
def react_wrap(master_opts):
"""
Create a ReactWrap instance
"""
return reactor.ReactWrap(master_opts)
# -----------------------------------------------------------------------------
# TESTS for ReactWrap
# -----------------------------------------------------------------------------
@pytest.mark.parametrize("schema", ["old", "new"])
def test_runner(schema, react_wrap):
"""
Test runner reactions using both the old and new config schema
"""
tag = f"{schema}_runner"
chunk = LOW_CHUNKS[tag][0]
thread_pool = Mock()
thread_pool.fire_async = Mock()
with patch.object(react_wrap, "pool", thread_pool):
react_wrap.run(chunk)
thread_pool.fire_async.assert_called_with(
react_wrap.client_cache["runner"].low,
args=WRAPPER_CALLS[tag],
)
@pytest.mark.parametrize("schema", ["old", "new"])
def test_wheel(schema, react_wrap):
"""
Test wheel reactions using both the old and new config schema
"""
tag = f"{schema}_wheel"
chunk = LOW_CHUNKS[tag][0]
thread_pool = Mock()
thread_pool.fire_async = Mock()
with patch.object(react_wrap, "pool", thread_pool):
react_wrap.run(chunk)
thread_pool.fire_async.assert_called_with(
react_wrap.client_cache["wheel"].low,
args=WRAPPER_CALLS[tag],
)
@pytest.mark.parametrize("schema", ["old", "new"])
def test_local(schema, react_wrap):
"""
Test local reactions using both the old and new config schema
"""
tag = f"{schema}_local"
chunk = LOW_CHUNKS[tag][0]
client_cache = {"local": Mock()}
client_cache["local"].cmd_async = Mock()
with patch.object(react_wrap, "client_cache", client_cache):
react_wrap.run(chunk)
client_cache["local"].cmd_async.assert_called_with(
*WRAPPER_CALLS[tag]["args"], **WRAPPER_CALLS[tag]["kwargs"]
)
@pytest.mark.parametrize("schema", ["old", "new"])
def test_cmd(schema, react_wrap):
"""
Test cmd reactions (alias for 'local') using both the old and new config schema
"""
tag = f"{schema}_cmd"
chunk = LOW_CHUNKS[tag][0]
client_cache = {"local": Mock()}
client_cache["local"].cmd_async = Mock()
with patch.object(react_wrap, "client_cache", client_cache):
react_wrap.run(chunk)
client_cache["local"].cmd_async.assert_called_with(
*WRAPPER_CALLS[tag]["args"], **WRAPPER_CALLS[tag]["kwargs"]
)
@pytest.mark.parametrize("schema", ["old", "new"])
def test_caller(schema, react_wrap):
"""
Test caller reactions using both the old and new config schema
"""
tag = f"{schema}_caller"
chunk = LOW_CHUNKS[tag][0]
client_cache = {"caller": Mock()}
client_cache["caller"].cmd = Mock()
with patch.object(react_wrap, "client_cache", client_cache):
react_wrap.run(chunk)
client_cache["caller"].cmd.assert_called_with(
*WRAPPER_CALLS[tag]["args"], **WRAPPER_CALLS[tag]["kwargs"]
)