Using Salt Factories#
Salt factories simplifies testing Salt related code outside of Salt’s source tree. A great example is a salt-extension.
Let’s consider this echo-extension
example.
The echo-extension
provides an execution module:
__virtualname__ = "echo"
def __virtual__():
return __virtualname__
def text(string):
"""
This function just returns any text that it's given.
CLI Example:
.. code-block:: bash
salt '*' echo.text 'foo bar baz quo qux'
"""
return __salt__["test.echo"](string)
def reverse(string):
"""
This function just returns any text that it's given, reversed.
CLI Example:
.. code-block:: bash
salt '*' echo.reverse 'foo bar baz quo qux'
"""
return __salt__["test.echo"](string)[::-1]
And also a state module:
__virtualname__ = "echo"
def __virtual__():
if "echo.text" not in __salt__:
return False, "The 'echo' execution module is not available"
return __virtualname__
def echoed(name):
ret = {"name": name, "changes": {}, "result": False, "comment": ""}
value = __salt__["echo.text"](name)
if value == name:
ret["result"] = True
ret["comment"] = f"The 'echo.echoed' returned: '{value}'"
return ret
def reversed(name):
"""
This example function should be replaced
"""
ret = {"name": name, "changes": {}, "result": False, "comment": ""}
value = __salt__["echo.reverse"](name)
if value == name[::-1]:
ret["result"] = True
ret["comment"] = f"The 'echo.reversed' returned: '{value}'"
return ret
One could start off with something simple like unit testing the extension’s code.
Unit Tests#
import pytest
import salt.modules.test as testmod
import echoext.modules.echo_mod as echo_module
@pytest.fixture
def configure_loader_modules():
module_globals = {
"__salt__": {"test.echo": testmod.echo},
}
return {
echo_module: module_globals,
}
def test_text():
echo_str = "Echoed!"
assert echo_module.text(echo_str) == echo_str
def test_reverse():
echo_str = "Echoed!"
expected = echo_str[::-1]
assert echo_module.reverse(echo_str) == expected
import pytest
import salt.modules.test as testmod
import echoext.modules.echo_mod as echo_module
import echoext.states.echo_mod as echo_state
@pytest.fixture
def configure_loader_modules():
return {
echo_module: {
"__salt__": {
"test.echo": testmod.echo,
},
},
echo_state: {
"__salt__": {
"echo.text": echo_module.text,
"echo.reverse": echo_module.reverse,
},
},
}
def test_echoed():
echo_str = "Echoed!"
expected = {
"name": echo_str,
"changes": {},
"result": True,
"comment": f"The 'echo.echoed' returned: '{echo_str}'",
}
assert echo_state.echoed(echo_str) == expected
def test_reversed():
echo_str = "Echoed!"
expected_str = echo_str[::-1]
expected = {
"name": echo_str,
"changes": {},
"result": True,
"comment": f"The 'echo.reversed' returned: '{expected_str}'",
}
assert echo_state.reversed(echo_str) == expected
The magical piece of code in the above example is the configure_loader_modules fixture.
Integration Tests#
import os
import pytest
from echoext import PACKAGE_ROOT
from saltfactories.utils import random_string
@pytest.fixture(scope="session")
def salt_factories_config():
"""
Return a dictionary with the keyword arguments for FactoriesManager
"""
coverage_rc_path = os.environ.get("COVERAGE_PROCESS_START")
if coverage_rc_path:
coverage_db_path = PACKAGE_ROOT / ".coverage"
else:
coverage_db_path = None
return {
"code_dir": str(PACKAGE_ROOT),
"coverage_rc_path": coverage_rc_path,
"coverage_db_path": coverage_db_path,
"inject_sitecustomize": "COVERAGE_PROCESS_START" in os.environ,
"start_timeout": 120 if os.environ.get("CI") else 60,
}
@pytest.fixture(scope="package")
def master(salt_factories):
return salt_factories.salt_master_daemon(random_string("master-"))
@pytest.fixture(scope="package")
def minion(master):
return master.salt_minion_daemon(random_string("minion-"))
import pytest
@pytest.fixture(scope="package")
def master(master):
with master.started():
yield master
@pytest.fixture(scope="package")
def minion(minion):
with minion.started():
yield minion
@pytest.fixture
def salt_run_cli(master):
return master.salt_run_cli()
@pytest.fixture
def salt_cli(master):
return master.salt_cli()
@pytest.fixture
def salt_call_cli(minion):
return minion.salt_call_cli()
import pytest
pytestmark = [
pytest.mark.requires_salt_modules("echo.text"),
]
def test_text(salt_call_cli):
echo_str = "Echoed!"
ret = salt_call_cli.run("echo.text", echo_str)
assert ret.returncode == 0
assert ret.data
assert ret.data == echo_str
def test_reverse(salt_call_cli):
echo_str = "Echoed!"
expected = echo_str[::-1]
ret = salt_call_cli.run("echo.reverse", echo_str)
assert ret.returncode == 0
assert ret.data
assert ret.data == expected
import pytest
pytestmark = [
pytest.mark.requires_salt_states("echo.text"),
]
def test_echoed(salt_call_cli):
echo_str = "Echoed!"
ret = salt_call_cli.run("state.single", "echo.echoed", echo_str)
assert ret.returncode == 0
assert ret.data
assert ret.data == echo_str
def test_reversed(salt_call_cli):
echo_str = "Echoed!"
expected = echo_str[::-1]
ret = salt_call_cli.run("state.single", "echo.reversed", echo_str)
assert ret.returncode == 0
assert ret.data
assert ret.data == expected
What happened above?
We started a salt master
We started a salt minion
The minion connects to the master
The master accepted the minion’s key automatically
We pinged the minion
A litle suggestion
Not all tests should be integration tests, in fact, only a small set of the test suite should be an integration test.