1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
|
"""Global conftest.py
This conftest is used for unit tests in ``cloudinit/`` and ``tests/unittests/``
as well as the integration tests in ``tests/integration_tests/``.
Any imports that are performed at the top-level here must be installed wherever
any of these tests run: that is to say, they must be listed in
``integration-requirements.txt`` and in ``test-requirements.txt``.
"""
import os
from unittest import mock
import pytest
from cloudinit import helpers, subp, util
class _FixtureUtils:
"""A namespace for fixture helper functions, used by fixture_utils.
These helper functions are all defined as staticmethods so they are
effectively functions; they are defined in a class only to give us a
namespace so calling them can look like
``fixture_utils.fixture_util_function()`` in test code.
"""
@staticmethod
def closest_marker_args_or(request, marker_name: str, default):
"""Get the args for closest ``marker_name`` or return ``default``
:param request:
A pytest request, as passed to a fixture.
:param marker_name:
The name of the marker to look for
:param default:
The value to return if ``marker_name`` is not found.
:return:
The args for the closest ``marker_name`` marker, or ``default``
if no such marker is found.
"""
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
@staticmethod
def closest_marker_first_arg_or(request, marker_name: str, default):
"""Get the first arg for closest ``marker_name`` or return ``default``
This is a convenience wrapper around closest_marker_args_or, see there
for full details.
"""
result = _FixtureUtils.closest_marker_args_or(
request, marker_name, [default]
)
if not result:
raise TypeError(
"Missing expected argument to {} marker".format(marker_name)
)
return result[0]
@pytest.fixture(autouse=True)
def disable_subp_usage(request, fixture_utils):
"""
Across all (pytest) tests, ensure that subp.subp is not invoked.
Note that this can only catch invocations where the ``subp`` module is
imported and ``subp.subp(...)`` is called. ``from cloudinit.subp import
subp`` imports happen before the patching here (or the CiTestCase
monkey-patching) happens, so are left untouched.
While ``disable_subp_usage`` unconditionally patches
``cloudinit.subp.subp``, any test-local patching will override this
patching (i.e. the mock created for that patch call will replace the mock
created by ``disable_subp_usage``), allowing tests to be written normally.
One important exception: if ``autospec=True`` is passed to such an
overriding patch call it will fail: autospeccing introspects the object
being patched and as ``subp.subp`` will always be a mock when that
autospeccing happens, the introspection fails. (The specific error is:
``TypeError: name must be a str, not a MagicMock``.)
To allow a particular test method or class to use ``subp.subp`` you can
mark it as such::
@pytest.mark.allow_all_subp
def test_whoami(self):
subp.subp(["whoami"])
To instead allow ``subp.subp`` usage for a specific command, you can use
the ``allow_subp_for`` mark::
@pytest.mark.allow_subp_for("bash")
def test_bash(self):
subp.subp(["bash"])
You can pass multiple commands as values; they will all be permitted::
@pytest.mark.allow_subp_for("bash", "whoami")
def test_several_things(self):
subp.subp(["bash"])
subp.subp(["whoami"])
This fixture (roughly) mirrors the functionality of
``CiTestCase.allowed_subp``. N.B. While autouse fixtures do affect
non-pytest tests, CiTestCase's ``allowed_subp`` does take precedence (and
we have ``TestDisableSubpUsageInTestSubclass`` to confirm that).
"""
allow_subp_for = fixture_utils.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 = fixture_utils.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
@pytest.fixture(scope="session")
def fixture_utils():
"""Return a namespace containing fixture utility functions.
See :py:class:`_FixtureUtils` for further details."""
return _FixtureUtils
@pytest.fixture
def httpretty():
"""
Enable HTTPretty for duration of the testcase, resetting before and after.
This will also ensure allow_net_connect is set to False, and temporarily
unset http_proxy in os.environ if present (to work around
https://github.com/gabrielfalcao/HTTPretty/issues/122).
"""
import httpretty as _httpretty
restore_proxy = os.environ.pop("http_proxy", None)
_httpretty.HTTPretty.allow_net_connect = False
_httpretty.reset()
_httpretty.enable()
yield _httpretty
_httpretty.disable()
_httpretty.reset()
if restore_proxy is not None:
os.environ["http_proxy"] = restore_proxy
@pytest.fixture
def paths(tmpdir):
"""
Return a helpers.Paths object configured to use a tmpdir.
(This uses the builtin tmpdir fixture.)
"""
dirs = {
"cloud_dir": tmpdir.mkdir("cloud_dir").strpath,
"run_dir": tmpdir.mkdir("run_dir").strpath,
}
return helpers.Paths(dirs)
@pytest.fixture(autouse=True, scope="session")
def monkeypatch_system_info():
def my_system_info():
return {
"platform": "invalid",
"system": "invalid",
"release": "invalid",
"python": "invalid",
"uname": ["invalid"] * 6,
"dist": ("Distro", "-1.1", "Codename"),
"variant": "ubuntu",
}
util.system_info = my_system_info
|