mirror of
https://github.com/saltstack/salt.git
synced 2025-04-17 10:10:20 +00:00
Add support for functional testing of state modules
This commit is contained in:
parent
a9dfc04a45
commit
a833c0f14c
3 changed files with 137 additions and 0 deletions
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
import shutil
|
||||
|
||||
import attr
|
||||
import pytest
|
||||
import salt.features
|
||||
import salt.loader
|
||||
|
@ -15,6 +16,7 @@ class Loaders:
|
|||
self.context = {}
|
||||
self._reset_state_funcs = [self.context.clear]
|
||||
self._grains = self._utils = self._modules = self._pillar = None
|
||||
self._serializers = self._states = None
|
||||
self.opts["grains"] = self.grains
|
||||
self.refresh_pillar()
|
||||
salt.features.setup_features(self.opts)
|
||||
|
@ -43,6 +45,49 @@ class Loaders:
|
|||
)
|
||||
return self._modules
|
||||
|
||||
@property
|
||||
def serializers(self):
|
||||
if self._serializers is None:
|
||||
self._serializers = salt.loader.serializers(self.opts)
|
||||
return self._serializers
|
||||
|
||||
@property
|
||||
def states(self):
|
||||
if self._states is None:
|
||||
_states = salt.loader.states(
|
||||
self.opts,
|
||||
functions=self.modules,
|
||||
utils=self.utils,
|
||||
serializers=self.serializers,
|
||||
context=self.context,
|
||||
)
|
||||
# For state execution modules, because we'd have to almost copy/paste what salt.modules.state.single
|
||||
# does, we actually "proxy" the call through salt.modules.state.single instead of calling the state
|
||||
# execution modules directly. This was also how the non pytest test suite worked
|
||||
# Let's load all modules now
|
||||
_states._load_all()
|
||||
# for funcname in list(_states):
|
||||
# _states[funcname]
|
||||
|
||||
# Now, we proxy loaded modules through salt.modules.state.single
|
||||
for module_name in list(_states.loaded_modules):
|
||||
for func_name in list(_states.loaded_modules[module_name]):
|
||||
full_func_name = "{}.{}".format(module_name, func_name)
|
||||
replacement_function = StateFunction(
|
||||
self.modules.state.single, full_func_name
|
||||
)
|
||||
_states._dict[full_func_name] = replacement_function
|
||||
_states.loaded_modules[module_name][
|
||||
func_name
|
||||
] = replacement_function
|
||||
setattr(
|
||||
_states.loaded_modules[module_name],
|
||||
func_name,
|
||||
replacement_function,
|
||||
)
|
||||
self._states = _states
|
||||
return self._states
|
||||
|
||||
@property
|
||||
def pillar(self):
|
||||
if self._pillar is None:
|
||||
|
@ -60,6 +105,92 @@ class Loaders:
|
|||
self.opts["pillar"] = self.pillar
|
||||
|
||||
|
||||
@attr.s
|
||||
class StateResult:
|
||||
raw = attr.ib()
|
||||
state_id = attr.ib(init=False)
|
||||
full_return = attr.ib(init=False)
|
||||
filtered = attr.ib(init=False)
|
||||
|
||||
@state_id.default
|
||||
def _state_id(self):
|
||||
return next(iter(self.raw.keys()))
|
||||
|
||||
@full_return.default
|
||||
def _full_return(self):
|
||||
return self.raw[self.state_id]
|
||||
|
||||
@filtered.default
|
||||
def _filtered_default(self):
|
||||
_filtered = {}
|
||||
for key, value in self.full_return.items():
|
||||
if key.startswith("_") or key in ("duration", "start_time"):
|
||||
continue
|
||||
_filtered[key] = value
|
||||
return _filtered
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self.full_return["name"]
|
||||
|
||||
@property
|
||||
def result(self):
|
||||
return self.full_return["result"]
|
||||
|
||||
@property
|
||||
def changes(self):
|
||||
return self.full_return["changes"]
|
||||
|
||||
@property
|
||||
def comment(self):
|
||||
return self.full_return["comment"]
|
||||
|
||||
def __eq__(self, value):
|
||||
raise RuntimeError(
|
||||
"Please assert comparissons with {}.filtered instead".format(
|
||||
self.__class__.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def __contains__(self, value):
|
||||
raise RuntimeError(
|
||||
"Please assert comparissons with {}.filtered instead".format(
|
||||
self.__class__.__name__
|
||||
)
|
||||
)
|
||||
|
||||
def __bool__(self):
|
||||
raise RuntimeError(
|
||||
"Please assert comparissons with {}.filtered instead".format(
|
||||
self.__class__.__name__
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@attr.s
|
||||
class StateFunction:
|
||||
proxy_func = attr.ib(repr=False)
|
||||
state_func = attr.ib()
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
name = None
|
||||
if args and len(args) == 1:
|
||||
name = args[0]
|
||||
if name is not None and "name" in kwargs:
|
||||
raise RuntimeError(
|
||||
"Either pass 'name' as the single argument to the call or remove 'name' as a keyword argument"
|
||||
)
|
||||
if name is None:
|
||||
name = kwargs.pop("name", None)
|
||||
if name is None:
|
||||
raise RuntimeError(
|
||||
"'name' was not passed as the single argument to the function nor as a keyword argument"
|
||||
)
|
||||
log.info("Calling state.single(%s, name=%s, %s)", self.state_func, name, kwargs)
|
||||
result = self.proxy_func(self.state_func, name=name, **kwargs)
|
||||
return StateResult(result)
|
||||
|
||||
|
||||
@pytest.fixture(scope="package")
|
||||
def minion_id():
|
||||
return "func-tests-minion"
|
||||
|
|
0
tests/pytests/functional/states/__init__.py
Normal file
0
tests/pytests/functional/states/__init__.py
Normal file
6
tests/pytests/functional/states/conftest.py
Normal file
6
tests/pytests/functional/states/conftest.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def states(loaders):
|
||||
return loaders.states
|
Loading…
Add table
Reference in a new issue