Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions cloudinit/tests/test_conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ def test_typeerrors_on_incorrect_usage(self):
# pylint: disable=no-value-for-parameter
subp.subp()

@pytest.mark.parametrize('disable_subp_usage', [False], indirect=True)
@pytest.mark.allow_all_subp
def test_subp_usage_can_be_reenabled(self):
subp.subp(['whoami'])

@pytest.mark.parametrize(
'disable_subp_usage', [['whoami'], 'whoami'], indirect=True)
@pytest.mark.allow_subp_for("whoami")
def test_subp_usage_can_be_conditionally_reenabled(self):
# The two parameters test each potential invocation with a single
# argument
Expand All @@ -31,15 +30,20 @@ def test_subp_usage_can_be_conditionally_reenabled(self):
assert "allowed: whoami" in str(excinfo.value)
subp.subp(['whoami'])

@pytest.mark.parametrize(
'disable_subp_usage', [['whoami', 'bash']], indirect=True)
@pytest.mark.allow_subp_for("whoami", "bash")
def test_subp_usage_can_be_conditionally_reenabled_for_multiple_cmds(self):
with pytest.raises(AssertionError) as excinfo:
subp.subp(["some", "args"])
assert "allowed: whoami,bash" in str(excinfo.value)
subp.subp(['bash', '-c', 'true'])
subp.subp(['whoami'])

@pytest.mark.allow_all_subp
@pytest.mark.allow_subp_for("bash")
def test_both_marks_raise_an_error(self):
with pytest.raises(AssertionError, match="marked both"):
subp.subp(["bash"])


class TestDisableSubpUsageInTestSubclass(CiTestCase):
"""Test that disable_subp_usage doesn't impact CiTestCase's subp logic."""
Expand Down
93 changes: 58 additions & 35 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@
from cloudinit import subp


def _closest_marker_args_or(request, marker_name: str, default):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this supposed to be _closest_marker_args_or_default ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be happy to land such a change if you want, but (unsurprisingly, as I wrote it 😁) I think its intent is clear (and means that callers can generally call it on a single LOC; though that isn't much of an advantage if this spelling isn't clear!).

If we do want to change it, _get_closest_marker_args might be better, following the underlying API (and dict.get which takes a default)?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to change it. I think it's fine. I just wanted to make sure it was what you intended and not an accidental misnaming.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I would like to point out the irony of me pointing out a possible typo, while at the same time writing the code look a lot nice. 😄

"""Get the args for the closest ``marker_name`` or return ``default``"""
try:
marker = request.node.get_closest_marker(marker_name)
except AttributeError:
# Older versions of pytest don't have the new API
marker = request.node.get_marker(marker_name)
if marker is not None:
return marker.args
return default


@pytest.yield_fixture(autouse=True)
def disable_subp_usage(request):
"""
Expand All @@ -15,25 +27,23 @@ def disable_subp_usage(request):
imports happen before the patching here (or the CiTestCase monkey-patching)
happens, so are left untouched.

To allow a particular test method or class to use subp.subp you can set the
parameter passed to this fixture to False using pytest.mark.parametrize::
To allow a particular test method or class to use subp.subp you can mark it
as such::

@pytest.mark.parametrize("disable_subp_usage", [False], indirect=True)
@pytest.mark.allow_all_subp
def test_whoami(self):
subp.subp(["whoami"])

To instead allow subp.subp usage for a specific command, you can set the
parameter passed to this fixture to that command:
To instead allow subp.subp usage for a specific command, you can use the
``allow_subp_for`` mark::

@pytest.mark.parametrize("disable_subp_usage", ["bash"], indirect=True)
@pytest.mark.allow_subp_for("bash")
def test_bash(self):
subp.subp(["bash"])

To specify multiple commands, set the parameter to a list (note the
double-layered list: we specify a single parameter that is itself a list):
You can pass multiple commands as values; they will all be permitted::

@pytest.mark.parametrize(
"disable_subp_usage", ["bash", "whoami"], indirect=True)
@pytest.mark.allow_subp_for("bash", "whoami")
def test_several_things(self):
subp.subp(["bash"])
subp.subp(["whoami"])
Expand All @@ -43,30 +53,43 @@ def test_several_things(self):
tests, CiTestCase's allowed_subp does take precedence (and we have
TestDisableSubpUsageInTestSubclass to confirm that).
"""
should_disable = getattr(request, "param", True)
if should_disable:
if not isinstance(should_disable, (list, str)):
def side_effect(args, *other_args, **kwargs):
raise AssertionError("Unexpectedly used subp.subp")
else:
# Look this up before our patch is in place, so we have access to
# the real implementation in side_effect
real_subp = subp.subp

if isinstance(should_disable, str):
should_disable = [should_disable]

def side_effect(args, *other_args, **kwargs):
cmd = args[0]
if cmd not in should_disable:
raise AssertionError(
"Unexpectedly used subp.subp to call {} (allowed:"
" {})".format(cmd, ",".join(should_disable))
)
return real_subp(args, *other_args, **kwargs)

with mock.patch('cloudinit.subp.subp', autospec=True) as m_subp:
m_subp.side_effect = side_effect
yield
allow_subp_for = _closest_marker_args_or(request, "allow_subp_for", None)
# Because the mark doesn't take arguments, `allow_all_subp` will be set to
# [] if the marker is present, so explicit None checks are required
allow_all_subp = _closest_marker_args_or(request, "allow_all_subp", None)

if allow_all_subp is not None and allow_subp_for is None:
# Only allow_all_subp specified, don't mock subp.subp
yield
return

if allow_all_subp is None and allow_subp_for is None:
# No marks, default behaviour; disallow all subp.subp usage
def side_effect(args, *other_args, **kwargs):
raise AssertionError("Unexpectedly used subp.subp")

elif allow_all_subp is not None and allow_subp_for is not None:
# Both marks, ambiguous request; raise an exception on all subp usage
def side_effect(args, *other_args, **kwargs):
raise AssertionError(
"Test marked both allow_all_subp and allow_subp_for: resolve"
" this either by modifying your test code, or by modifying"
" disable_subp_usage to handle precedence."
)
else:
# Look this up before our patch is in place, so we have access to
# the real implementation in side_effect
real_subp = subp.subp

def side_effect(args, *other_args, **kwargs):
cmd = args[0]
if cmd not in allow_subp_for:
raise AssertionError(
"Unexpectedly used subp.subp to call {} (allowed:"
" {})".format(cmd, ",".join(allow_subp_for))
)
return real_subp(args, *other_args, **kwargs)

with mock.patch("cloudinit.subp.subp", autospec=True) as m_subp:
m_subp.side_effect = side_effect
yield
3 changes: 1 addition & 2 deletions tests/unittests/test_datasource/test_opennebula.py
Original file line number Diff line number Diff line change
Expand Up @@ -928,8 +928,7 @@ def test_multiple_nics(self):


class TestParseShellConfig:

@pytest.mark.parametrize('disable_subp_usage', ['bash'], indirect=True)
@pytest.mark.allow_subp_for("bash")
def test_no_seconds(self):
cfg = '\n'.join(["foo=bar", "SECONDS=2", "xx=foo"])
# we could test 'sleep 2', but that would make the test run slower.
Expand Down
2 changes: 1 addition & 1 deletion tests/unittests/test_render_cloudcfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"netbsd", "openbsd", "rhel", "suse", "ubuntu", "unknown"]


@pytest.mark.parametrize('disable_subp_usage', [sys.executable], indirect=True)
@pytest.mark.allow_subp_for(sys.executable)
class TestRenderCloudCfg:

cmd = [sys.executable, os.path.realpath('tools/render-cloudcfg')]
Expand Down
7 changes: 7 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,10 @@ commands = {envpython} -m tests.cloud_tests {posargs}
passenv = HOME TRAVIS
deps =
-r{toxinidir}/integration-requirements.txt

[pytest]
# TODO: s/--strict/--strict-markers/ once xenial support is dropped
addopts = --strict
markers =
allow_subp_for: allow subp usage for the given commands (disable_subp_usage)
allow_all_subp: allow all subp usage (disable_subp_usage)