Merge ~hloeung/jenkins-agent-charm:master into jenkins-agent-charm:master

Proposed by Haw Loeung
Status: Merged
Approved by: Haw Loeung
Approved revision: b1a4abd7e54cb8bfc20d167bc6c57f17e1b5f0ab
Merged at revision: 98c0339c7227b49a74cd9583da140bd00b6ffd8b
Proposed branch: ~hloeung/jenkins-agent-charm:master
Merge into: jenkins-agent-charm:master
Diff against target: 353 lines (+207/-24)
3 files modified
pytest.ini (+6/-0)
tests/unit/test_jenkins_slave.py (+200/-22)
tox.ini (+1/-2)
Reviewer Review Type Date Requested Status
Stuart Bishop (community) Approve
Haw Loeung Needs Resubmitting
Joel Sing (community) Needs Information
Review via email: mp+364302@code.launchpad.net

Commit message

Unit test all the things.

Description of the change

It's still missing unit tests for configure_nagios() which I'll work through next.

To post a comment you must log in.
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

This merge proposal is being monitored by mergebot. Change the status to Approved to merge.

Revision history for this message
Joel Sing (jsing) wrote :

The layer status change is a functional change, which I think should be split out and handled in a separate merge proposal to the testing additions (unless I'm missing the reason this cannot be done).

review: Needs Information
Revision history for this message
Haw Loeung (hloeung) wrote :

Okay, I've split out the change to layer-status to a separate MP - https://code.launchpad.net/~hloeung/jenkins-slave-charm/+git/jenkins-slave-charm/+merge/364411

review: Needs Resubmitting
Revision history for this message
Stuart Bishop (stub) wrote :

This all looks great.

The module mocks need to be reset in the TestCase's setUp method, rather than in the tests themselves.

review: Approve
Revision history for this message
Haw Loeung (hloeung) :
Revision history for this message
🤖 Canonical IS Merge Bot (canonical-is-mergebot) wrote :

Change successfully merged at revision 98c0339c7227b49a74cd9583da140bd00b6ffd8b

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/pytest.ini b/pytest.ini
2new file mode 100644
3index 0000000..39da980
4--- /dev/null
5+++ b/pytest.ini
6@@ -0,0 +1,6 @@
7+[pytest]
8+filterwarnings =
9+ # https://github.com/juju/charm-helpers/issues/294
10+ ignore:.*inspect.getargspec\(\) is deprecated.*:DeprecationWarning
11+ # https://github.com/juju/charm-helpers/issues/293
12+ ignore:.*dist\(\) and linux_distribution\(\) functions are deprecated:PendingDeprecationWarning
13diff --git a/tests/unit/test_jenkins_slave.py b/tests/unit/test_jenkins_slave.py
14index 7bd3ead..53e0e51 100644
15--- a/tests/unit/test_jenkins_slave.py
16+++ b/tests/unit/test_jenkins_slave.py
17@@ -2,21 +2,33 @@ import grp
18 import os
19 import pwd
20 import shutil
21+import stat
22 import sys
23 import tempfile
24 import unittest
25 from unittest import mock
26
27+
28+# We also need to mock up charms.apt and charms.layer so we can run
29+# unit tests without having to build the charm and pull in layers such
30+# as layer-status.
31 sys.modules['charms.apt'] = mock.MagicMock()
32-from charms import apt # NOQA: E402
33 sys.modules['charms.layer'] = mock.MagicMock()
34+from charms import apt # NOQA: E402
35+from charms.layer import status # NOQA: E402
36
37 # Add path to where our reactive layer lives and import.
38 sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
39 from reactive.jenkins_slave import (
40+ upgrade_charm,
41 config_changed,
42 install,
43 configure_jenkins_slave,
44+ blocked_on_jenkins_url,
45+ set_active,
46+ slave_relation_changed,
47+ slave_relation_removed,
48+ slave_relation,
49 file_to_units,
50 write_default_conf,
51 ) # NOQA: E402
52@@ -27,7 +39,9 @@ INITIAL_CONF = 'tests/unit/files/jenkins-slave-default'
53
54 class TestSetDefaultConf(unittest.TestCase):
55 def setUp(self):
56+ self.maxDiff = None
57 self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-')
58+ self.addCleanup(shutil.rmtree, self.tmpdir)
59 temp_file = tempfile.NamedTemporaryFile(delete=False, dir=self.tmpdir)
60 with open(INITIAL_CONF, 'rb') as f:
61 conf = f.read().decode('utf-8')
62@@ -56,46 +70,103 @@ class TestSetDefaultConf(unittest.TestCase):
63 self.addCleanup(patcher.stop)
64 self.mock_local_unit.return_value = 'mock-jenkins-slave/0'
65
66+ patcher = mock.patch('charmhelpers.core.hookenv.unit_private_ip')
67+ self.mock_local_unit = patcher.start()
68+ self.addCleanup(patcher.stop)
69+ self.mock_local_unit.return_value = '10.1.2.3'
70+
71 patcher = mock.patch('charmhelpers.core.hookenv.log')
72 self.mock_log = patcher.start()
73 self.addCleanup(patcher.stop)
74 self.mock_log.return_value = ''
75
76- def tearDown(self):
77- shutil.rmtree(self.tmpdir)
78+ patcher = mock.patch('os.uname')
79+ self.mock_log = patcher.start()
80+ self.addCleanup(patcher.stop)
81+ self.mock_log.return_value = ('Linux', 'mock-jenkins-slave', '4.15.0-46-generic',
82+ '#49-Ubuntu SMP Wed Feb 6 09:33:07 UTC 2019', 'x86_64')
83+
84+ apt.reset_mock()
85+ status.active.reset_mock()
86+ status.maintenance.reset_mock()
87+
88+ @mock.patch('charms.reactive.clear_flag')
89+ def test_hook_upgrade_charm_flags(self, clear_flag):
90+ '''Test correct flags set via upgrade-charm hook'''
91+ upgrade_charm()
92+ self.assertFalse(status.maintenance.assert_called())
93+ expected = [mock.call('jenkins-slave.active'), mock.call('jenkins-slave.installed')]
94+ self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True))
95
96 @mock.patch('charms.reactive.clear_flag')
97 def test_hook_config_changed(self, clear_flag):
98+ '''Test correct flags are set via config-changed charm hook'''
99 config_changed()
100+ self.assertFalse(status.maintenance.assert_called())
101 expected = [mock.call('jenkins-slave.blocked'),
102 mock.call('jenkins-slave.configured'),
103 mock.call('nagios-nrpe.configured')]
104- self.assertEqual(clear_flag.call_args_list, expected)
105+ self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True))
106
107+ @mock.patch('charmhelpers.core.hookenv.config')
108 @mock.patch('charmhelpers.core.host.adduser')
109 @mock.patch('charmhelpers.core.host.mkdir')
110 @mock.patch('charmhelpers.core.host.service')
111+ @mock.patch('charmhelpers.core.host.user_exists')
112 @mock.patch('reactive.jenkins_slave.apt_purge')
113 @mock.patch('reactive.jenkins_slave.file_to_units')
114 @mock.patch('reactive.jenkins_slave.write_default_conf')
115- def test_hook_install(self, write_default_conf, file_to_units, apt_purge, service, mkdir, adduser):
116+ def test_hook_install(
117+ self, write_default_conf, file_to_units, apt_purge, user_exists, service,
118+ mkdir, adduser, config):
119+ config.return_value = {'tools': 'some-tools-package'}
120+ apt.install_queued.return_value = True
121+ user_exists.return_value = False
122 install()
123+ self.assertFalse(status.maintenance.assert_called())
124+
125 expected = [mock.call(home_dir='/var/lib/jenkins', system_user=True, username='jenkins')]
126- self.assertEqual(adduser.call_args_list, expected)
127+ self.assertFalse(adduser.assert_has_calls(expected, any_order=True))
128 expected = [mock.call('/var/lib/jenkins', group='jenkins', owner='jenkins'),
129 mock.call('/var/log/jenkins', group='jenkins', owner='jenkins')]
130- self.assertEqual(mkdir.call_args_list, expected)
131- self.assertEqual(service.call_args_list, [mock.call('enable', 'jenkins-slave')])
132- self.assertEqual(apt_purge.call_args_list, [mock.call(['jenkins-slave'])])
133+ self.assertFalse(mkdir.assert_has_calls(expected, any_order=True))
134+ self.assertFalse(service.assert_has_calls([mock.call('enable', 'jenkins-slave')], any_order=True))
135+ self.assertFalse(apt_purge.assert_has_calls([mock.call(['jenkins-slave'])], any_order=True))
136 expected = [mock.call('files/download-slave.sh', '/usr/local/sbin/download-slave.sh'),
137 mock.call('files/jenkins-slave-logrotate-config', '/etc/logrotate.d/jenkins-slave'),
138 mock.call('files/jenkins-slave-systemd-config', '/lib/systemd/system/jenkins-slave.service')]
139- self.assertEqual(file_to_units.call_args_list, expected)
140- self.assertEqual(write_default_conf.call_args_list, [mock.call()])
141- expected = [mock.call.queue_install(['wget', 'default-jre-headless']),
142+ self.assertFalse(file_to_units.assert_has_calls(expected, any_order=True))
143+ self.assertFalse(write_default_conf.assert_called())
144+ expected = [mock.call.queue_install(['wget', 'default-jre-headless', 'some-tools-package']),
145 mock.call.install_queued()]
146 self.assertEqual(apt.method_calls, expected)
147
148+ apt.install_queued.return_value = False
149+ apt_purge.reset_mock()
150+ install()
151+ self.assertFalse(apt_purge.assert_not_called())
152+
153+ apt.install_queued.return_value = True
154+ user_exists.return_value = True
155+ adduser.reset_mock()
156+ install()
157+ self.assertFalse(adduser.assert_not_called())
158+
159+ @mock.patch('charmhelpers.core.host.lsb_release')
160+ @mock.patch('charmhelpers.core.host.adduser')
161+ @mock.patch('charmhelpers.core.host.mkdir')
162+ @mock.patch('reactive.jenkins_slave.file_to_units')
163+ @mock.patch('reactive.jenkins_slave.write_default_conf')
164+ def test_hook_install_trusty(self, write_default_conf, file_to_units, mkdir, adduser, lsb_release):
165+ apt.install_queued.return_value = True
166+ lsb_release.return_value = {'DISTRIB_CODENAME': 'trusty'}
167+ install()
168+ self.assertFalse(status.maintenance.assert_called())
169+
170+ # file_to_units('files/jenkins-slave-upstart-config', '/etc/init/jenkins-slave.conf')
171+ expected = [mock.call('files/jenkins-slave-upstart-config', '/etc/init/jenkins-slave.conf')]
172+ self.assertFalse(file_to_units.assert_has_calls(expected, any_order=True))
173+
174 @mock.patch('charms.reactive.clear_flag')
175 @mock.patch('charms.reactive.set_flag')
176 @mock.patch('charmhelpers.core.hookenv.config')
177@@ -107,9 +178,9 @@ class TestSetDefaultConf(unittest.TestCase):
178 config.return_value = {}
179 unitdata_kv.return_value = {}
180 configure_jenkins_slave()
181- self.assertEqual(write_default_conf.call_args_list, [])
182- self.assertEqual(set_flag.call_args_list, [mock.call('jenkins-slave.blocked')])
183- self.assertEqual(clear_flag.call_args_list, [mock.call('jenkins-slave.active')])
184+ self.assertFalse(write_default_conf.assert_not_called())
185+ self.assertFalse(set_flag.assert_has_calls([mock.call('jenkins-slave.blocked')], any_order=True))
186+ self.assertFalse(clear_flag.assert_has_calls([mock.call('jenkins-slave.active')], any_order=True))
187
188 @mock.patch('charms.reactive.clear_flag')
189 @mock.patch('charms.reactive.set_flag')
190@@ -122,10 +193,10 @@ class TestSetDefaultConf(unittest.TestCase):
191 config.return_value = {'master_url': 'http://10.1.1.1:8080'}
192 unitdata_kv.return_value = {}
193 configure_jenkins_slave()
194- self.assertEqual(write_default_conf.call_args_list, [mock.call('http://10.1.1.1:8080')])
195- self.assertEqual(set_flag.call_args_list, [mock.call('jenkins-slave.configured')])
196+ self.assertFalse(write_default_conf.assert_called_once_with('http://10.1.1.1:8080'))
197+ self.assertFalse(set_flag.assert_has_calls([mock.call('jenkins-slave.configured')], any_order=True))
198 expected = [mock.call('jenkins-slave.active'), mock.call('nagios-nrpe.configured')]
199- self.assertEqual(clear_flag.call_args_list, expected)
200+ self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True))
201
202 @mock.patch('charms.reactive.clear_flag')
203 @mock.patch('charms.reactive.set_flag')
204@@ -138,11 +209,107 @@ class TestSetDefaultConf(unittest.TestCase):
205 config.return_value = {}
206 unitdata_kv.return_value = {'url': 'http://10.22.22.22:8080'}
207 configure_jenkins_slave()
208- print(write_default_conf.call_args_list)
209- self.assertEqual(write_default_conf.call_args_list, [mock.call('http://10.22.22.22:8080')])
210- self.assertEqual(set_flag.call_args_list, [mock.call('jenkins-slave.configured')])
211+ self.assertTrue(write_default_conf.called_once_with('http://10.22.22.22:8080'))
212+ self.assertFalse(set_flag.assert_has_calls([mock.call('jenkins-slave.configured')], any_order=True))
213 expected = [mock.call('jenkins-slave.active'), mock.call('nagios-nrpe.configured')]
214- self.assertEqual(clear_flag.call_args_list, expected)
215+ self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True))
216+
217+ @mock.patch('charms.reactive.clear_flag')
218+ def test_blocked_on_jenkins_url(self, clear_flag):
219+ blocked_on_jenkins_url()
220+ self.assertFalse(clear_flag.assert_has_calls([mock.call('jenkins-slave.active')], any_order=True))
221+
222+ @mock.patch('charmhelpers.core.host.service_running')
223+ @mock.patch('charmhelpers.core.host.service_restart')
224+ @mock.patch('charmhelpers.core.host.service_start')
225+ def test_set_active_running(self, service_start, service_restart, service_running):
226+ '''Test service restarted when already running'''
227+ service_running.return_value = True
228+ set_active()
229+ self.assertFalse(status.maintenance.assert_called())
230+ self.assertFalse(service_start.assert_not_called())
231+ self.assertFalse(service_restart.assert_called_once_with('jenkins-slave'))
232+
233+ @mock.patch('charmhelpers.core.host.service_running')
234+ @mock.patch('charmhelpers.core.host.service_restart')
235+ @mock.patch('charmhelpers.core.host.service_start')
236+ def test_set_active_not_running(self, service_start, service_restart, service_running):
237+ '''Test service restarted when not running'''
238+ service_running.return_value = False
239+ set_active()
240+ self.assertFalse(status.maintenance.assert_called())
241+ self.assertFalse(service_start.assert_called_once_with('jenkins-slave'))
242+ self.assertFalse(service_restart.assert_not_called())
243+
244+ @mock.patch('charms.reactive.clear_flag')
245+ @mock.patch('charms.reactive.set_flag')
246+ def test_hook_slave_relation_changed_flags(self, set_flag, clear_flag):
247+ slave_relation_changed()
248+ expected = [
249+ mock.call('jenkins-slave.blocked'),
250+ mock.call('jenkins-slave.configured'),
251+ mock.call('slave-relation.configured'),
252+ ]
253+ self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True))
254+ self.assertFalse(set_flag.assert_has_calls([mock.call('slave-relation.available')], any_order=True))
255+
256+ @mock.patch('charms.reactive.clear_flag')
257+ def test_hook_slave_relation_removed_flags(self, clear_flag):
258+ slave_relation_removed()
259+ self.assertFalse(clear_flag.assert_has_calls([mock.call('slave-relation.available')], any_order=True))
260+
261+ @mock.patch('charms.reactive.clear_flag')
262+ @mock.patch('charms.reactive.set_flag')
263+ @mock.patch('charmhelpers.core.hookenv.config')
264+ @mock.patch('charmhelpers.core.hookenv.relation_get')
265+ @mock.patch('charmhelpers.core.hookenv.relation_set')
266+ @mock.patch('charmhelpers.core.unitdata.kv')
267+ @mock.patch('reactive.jenkins_slave.write_default_conf')
268+ def test_hook_slave_relation(
269+ self, write_default_conf, unitdata_kv, relation_set, relation_get,
270+ config, set_flag, clear_flag):
271+ config.return_value = {'master_url': ''}
272+ relation_get.return_value = 'http://10.1.1.1:8080'
273+ slave_relation()
274+ self.assertFalse(relation_get.assert_called_once_with('url'))
275+ self.assertFalse(clear_flag.assert_called_once_with('jenkins-slave.active'))
276+ self.assertFalse(set_flag.assert_called_once_with('slave-relation.configured'))
277+ expected = [mock.call(executors=2),
278+ mock.call(labels='x86_64'),
279+ mock.call(slavehost='mock-jenkins-slave-0'),
280+ mock.call(slaveaddress='10.1.2.3')]
281+ self.assertFalse(relation_set.assert_has_calls(expected, any_order=True))
282+
283+ config.return_value = {'master_url': '', 'labels': 'label1 label2'}
284+ relation_get.return_value = 'http://10.1.1.1:8080'
285+ relation_set.reset_mock()
286+ slave_relation()
287+ expected = [mock.call(executors=2),
288+ mock.call(labels='label1 label2'),
289+ mock.call(slavehost='mock-jenkins-slave-0'),
290+ mock.call(slaveaddress='10.1.2.3')]
291+ self.assertFalse(relation_set.assert_has_calls(expected, any_order=True))
292+
293+ @mock.patch('charmhelpers.core.hookenv.config')
294+ @mock.patch('charmhelpers.core.hookenv.relation_get')
295+ @mock.patch('charms.reactive.set_flag')
296+ def test_hook_slave_relation_master_url_set(self, set_flag, relation_get, config):
297+ config.return_value = {'master_url': 'http://10.1.1.1:8080'}
298+ relation_get.return_value = ''
299+ slave_relation()
300+ self.assertFalse(relation_get.assert_not_called())
301+ self.assertFalse(set_flag.assert_called_once_with('slave-relation.configured'))
302+
303+ @mock.patch('charms.reactive.clear_flag')
304+ @mock.patch('charms.reactive.set_flag')
305+ @mock.patch('charmhelpers.core.hookenv.config')
306+ @mock.patch('charmhelpers.core.hookenv.relation_get')
307+ def test_hook_slave_relation_relation_url_not_set(
308+ self, relation_get, config, set_flag, clear_flag):
309+ config.return_value = {'master_url': ''}
310+ relation_get.return_value = ''
311+ slave_relation()
312+ self.assertFalse(clear_flag.assert_not_called())
313
314 def test_write_default_conf_update(self):
315 write_default_conf('http://10.1.1.1:8080', self.user, self.group, self.conf_file)
316@@ -196,6 +363,17 @@ class TestSetDefaultConf(unittest.TestCase):
317 self.assertEqual(grp.getgrgid(os.stat(dest).st_gid).gr_name, self.group)
318 self.assertFalse(os.access(dest, os.X_OK))
319
320+ def test_file_to_units_with_perms(self):
321+ source = os.path.join(self.charm_dir, 'tests/unit/files/somefile')
322+ dest = os.path.join(self.tmpdir, os.path.basename(source))
323+ file_to_units(source, dest, owner=self.user, group=self.group, perms=0o640)
324+ with open(dest, 'rb') as fh:
325+ want = fh.read().decode('utf-8')
326+ self.assertTrue(conf_equals(source, want))
327+ self.assertEqual(pwd.getpwuid(os.stat(dest).st_uid).pw_name, self.user)
328+ self.assertEqual(grp.getgrgid(os.stat(dest).st_gid).gr_name, self.group)
329+ self.assertEqual(os.stat(dest)[stat.ST_MODE], 0o100640)
330+
331
332 def conf_equals(conf_file, want):
333 with open(conf_file, 'rb') as conf:
334diff --git a/tox.ini b/tox.ini
335index ee5dbeb..d6b7675 100644
336--- a/tox.ini
337+++ b/tox.ini
338@@ -9,7 +9,7 @@ setenv =
339 PYTHONPATH = .
340
341 [testenv:unit]
342-commands = pytest -v --ignore {toxinidir}/tests/functional --cov=lib --cov=reactive --cov=actions --cov-report=term
343+commands = pytest -v --ignore {toxinidir}/tests/functional --cov=lib --cov=reactive --cov=actions --cov-report=term-missing --cov-branch
344 deps = -r{toxinidir}/tests/unit/requirements.txt
345 -r{toxinidir}/requirements.txt
346 setenv = PYTHONPATH={toxinidir}/lib
347@@ -32,6 +32,5 @@ exclude =
348 .git,
349 __pycache__,
350 .tox,
351- hooks/install.d/,
352 max-line-length = 120
353 max-complexity = 10

Subscribers

People subscribed via source and target branches