Merge ~peppepetra/charm-sudo-pair:fix-tests into ~sudo-pair-charmers/charm-sudo-pair:master
- Git
- lp:~peppepetra/charm-sudo-pair
- fix-tests
- Merge into master
Proposed by
Giuseppe Petralia
Status: | Merged |
---|---|
Approved by: | Giuseppe Petralia |
Approved revision: | 51a606515c12e0087a05ebb350794530d4f51987 |
Merged at revision: | 51a606515c12e0087a05ebb350794530d4f51987 |
Proposed branch: | ~peppepetra/charm-sudo-pair:fix-tests |
Merge into: | ~sudo-pair-charmers/charm-sudo-pair:master |
Diff against target: |
841 lines (+214/-130) 12 files modified
.gitignore (+33/-7) Makefile (+25/-22) actions/actions.py (+2/-2) lib/libsudopair.py (+22/-2) reactive/sudo_pair.py (+1/-1) tests/functional/conftest.py (+23/-21) tests/functional/requirements.txt (+1/-0) tests/functional/test_deploy.py (+62/-51) tests/unit/conftest.py (+3/-2) tests/unit/requirements.txt (+1/-0) tests/unit/test_libsudopair.py (+18/-4) tox.ini (+23/-18) |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Alvaro Uria (community) | Approve | ||
Review via email:
|
Commit message
Description of the change
To post a comment you must log in.
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1 | diff --git a/.gitignore b/.gitignore |
2 | index a437a87..eda8bea 100644 |
3 | --- a/.gitignore |
4 | +++ b/.gitignore |
5 | @@ -1,7 +1,33 @@ |
6 | -__pycache__ |
7 | -*~ |
8 | -*.swp |
9 | -builds/ |
10 | -.idea |
11 | -.tox |
12 | -/repo-info |
13 | +# Byte-compiled / optimized / DLL files |
14 | +__pycache__/ |
15 | +*.py[cod] |
16 | +*$py.class |
17 | + |
18 | +# Log files |
19 | +*.log |
20 | + |
21 | +.tox/ |
22 | +src/.tox/ |
23 | +.coverage |
24 | + |
25 | +# vi |
26 | +.*.swp |
27 | + |
28 | +# pycharm |
29 | +.idea/ |
30 | +.unit-state.db |
31 | +src/.unit-state.db |
32 | + |
33 | +# version data |
34 | +repo-info |
35 | + |
36 | + |
37 | +# reports |
38 | +report/* |
39 | +src/report/* |
40 | + |
41 | +# virtual env |
42 | +venv/* |
43 | + |
44 | +# builds |
45 | +builds/* |
46 | \ No newline at end of file |
47 | diff --git a/Makefile b/Makefile |
48 | index 6c2a104..c7506b2 100644 |
49 | --- a/Makefile |
50 | +++ b/Makefile |
51 | @@ -1,7 +1,9 @@ |
52 | -PROJECTPATH = $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) |
53 | -ifndef JUJU_REPOSITORY |
54 | - JUJU_REPOSITORY := $(shell pwd) |
55 | - $(warning Warning JUJU_REPOSITORY was not set, defaulting to $(JUJU_REPOSITORY)) |
56 | +PROJECTPATH = $(dir $(realpath $(MAKEFILE_LIST))) |
57 | +DIRNAME = $(notdir $(PROJECTPATH:%/=%)) |
58 | + |
59 | +ifndef CHARM_BUILD_DIR |
60 | + CHARM_BUILD_DIR := /tmp/$(DIRNAME)-builds |
61 | + $(warning Warning CHARM_BUILD_DIR was not set, defaulting to $(CHARM_BUILD_DIR)) |
62 | endif |
63 | |
64 | help: |
65 | @@ -9,42 +11,43 @@ help: |
66 | @echo "" |
67 | @echo " make help - show this text" |
68 | @echo " make lint - run flake8" |
69 | - @echo " make test - run the unittests and lint" |
70 | - @echo " make unittest - run the tests defined in the unit subdirectory" |
71 | + @echo " make test - run the functional tests, unittests and lint" |
72 | + @echo " make unittest - run the tests defined in the unittest subdirectory" |
73 | @echo " make functional - run the tests defined in the functional subdirectory" |
74 | @echo " make release - build the charm" |
75 | @echo " make clean - remove unneeded files" |
76 | @echo "" |
77 | |
78 | -submodules: |
79 | - @echo "Cloning submodules" |
80 | - @git submodule update --init --recursive |
81 | - |
82 | lint: |
83 | @echo "Running flake8" |
84 | @tox -e lint |
85 | |
86 | -test: unittest functional lint |
87 | +test: lint unittest functional |
88 | + |
89 | +functional: build |
90 | + @PYTEST_KEEP_MODEL=$(PYTEST_KEEP_MODEL) \ |
91 | + PYTEST_CLOUD_NAME=$(PYTEST_CLOUD_NAME) \ |
92 | + PYTEST_CLOUD_REGION=$(PYTEST_CLOUD_REGION) \ |
93 | + tox -e functional |
94 | |
95 | unittest: |
96 | @tox -e unit |
97 | |
98 | -functional: build |
99 | - @tox -e functional |
100 | - |
101 | build: |
102 | - @echo "Building charm to base directory $(JUJU_REPOSITORY)" |
103 | - @-git describe --tags > $(PROJECTPATH)/repo-info |
104 | - @LAYER_PATH=./layers INTERFACE_PATH=./interfaces\ |
105 | - JUJU_REPOSITORY=$(JUJU_REPOSITORY) charm build ./ --force |
106 | + @echo "Building charm to base directory $(CHARM_BUILD_DIR)" |
107 | + @-git describe --tags > ./repo-info |
108 | + @CHARM_LAYERS_DIR=./layers CHARM_INTERFACES_DIR=./interfaces TERM=linux\ |
109 | + charm build --output-dir $(CHARM_BUILD_DIR) $(PROJECTPATH) --force |
110 | |
111 | release: clean build |
112 | - @echo "Charm is built at $(JUJU_REPOSITORY)/builds" |
113 | + @echo "Charm is built at $(CHARM_BUILD_DIR)/builds" |
114 | |
115 | clean: |
116 | @echo "Cleaning files" |
117 | - @rm -rf $(PROJECTPATH)/.tox |
118 | - @rm -rf $(PROJECTPATH)/.pytest_cache |
119 | + @find $(PROJECTPATH) -iname __pycache__ -exec rm -r {} + |
120 | + @if [ -d $(CHARM_BUILD_DIR)/builds ] ; then rm -r $(CHARM_BUILD_DIR)/builds ; fi |
121 | + @if [ -d $(PROJECTPATH)/.tox ] ; then rm -r $(PROJECTPATH)/.tox ; fi |
122 | + @if [ -d $(PROJECTPATH)/.pytest_cache ] ; then rm -r $(PROJECTPATH)/.pytest_cache ; fi |
123 | |
124 | # The targets below don't depend on a file |
125 | -.PHONY: lint test unittest functional build release clean help submodules |
126 | +.PHONY: lint test unittest functional build release clean help |
127 | diff --git a/actions/actions.py b/actions/actions.py |
128 | index 7dd03d8..ccf3ffc 100755 |
129 | --- a/actions/actions.py |
130 | +++ b/actions/actions.py |
131 | @@ -16,7 +16,7 @@ |
132 | import os |
133 | import sys |
134 | |
135 | -from charmhelpers.core.hookenv import action_set, action_fail |
136 | +from charmhelpers.core.hookenv import action_fail, action_set |
137 | |
138 | |
139 | sys.path.append("lib") |
140 | @@ -25,7 +25,7 @@ import libsudopair # NOQA |
141 | |
142 | |
143 | def remove(): |
144 | - """Action to remove sudo-pair config and binaries""" |
145 | + """Remove sudo-pair config and binaries.""" |
146 | sph = libsudopair.SudoPairHelper() |
147 | sph.deconfigure() |
148 | action_set({"message": "Successfully removed sudo-pair config and binaries"}) |
149 | diff --git a/lib/libsudopair.py b/lib/libsudopair.py |
150 | index 095e9a4..05d1f9d 100644 |
151 | --- a/lib/libsudopair.py |
152 | +++ b/lib/libsudopair.py |
153 | @@ -1,9 +1,11 @@ |
154 | import grp |
155 | import os |
156 | -from charmhelpers.core import host, hookenv, templating |
157 | + |
158 | +from charmhelpers.core import hookenv, host, templating |
159 | |
160 | |
161 | def check_valid_group(group_name): |
162 | + """Check that a group exists.""" |
163 | try: |
164 | grp.getgrnam(group_name) |
165 | return True |
166 | @@ -12,12 +14,14 @@ def check_valid_group(group_name): |
167 | |
168 | |
169 | def group_id(group_name): |
170 | + """Check that a group exists.""" |
171 | return grp.getgrnam(group_name).gr_gid |
172 | |
173 | |
174 | def group_names_to_group_ids(group_names): |
175 | """ |
176 | - From Group Names comma-separated list to Group Ids |
177 | + Return comma-separated list of Group Ids. |
178 | + |
179 | :param group_names: i.e. "root,user1,user2" |
180 | :return gids: i.e. "0,1001,1002" |
181 | """ |
182 | @@ -26,6 +30,7 @@ def group_names_to_group_ids(group_names): |
183 | |
184 | |
185 | def copy_file(source, destination, owner, group, perms): |
186 | + """Copy a file on the unit.""" |
187 | if destination is not None: |
188 | target_dir = os.path.dirname(destination) |
189 | if not os.path.exists(target_dir): |
190 | @@ -37,7 +42,10 @@ def copy_file(source, destination, owner, group, perms): |
191 | |
192 | |
193 | class SudoPairHelper(object): |
194 | + """Configure sudo-pair.""" |
195 | + |
196 | def __init__(self): |
197 | + """Retrieve charm config and set defaults.""" |
198 | self.charm_config = hookenv.config() |
199 | self.binary_path = '/usr/bin/sudo_approve' |
200 | self.sudo_conf_path = '/etc/sudo.conf' |
201 | @@ -58,6 +66,7 @@ class SudoPairHelper(object): |
202 | self.sudo_approve_perms = 0o755 |
203 | |
204 | def get_config(self): |
205 | + """Return config as a dict.""" |
206 | config = { |
207 | 'binary_path': self.binary_path, |
208 | 'user_prompt_path': self.user_prompt_path, |
209 | @@ -71,41 +80,51 @@ class SudoPairHelper(object): |
210 | return config |
211 | |
212 | def set_charm_config(self, charm_config): |
213 | + """Update configuration.""" |
214 | self.charm_config = charm_config |
215 | |
216 | def render_sudo_conf(self): |
217 | + """Render sudo.conf file.""" |
218 | return templating.render('sudo.conf.tmpl', self.sudo_conf_path, self.get_config(), |
219 | perms=self.sudo_conf_perms, owner=self.owner, group=self.group) |
220 | |
221 | def create_socket_dir(self): |
222 | + """Create socket dir.""" |
223 | host.mkdir(self.socket_dir, perms=self.socket_dir_perms, owner=self.owner, group=self.group) |
224 | |
225 | def create_tmpfiles_conf(self): |
226 | + """Create temporary conf file.""" |
227 | with open(self.tmpfiles_conf, "w") as f: |
228 | f.write("d {} 0755 - - -\n".format(self.socket_dir)) |
229 | |
230 | def install_sudo_pair_so(self): |
231 | + """Install sudo-pair lib.""" |
232 | sudo_pair_lib = os.path.join(hookenv.charm_dir(), 'files', 'sudo_pair.so') |
233 | copy_file(sudo_pair_lib, self.sudo_lib_path, self.owner, self.group, self.sudo_pair_so_perms) |
234 | |
235 | def copy_user_prompt(self): |
236 | + """Copy user prompt on the unit.""" |
237 | prompt_file = os.path.join(hookenv.charm_dir(), 'files', 'sudo.prompt.user') |
238 | copy_file(prompt_file, self.user_prompt_path, self.owner, self.group, self.prompt_perms) |
239 | |
240 | def copy_pair_prompt(self): |
241 | + """Copy pair prompt on the unit.""" |
242 | prompt_file = os.path.join(hookenv.charm_dir(), 'files', 'sudo.prompt.pair') |
243 | copy_file(prompt_file, self.pair_prompt_path, self.owner, self.group, self.prompt_perms) |
244 | |
245 | def copy_sudoers(self): |
246 | + """Copy sudoers file on the unit.""" |
247 | sudoers_file = os.path.join(hookenv.charm_dir(), 'files', 'sudoers') |
248 | copy_file(sudoers_file, self.sudoers_path, self.owner, self.group, self.sudoers_perms) |
249 | |
250 | def render_sudo_approve(self): |
251 | + """Render sudo-approve file.""" |
252 | hookenv.log("Rendering sudo_approve.tmpl to {}".format(self.binary_path)) |
253 | return templating.render('sudo_approve.tmpl', self.binary_path, self.get_config(), |
254 | perms=self.sudo_approve_perms, owner=self.owner, group=self.group) |
255 | |
256 | def render_bypass_cmds(self): |
257 | + """Render bypass command file.""" |
258 | if self.get_config()['bypass_cmds'] != "" and self.get_config()['bypass_group'] != "": |
259 | hookenv.log("Render bypass cmds to {}".format(self.sudoers_bypass_path)) |
260 | return templating.render('91-bypass-sudopair-cmds.tmpl', self.sudoers_bypass_path, |
261 | @@ -113,6 +132,7 @@ class SudoPairHelper(object): |
262 | return None |
263 | |
264 | def deconfigure(self): |
265 | + """Remove sudo-pair configuration.""" |
266 | paths = [ |
267 | self.sudo_conf_path, |
268 | self.sudo_lib_path, |
269 | diff --git a/reactive/sudo_pair.py b/reactive/sudo_pair.py |
270 | index be557f8..974dd66 100644 |
271 | --- a/reactive/sudo_pair.py |
272 | +++ b/reactive/sudo_pair.py |
273 | @@ -1,6 +1,6 @@ |
274 | -from charms.reactive import when, when_not, set_state, remove_state, hook |
275 | from charmhelpers.core import hookenv |
276 | |
277 | +from charms.reactive import hook, remove_state, set_state, when, when_not |
278 | |
279 | from libsudopair import SudoPairHelper |
280 | |
281 | diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py |
282 | index 7279748..aa42a09 100644 |
283 | --- a/tests/functional/conftest.py |
284 | +++ b/tests/functional/conftest.py |
285 | @@ -2,21 +2,23 @@ |
286 | |
287 | import asyncio |
288 | import json |
289 | -import juju |
290 | import os |
291 | -import pytest |
292 | import uuid |
293 | + |
294 | +import juju |
295 | from juju.controller import Controller |
296 | from juju.errors import JujuError |
297 | from juju.model import Model |
298 | |
299 | +import pytest |
300 | + |
301 | + |
302 | STAT_FILE = "python3 -c \"import json; import os; s=os.stat('%s'); print(json.dumps({'uid': s.st_uid, 'gid': s.st_gid, 'mode': oct(s.st_mode), 'size': s.st_size}))\"" # noqa: E501 |
303 | |
304 | |
305 | @pytest.yield_fixture(scope='module') |
306 | def event_loop(request): |
307 | - '''Override the default pytest event loop to allow for broaded scoped |
308 | - fixtures''' |
309 | + """Override the default pytest event loop to allow for broaded scopedv fixtures.""" |
310 | loop = asyncio.get_event_loop_policy().new_event_loop() |
311 | asyncio.set_event_loop(loop) |
312 | loop.set_debug(True) |
313 | @@ -27,7 +29,7 @@ def event_loop(request): |
314 | |
315 | @pytest.fixture(scope='module') |
316 | async def controller(): |
317 | - '''Connect to the current controller''' |
318 | + """Connect to the current controller.""" |
319 | controller = Controller() |
320 | await controller.connect_current() |
321 | yield controller |
322 | @@ -36,7 +38,7 @@ async def controller(): |
323 | |
324 | @pytest.fixture(scope='module') |
325 | async def model(controller): |
326 | - '''This model lives only for the duration of the test''' |
327 | + """Create a model that lives only for the duration of the test.""" |
328 | model_name = "functest-{}".format(uuid.uuid4()) |
329 | model = await controller.add_model(model_name) |
330 | yield model |
331 | @@ -50,7 +52,7 @@ async def model(controller): |
332 | |
333 | @pytest.fixture(scope='module') |
334 | async def current_model(): |
335 | - '''Returns the current model, does not create or destroy it''' |
336 | + """Return the current model, does not create or destroy it.""" |
337 | model = Model() |
338 | await model.connect_current() |
339 | yield model |
340 | @@ -59,7 +61,7 @@ async def current_model(): |
341 | |
342 | @pytest.fixture |
343 | async def get_app(model): |
344 | - '''Returns the application requested''' |
345 | + """Return the application requested.""" |
346 | async def _get_app(name): |
347 | try: |
348 | return model.applications[name] |
349 | @@ -70,7 +72,7 @@ async def get_app(model): |
350 | |
351 | @pytest.fixture |
352 | async def get_unit(model): |
353 | - '''Returns the requested <app_name>/<unit_number> unit''' |
354 | + """Return the requested <app_name>/<unit_number> unit.""" |
355 | async def _get_unit(name): |
356 | try: |
357 | (app_name, unit_number) = name.split('/') |
358 | @@ -82,7 +84,7 @@ async def get_unit(model): |
359 | |
360 | @pytest.fixture |
361 | async def get_entity(model, get_unit, get_app): |
362 | - '''Returns a unit or an application''' |
363 | + """Return a unit or an application.""" |
364 | async def _get_entity(name): |
365 | try: |
366 | return await get_unit(name) |
367 | @@ -96,12 +98,12 @@ async def get_entity(model, get_unit, get_app): |
368 | |
369 | @pytest.fixture |
370 | async def run_command(get_unit): |
371 | - ''' |
372 | - Runs a command on a unit. |
373 | + """ |
374 | + Run a command on a unit. |
375 | |
376 | :param cmd: Command to be run |
377 | :param target: Unit object or unit name string |
378 | - ''' |
379 | + """ |
380 | async def _run_command(cmd, target): |
381 | unit = ( |
382 | target |
383 | @@ -115,12 +117,12 @@ async def run_command(get_unit): |
384 | |
385 | @pytest.fixture |
386 | async def file_stat(run_command): |
387 | - ''' |
388 | - Runs stat on a file |
389 | + """ |
390 | + Run stat on a file. |
391 | |
392 | :param path: File path |
393 | :param target: Unit object or unit name string |
394 | - ''' |
395 | + """ |
396 | async def _file_stat(path, target): |
397 | cmd = STAT_FILE % path |
398 | results = await run_command(cmd, target) |
399 | @@ -130,12 +132,12 @@ async def file_stat(run_command): |
400 | |
401 | @pytest.fixture |
402 | async def file_contents(run_command): |
403 | - ''' |
404 | - Returns the contents of a file |
405 | + """ |
406 | + Return the contents of a file. |
407 | |
408 | :param path: File path |
409 | :param target: Unit object or unit name string |
410 | - ''' |
411 | + """ |
412 | async def _file_contents(path, target): |
413 | cmd = 'cat {}'.format(path) |
414 | results = await run_command(cmd, target) |
415 | @@ -145,7 +147,7 @@ async def file_contents(run_command): |
416 | |
417 | @pytest.fixture |
418 | async def reconfigure_app(get_app, model): |
419 | - '''Applies a different config to the requested app''' |
420 | + """Apply a different config to the requested app.""" |
421 | async def _reconfigure_app(cfg, target): |
422 | application = ( |
423 | target |
424 | @@ -160,7 +162,7 @@ async def reconfigure_app(get_app, model): |
425 | |
426 | @pytest.fixture |
427 | async def create_group(run_command): |
428 | - '''Creates the UNIX group specified''' |
429 | + """Create the UNIX group specified.""" |
430 | async def _create_group(group_name, target): |
431 | cmd = "sudo groupadd %s" % group_name |
432 | await run_command(cmd, target) |
433 | diff --git a/tests/functional/requirements.txt b/tests/functional/requirements.txt |
434 | index b9815c2..1da5a06 100644 |
435 | --- a/tests/functional/requirements.txt |
436 | +++ b/tests/functional/requirements.txt |
437 | @@ -2,5 +2,6 @@ juju |
438 | requests |
439 | pytest |
440 | pytest-asyncio |
441 | +pytest-cov |
442 | mock |
443 | flake8 |
444 | diff --git a/tests/functional/test_deploy.py b/tests/functional/test_deploy.py |
445 | index 5c8e690..cdb5ae4 100644 |
446 | --- a/tests/functional/test_deploy.py |
447 | +++ b/tests/functional/test_deploy.py |
448 | @@ -1,38 +1,67 @@ |
449 | #!/usr/bin/python3.6 |
450 | -import asyncio |
451 | + |
452 | import os |
453 | + |
454 | import pytest |
455 | |
456 | pytestmark = pytest.mark.asyncio |
457 | |
458 | +charm_build_dir = os.getenv('CHARM_BUILD_DIR', '..').rstrip('/') |
459 | + |
460 | + |
461 | +sources = [('local', '{}/builds/sudo-pair'.format(charm_build_dir))] |
462 | + |
463 | +series = ['xenial', |
464 | + 'bionic', |
465 | + ] |
466 | |
467 | -def get_series(): |
468 | - series = os.getenv('test_series', 'xenial bionic').strip() |
469 | - return series.split() |
470 | |
471 | ############ |
472 | # FIXTURES # |
473 | ############ |
474 | |
475 | |
476 | -# This fixture shouldn't really be in conftest.py since it's specific to this |
477 | -# charm |
478 | -@pytest.fixture(scope='module', |
479 | - params=get_series()) |
480 | -async def deploy_app(request, model): |
481 | - '''Deploys the sudo_pair app as a subordinate of ubuntu''' |
482 | - release = request.param |
483 | +@pytest.fixture(params=series) |
484 | +def series(request): |
485 | + """Return ubuntu version (i.e. xenial) in use in the test.""" |
486 | + return request.param |
487 | + |
488 | + |
489 | +@pytest.fixture(params=sources, ids=[s[0] for s in sources]) |
490 | +def source(request): |
491 | + """Return source of the charm under test (i.e. local, cs).""" |
492 | + return request.param |
493 | + |
494 | + |
495 | +@pytest.fixture |
496 | +async def app(model, series, source): |
497 | + """Return application of the charm under test.""" |
498 | + app_name = 'sudo-pair-{}'.format(series) |
499 | + return await model._wait_for_new('application', app_name) |
500 | + |
501 | + |
502 | +@pytest.fixture |
503 | +async def unit(app): |
504 | + """Return the sudo_pair unit we've deployed.""" |
505 | + return app.units[0] |
506 | + |
507 | + |
508 | +######### |
509 | +# TESTS # |
510 | +######### |
511 | |
512 | +async def test_deploy_app(model, series, source): |
513 | + """Deploy the sudo_pair app as a subordinate of ubuntu.""" |
514 | await model.deploy( |
515 | 'ubuntu', |
516 | - application_name='ubuntu-' + release, |
517 | - series=release, |
518 | + application_name='ubuntu-' + series, |
519 | + series=series, |
520 | channel='stable' |
521 | ) |
522 | sudo_pair_app = await model.deploy( |
523 | - '{}/builds/sudo-pair'.format(os.getenv('JUJU_REPOSITORY')), |
524 | - application_name='sudo-pair-' + release, |
525 | - series=release, |
526 | + source[1], |
527 | + application_name='sudo-pair-' + series, |
528 | + series=series, |
529 | num_units=0, |
530 | config={ |
531 | 'bypass_cmds': '/bin/ls', |
532 | @@ -41,27 +70,17 @@ async def deploy_app(request, model): |
533 | } |
534 | ) |
535 | await model.add_relation( |
536 | - 'ubuntu-{}:juju-info'.format(release), |
537 | - 'sudo-pair-{}:juju-info'.format(release)) |
538 | + 'ubuntu-{}:juju-info'.format(series), |
539 | + 'sudo-pair-{}:juju-info'.format(series)) |
540 | |
541 | await model.block_until(lambda: sudo_pair_app.status == 'active') |
542 | - yield sudo_pair_app |
543 | # no need to cleanup since the model will be be torn down at the end of the |
544 | # testing |
545 | |
546 | |
547 | -@pytest.fixture(scope='module') |
548 | -async def unit(deploy_app): |
549 | - '''Returns the sudo_pair unit we've deployed''' |
550 | - return deploy_app.units.pop() |
551 | - |
552 | -######### |
553 | -# TESTS # |
554 | -######### |
555 | - |
556 | - |
557 | -async def test_deploy(deploy_app): |
558 | - assert deploy_app.status == 'active' |
559 | +async def test_status(app): |
560 | + """Check that the app is in active state.""" |
561 | + assert app.status == 'active' |
562 | |
563 | |
564 | @pytest.mark.parametrize("path,expected_stat", [ |
565 | @@ -86,6 +105,7 @@ async def test_deploy(deploy_app): |
566 | 'uid': 0, |
567 | 'mode': '0o40644'})]) |
568 | async def test_stats(path, expected_stat, unit, file_stat): |
569 | + """Check that created files have the correct permissions.""" |
570 | test_stat = await file_stat(path, unit) |
571 | assert test_stat['size'] > 0 |
572 | assert test_stat['gid'] == expected_stat['gid'] |
573 | @@ -94,11 +114,13 @@ async def test_stats(path, expected_stat, unit, file_stat): |
574 | |
575 | |
576 | async def test_sudoers(file_contents, unit): |
577 | + """Check the content of sudoers file.""" |
578 | sudoers_content = await file_contents("/etc/sudoers", unit) |
579 | assert 'Defaults log_output' in sudoers_content |
580 | |
581 | |
582 | async def test_sudoers_bypass_conf(file_contents, unit): |
583 | + """Check the content of sudoers bypass command file.""" |
584 | path = "/etc/sudoers.d/91-bypass-sudopair-cmds" |
585 | sudoers_bypass_content = await file_contents(path=path, |
586 | target=unit) |
587 | @@ -106,41 +128,30 @@ async def test_sudoers_bypass_conf(file_contents, unit): |
588 | assert content in sudoers_bypass_content |
589 | |
590 | |
591 | -async def test_reconfigure(reconfigure_app, file_contents, unit, deploy_app): |
592 | - '''Change a charm config parameter and verify that it has been propagated to |
593 | - the unit''' |
594 | +async def test_reconfigure(reconfigure_app, file_contents, unit, app): |
595 | + """Change a charm config parameter and verify that it has been propagated to the unit.""" |
596 | sudo_approve_path = '/usr/bin/sudo_approve' |
597 | await reconfigure_app(cfg={'auto_approve': 'false'}, |
598 | - target=deploy_app) |
599 | + target=app) |
600 | sudo_approve_content = await file_contents(path=sudo_approve_path, |
601 | target=unit) |
602 | new_content = 'echo "You can\'t approve your own session."' |
603 | assert new_content in sudo_approve_content |
604 | |
605 | |
606 | -async def test_remove_relation(deploy_app, model, run_command): |
607 | - series = deploy_app.units[0].data['series'] |
608 | +async def test_remove_relation(app, model, run_command): |
609 | + """Check that the relation is removed.""" |
610 | + series = app.units[0].data['series'] |
611 | app_name = 'sudo-pair-{}'.format(series) |
612 | principalname = 'ubuntu-{}'.format(series) |
613 | - await deploy_app.remove_relation( |
614 | + await app.remove_relation( |
615 | '{}:juju-info'.format(app_name), |
616 | '{}:juju-info'.format(principalname)) |
617 | - await model.block_until(lambda: not deploy_app.relations) |
618 | + await model.block_until(lambda: not app.relations) |
619 | principal = model.applications[principalname].units[0] |
620 | res = await run_command('test -f /etc/sudo.conf || echo gone', target=principal) |
621 | assert res['Stdout'].strip() == 'gone' |
622 | await model.add_relation( |
623 | '{}:juju-info'.format(principalname), |
624 | '{}:juju-info'.format(app_name)) |
625 | - await model.block_until(lambda: deploy_app.relations) |
626 | - |
627 | - |
628 | -async def test_remove_unit(deploy_app, model, run_command): |
629 | - series = deploy_app.units[0].data['series'] |
630 | - app_name = 'sudo-pair-{}'.format(series) |
631 | - await deploy_app.destroy() |
632 | - while app_name in model.applications: |
633 | - await asyncio.sleep(2) |
634 | - principal = model.applications['ubuntu-{}'.format(series)].units[0] |
635 | - res = await run_command('test -f /etc/sudo.conf || echo gone', target=principal) |
636 | - assert res['Stdout'].strip() == 'gone' |
637 | + await model.block_until(lambda: app.relations) |
638 | diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py |
639 | index 0e2c034..c0800ea 100644 |
640 | --- a/tests/unit/conftest.py |
641 | +++ b/tests/unit/conftest.py |
642 | @@ -1,11 +1,12 @@ |
643 | #!/usr/bin/python3 |
644 | |
645 | -import pytest |
646 | -import pwd |
647 | import grp |
648 | import os |
649 | +import pwd |
650 | from pathlib import Path |
651 | |
652 | +import pytest |
653 | + |
654 | |
655 | @pytest.fixture |
656 | def mock_hookenv_config(monkeypatch): |
657 | diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt |
658 | index 081ed97..04dbf13 100644 |
659 | --- a/tests/unit/requirements.txt |
660 | +++ b/tests/unit/requirements.txt |
661 | @@ -2,3 +2,4 @@ charmhelpers |
662 | charms.reactive |
663 | pytest |
664 | mock |
665 | +pytest-cov |
666 | \ No newline at end of file |
667 | diff --git a/tests/unit/test_libsudopair.py b/tests/unit/test_libsudopair.py |
668 | index fea4985..3a598af 100644 |
669 | --- a/tests/unit/test_libsudopair.py |
670 | +++ b/tests/unit/test_libsudopair.py |
671 | @@ -1,6 +1,6 @@ |
672 | -import os |
673 | -import grp |
674 | import filecmp |
675 | +import grp |
676 | +import os |
677 | |
678 | from libsudopair import ( |
679 | check_valid_group, |
680 | @@ -18,14 +18,18 @@ def test_group_id(): |
681 | |
682 | |
683 | class TestSudoPairHelper(): |
684 | + """Module to test SudoPairHelper lib.""" |
685 | + |
686 | def test_pytest(self): |
687 | + """Assert testing is carryied using pytest.""" |
688 | assert True |
689 | |
690 | def test_sph(self, sph): |
691 | - ''' See if the ph fixture works to load charm configs ''' |
692 | + """See if the sph fixture works to load charm configs.""" |
693 | assert isinstance(sph.charm_config, dict) |
694 | |
695 | def test_get_config(self, sph): |
696 | + """Check if config contains all the required entries.""" |
697 | default_keywords = [ |
698 | 'binary_path', |
699 | 'user_prompt_path', |
700 | @@ -39,6 +43,7 @@ class TestSudoPairHelper(): |
701 | assert option in config |
702 | |
703 | def test_set_charm_config(self, sph): |
704 | + """Set new config.""" |
705 | charm_config = { |
706 | 'groups_enforced': 'root', |
707 | 'groups_exempted': '', |
708 | @@ -54,6 +59,7 @@ class TestSudoPairHelper(): |
709 | assert sph.get_config()[option] == charm_config[option] |
710 | |
711 | def test_render_sudo_conf(self, sph, tmpdir): |
712 | + """Check that sudo.conf is rendered correctly.""" |
713 | # Default config |
714 | content = sph.render_sudo_conf() |
715 | expected_content = 'Plugin sudo_pair sudo_pair.so binary_path={} ' \ |
716 | @@ -107,7 +113,8 @@ class TestSudoPairHelper(): |
717 | content = sph.render_sudo_conf() |
718 | assert expected_content in content |
719 | |
720 | - def test_render_bypass_cmds(self, sph, tmpdir): |
721 | + def test_render_bypass_cmds(self, sph): |
722 | + """Check that sudoers file is rendered correctly.""" |
723 | # Root bypass /bin/ls |
724 | expected_content = '%root ALL = (ALL) NOLOG_OUTPUT: /bin/ls' |
725 | charm_config = { |
726 | @@ -122,6 +129,7 @@ class TestSudoPairHelper(): |
727 | assert expected_content in content |
728 | |
729 | def test_render_sudo_approve(self, sph, tmpdir): |
730 | + """Check that sudo_approve file is rendered correctly.""" |
731 | # Auto Approve true |
732 | expected_content = 'echo ${log_line} >> /var/log/sudo_pair.log' |
733 | socket_dir = tmpdir.join('/var/run/sudo_pair') |
734 | @@ -144,10 +152,12 @@ class TestSudoPairHelper(): |
735 | assert expected_content in content |
736 | |
737 | def test_create_socket_dir(self, sph, tmpdir): |
738 | + """Check that sudo_pair socket dir exists.""" |
739 | sph.create_socket_dir() |
740 | assert os.path.exists(tmpdir.join('/var/run/sudo_pair')) |
741 | |
742 | def test_create_tmpfiles_conf(self, sph, tmpdir): |
743 | + """Check that sudo pair temporary conf is rendered correctly.""" |
744 | sph.create_tmpfiles_conf() |
745 | expected_content = 'd {} 0755 - - -\n'.format(sph.socket_dir) |
746 | with open(tmpdir.join('/usr/lib/tmpfiles.d/sudo_pair.conf')) as f: |
747 | @@ -155,17 +165,21 @@ class TestSudoPairHelper(): |
748 | assert expected_content in content |
749 | |
750 | def test_install_sudo_pair_so(self, sph, tmpdir): |
751 | + """Check that sudo system lib exists.""" |
752 | sph.install_sudo_pair_so() |
753 | assert filecmp.cmp('./files/sudo_pair.so', tmpdir.join('/usr/lib/sudo/sudo_pair.so')) |
754 | |
755 | def test_copy_user_prompt(self, sph, tmpdir): |
756 | + """Check that user prompt exists.""" |
757 | sph.copy_user_prompt() |
758 | assert filecmp.cmp('./files/sudo.prompt.user', tmpdir.join('/etc/sudo_pair.prompt.user')) |
759 | |
760 | def test_copy_pair_prompt(self, sph, tmpdir): |
761 | + """Check that pair prompt exists.""" |
762 | sph.copy_pair_prompt() |
763 | assert filecmp.cmp('./files/sudo.prompt.pair', tmpdir.join('/etc/sudo_pair.prompt.pair')) |
764 | |
765 | def test_copy_sudoers(self, sph, tmpdir): |
766 | + """Check that sudoers file exists.""" |
767 | sph.copy_sudoers() |
768 | assert filecmp.cmp('./files/sudoers', tmpdir.join('/etc/sudoers')) |
769 | diff --git a/tox.ini b/tox.ini |
770 | index 562d0ba..fc364de 100644 |
771 | --- a/tox.ini |
772 | +++ b/tox.ini |
773 | @@ -1,6 +1,6 @@ |
774 | [tox] |
775 | skipsdist=True |
776 | -envlist = unit, functional, func_xenial |
777 | +envlist = unit, functional |
778 | skip_missing_interpreters = True |
779 | |
780 | [testenv] |
781 | @@ -9,38 +9,43 @@ setenv = |
782 | PYTHONPATH = . |
783 | |
784 | [testenv:unit] |
785 | -commands = pytest -v --ignore {toxinidir}/tests/amulet --ignore {toxinidir}/tests/functional |
786 | +commands = pytest -v --ignore {toxinidir}/tests/functional \ |
787 | + --cov=lib \ |
788 | + --cov=reactive \ |
789 | + --cov=actions \ |
790 | + --cov-report=term \ |
791 | + --cov-report=annotate:report/annotated \ |
792 | + --cov-report=html:report/html |
793 | deps = -r{toxinidir}/tests/unit/requirements.txt |
794 | + |
795 | setenv = PYTHONPATH={toxinidir}/lib |
796 | |
797 | [testenv:functional] |
798 | passenv = |
799 | HOME |
800 | - JUJU_REPOSITORY |
801 | + CHARM_BUILD_DIR |
802 | PATH |
803 | -commands = pytest -v --ignore {toxinidir}/tests/unit --ignore {toxinidir}/tests/amulet |
804 | + PYTEST_KEEP_MODEL |
805 | + PYTEST_CLOUD_NAME |
806 | + PYTEST_CLOUD_REGION |
807 | +commands = pytest -v --ignore {toxinidir}/tests/unit |
808 | deps = -r{toxinidir}/tests/functional/requirements.txt |
809 | |
810 | |
811 | -[testenv:func_xenial] |
812 | -passenv = |
813 | - HOME |
814 | - JUJU_REPOSITORY |
815 | - PATH |
816 | - test_preserve_model |
817 | -setenv = test_series=xenial |
818 | -commands = pytest -v --ignore {toxinidir}/tests/unit --ignore {toxinidir}/tests/amulet |
819 | -deps = -r{toxinidir}/tests/functional/requirements.txt |
820 | - |
821 | [testenv:lint] |
822 | commands = flake8 |
823 | -deps = flake8 |
824 | +deps = |
825 | + flake8 |
826 | + flake8-docstrings |
827 | + flake8-import-order |
828 | + pep8-naming |
829 | + flake8-colors |
830 | |
831 | [flake8] |
832 | -max-line-length = 120 |
833 | -max-complexity=10 |
834 | -ignore = W503 |
835 | +ignore = D100,D103 # Missing docstring in public module/function |
836 | exclude = |
837 | .git, |
838 | __pycache__, |
839 | .tox, |
840 | +max-line-length = 120 |
841 | +max-complexity = 10 |
MP lgtm (only code review, I haven't run make...)
Added a comment inline to possibly remove "make submodules". Thank you.