Merge ~peppepetra/charm-sudo-pair:fix-tests into ~sudo-pair-charmers/charm-sudo-pair: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)
Reviewer Review Type Date Requested Status
Alvaro Uria (community) Approve
Review via email: mp+379381@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Alvaro Uria (aluria) wrote :

MP lgtm (only code review, I haven't run make...)
Added a comment inline to possibly remove "make submodules". Thank you.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/.gitignore b/.gitignore
2index 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
47diff --git a/Makefile b/Makefile
48index 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
127diff --git a/actions/actions.py b/actions/actions.py
128index 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"})
149diff --git a/lib/libsudopair.py b/lib/libsudopair.py
150index 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,
269diff --git a/reactive/sudo_pair.py b/reactive/sudo_pair.py
270index 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
281diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py
282index 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)
433diff --git a/tests/functional/requirements.txt b/tests/functional/requirements.txt
434index 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
444diff --git a/tests/functional/test_deploy.py b/tests/functional/test_deploy.py
445index 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)
638diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py
639index 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):
657diff --git a/tests/unit/requirements.txt b/tests/unit/requirements.txt
658index 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
667diff --git a/tests/unit/test_libsudopair.py b/tests/unit/test_libsudopair.py
668index 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'))
769diff --git a/tox.ini b/tox.ini
770index 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

Subscribers

People subscribed via source and target branches