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
diff --git a/pytest.ini b/pytest.ini
0new file mode 1006440new file mode 100644
index 0000000..39da980
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,6 @@
1[pytest]
2filterwarnings =
3 # https://github.com/juju/charm-helpers/issues/294
4 ignore:.*inspect.getargspec\(\) is deprecated.*:DeprecationWarning
5 # https://github.com/juju/charm-helpers/issues/293
6 ignore:.*dist\(\) and linux_distribution\(\) functions are deprecated:PendingDeprecationWarning
diff --git a/tests/unit/test_jenkins_slave.py b/tests/unit/test_jenkins_slave.py
index 7bd3ead..53e0e51 100644
--- a/tests/unit/test_jenkins_slave.py
+++ b/tests/unit/test_jenkins_slave.py
@@ -2,21 +2,33 @@ import grp
2import os2import os
3import pwd3import pwd
4import shutil4import shutil
5import stat
5import sys6import sys
6import tempfile7import tempfile
7import unittest8import unittest
8from unittest import mock9from unittest import mock
910
11
12# We also need to mock up charms.apt and charms.layer so we can run
13# unit tests without having to build the charm and pull in layers such
14# as layer-status.
10sys.modules['charms.apt'] = mock.MagicMock()15sys.modules['charms.apt'] = mock.MagicMock()
11from charms import apt # NOQA: E402
12sys.modules['charms.layer'] = mock.MagicMock()16sys.modules['charms.layer'] = mock.MagicMock()
17from charms import apt # NOQA: E402
18from charms.layer import status # NOQA: E402
1319
14# Add path to where our reactive layer lives and import.20# Add path to where our reactive layer lives and import.
15sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))21sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))))
16from reactive.jenkins_slave import (22from reactive.jenkins_slave import (
23 upgrade_charm,
17 config_changed,24 config_changed,
18 install,25 install,
19 configure_jenkins_slave,26 configure_jenkins_slave,
27 blocked_on_jenkins_url,
28 set_active,
29 slave_relation_changed,
30 slave_relation_removed,
31 slave_relation,
20 file_to_units,32 file_to_units,
21 write_default_conf,33 write_default_conf,
22) # NOQA: E40234) # NOQA: E402
@@ -27,7 +39,9 @@ INITIAL_CONF = 'tests/unit/files/jenkins-slave-default'
2739
28class TestSetDefaultConf(unittest.TestCase):40class TestSetDefaultConf(unittest.TestCase):
29 def setUp(self):41 def setUp(self):
42 self.maxDiff = None
30 self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-')43 self.tmpdir = tempfile.mkdtemp(prefix='charm-unittests-')
44 self.addCleanup(shutil.rmtree, self.tmpdir)
31 temp_file = tempfile.NamedTemporaryFile(delete=False, dir=self.tmpdir)45 temp_file = tempfile.NamedTemporaryFile(delete=False, dir=self.tmpdir)
32 with open(INITIAL_CONF, 'rb') as f:46 with open(INITIAL_CONF, 'rb') as f:
33 conf = f.read().decode('utf-8')47 conf = f.read().decode('utf-8')
@@ -56,46 +70,103 @@ class TestSetDefaultConf(unittest.TestCase):
56 self.addCleanup(patcher.stop)70 self.addCleanup(patcher.stop)
57 self.mock_local_unit.return_value = 'mock-jenkins-slave/0'71 self.mock_local_unit.return_value = 'mock-jenkins-slave/0'
5872
73 patcher = mock.patch('charmhelpers.core.hookenv.unit_private_ip')
74 self.mock_local_unit = patcher.start()
75 self.addCleanup(patcher.stop)
76 self.mock_local_unit.return_value = '10.1.2.3'
77
59 patcher = mock.patch('charmhelpers.core.hookenv.log')78 patcher = mock.patch('charmhelpers.core.hookenv.log')
60 self.mock_log = patcher.start()79 self.mock_log = patcher.start()
61 self.addCleanup(patcher.stop)80 self.addCleanup(patcher.stop)
62 self.mock_log.return_value = ''81 self.mock_log.return_value = ''
6382
64 def tearDown(self):83 patcher = mock.patch('os.uname')
65 shutil.rmtree(self.tmpdir)84 self.mock_log = patcher.start()
85 self.addCleanup(patcher.stop)
86 self.mock_log.return_value = ('Linux', 'mock-jenkins-slave', '4.15.0-46-generic',
87 '#49-Ubuntu SMP Wed Feb 6 09:33:07 UTC 2019', 'x86_64')
88
89 apt.reset_mock()
90 status.active.reset_mock()
91 status.maintenance.reset_mock()
92
93 @mock.patch('charms.reactive.clear_flag')
94 def test_hook_upgrade_charm_flags(self, clear_flag):
95 '''Test correct flags set via upgrade-charm hook'''
96 upgrade_charm()
97 self.assertFalse(status.maintenance.assert_called())
98 expected = [mock.call('jenkins-slave.active'), mock.call('jenkins-slave.installed')]
99 self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True))
66100
67 @mock.patch('charms.reactive.clear_flag')101 @mock.patch('charms.reactive.clear_flag')
68 def test_hook_config_changed(self, clear_flag):102 def test_hook_config_changed(self, clear_flag):
103 '''Test correct flags are set via config-changed charm hook'''
69 config_changed()104 config_changed()
105 self.assertFalse(status.maintenance.assert_called())
70 expected = [mock.call('jenkins-slave.blocked'),106 expected = [mock.call('jenkins-slave.blocked'),
71 mock.call('jenkins-slave.configured'),107 mock.call('jenkins-slave.configured'),
72 mock.call('nagios-nrpe.configured')]108 mock.call('nagios-nrpe.configured')]
73 self.assertEqual(clear_flag.call_args_list, expected)109 self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True))
74110
111 @mock.patch('charmhelpers.core.hookenv.config')
75 @mock.patch('charmhelpers.core.host.adduser')112 @mock.patch('charmhelpers.core.host.adduser')
76 @mock.patch('charmhelpers.core.host.mkdir')113 @mock.patch('charmhelpers.core.host.mkdir')
77 @mock.patch('charmhelpers.core.host.service')114 @mock.patch('charmhelpers.core.host.service')
115 @mock.patch('charmhelpers.core.host.user_exists')
78 @mock.patch('reactive.jenkins_slave.apt_purge')116 @mock.patch('reactive.jenkins_slave.apt_purge')
79 @mock.patch('reactive.jenkins_slave.file_to_units')117 @mock.patch('reactive.jenkins_slave.file_to_units')
80 @mock.patch('reactive.jenkins_slave.write_default_conf')118 @mock.patch('reactive.jenkins_slave.write_default_conf')
81 def test_hook_install(self, write_default_conf, file_to_units, apt_purge, service, mkdir, adduser):119 def test_hook_install(
120 self, write_default_conf, file_to_units, apt_purge, user_exists, service,
121 mkdir, adduser, config):
122 config.return_value = {'tools': 'some-tools-package'}
123 apt.install_queued.return_value = True
124 user_exists.return_value = False
82 install()125 install()
126 self.assertFalse(status.maintenance.assert_called())
127
83 expected = [mock.call(home_dir='/var/lib/jenkins', system_user=True, username='jenkins')]128 expected = [mock.call(home_dir='/var/lib/jenkins', system_user=True, username='jenkins')]
84 self.assertEqual(adduser.call_args_list, expected)129 self.assertFalse(adduser.assert_has_calls(expected, any_order=True))
85 expected = [mock.call('/var/lib/jenkins', group='jenkins', owner='jenkins'),130 expected = [mock.call('/var/lib/jenkins', group='jenkins', owner='jenkins'),
86 mock.call('/var/log/jenkins', group='jenkins', owner='jenkins')]131 mock.call('/var/log/jenkins', group='jenkins', owner='jenkins')]
87 self.assertEqual(mkdir.call_args_list, expected)132 self.assertFalse(mkdir.assert_has_calls(expected, any_order=True))
88 self.assertEqual(service.call_args_list, [mock.call('enable', 'jenkins-slave')])133 self.assertFalse(service.assert_has_calls([mock.call('enable', 'jenkins-slave')], any_order=True))
89 self.assertEqual(apt_purge.call_args_list, [mock.call(['jenkins-slave'])])134 self.assertFalse(apt_purge.assert_has_calls([mock.call(['jenkins-slave'])], any_order=True))
90 expected = [mock.call('files/download-slave.sh', '/usr/local/sbin/download-slave.sh'),135 expected = [mock.call('files/download-slave.sh', '/usr/local/sbin/download-slave.sh'),
91 mock.call('files/jenkins-slave-logrotate-config', '/etc/logrotate.d/jenkins-slave'),136 mock.call('files/jenkins-slave-logrotate-config', '/etc/logrotate.d/jenkins-slave'),
92 mock.call('files/jenkins-slave-systemd-config', '/lib/systemd/system/jenkins-slave.service')]137 mock.call('files/jenkins-slave-systemd-config', '/lib/systemd/system/jenkins-slave.service')]
93 self.assertEqual(file_to_units.call_args_list, expected)138 self.assertFalse(file_to_units.assert_has_calls(expected, any_order=True))
94 self.assertEqual(write_default_conf.call_args_list, [mock.call()])139 self.assertFalse(write_default_conf.assert_called())
95 expected = [mock.call.queue_install(['wget', 'default-jre-headless']),140 expected = [mock.call.queue_install(['wget', 'default-jre-headless', 'some-tools-package']),
96 mock.call.install_queued()]141 mock.call.install_queued()]
97 self.assertEqual(apt.method_calls, expected)142 self.assertEqual(apt.method_calls, expected)
98143
144 apt.install_queued.return_value = False
145 apt_purge.reset_mock()
146 install()
147 self.assertFalse(apt_purge.assert_not_called())
148
149 apt.install_queued.return_value = True
150 user_exists.return_value = True
151 adduser.reset_mock()
152 install()
153 self.assertFalse(adduser.assert_not_called())
154
155 @mock.patch('charmhelpers.core.host.lsb_release')
156 @mock.patch('charmhelpers.core.host.adduser')
157 @mock.patch('charmhelpers.core.host.mkdir')
158 @mock.patch('reactive.jenkins_slave.file_to_units')
159 @mock.patch('reactive.jenkins_slave.write_default_conf')
160 def test_hook_install_trusty(self, write_default_conf, file_to_units, mkdir, adduser, lsb_release):
161 apt.install_queued.return_value = True
162 lsb_release.return_value = {'DISTRIB_CODENAME': 'trusty'}
163 install()
164 self.assertFalse(status.maintenance.assert_called())
165
166 # file_to_units('files/jenkins-slave-upstart-config', '/etc/init/jenkins-slave.conf')
167 expected = [mock.call('files/jenkins-slave-upstart-config', '/etc/init/jenkins-slave.conf')]
168 self.assertFalse(file_to_units.assert_has_calls(expected, any_order=True))
169
99 @mock.patch('charms.reactive.clear_flag')170 @mock.patch('charms.reactive.clear_flag')
100 @mock.patch('charms.reactive.set_flag')171 @mock.patch('charms.reactive.set_flag')
101 @mock.patch('charmhelpers.core.hookenv.config')172 @mock.patch('charmhelpers.core.hookenv.config')
@@ -107,9 +178,9 @@ class TestSetDefaultConf(unittest.TestCase):
107 config.return_value = {}178 config.return_value = {}
108 unitdata_kv.return_value = {}179 unitdata_kv.return_value = {}
109 configure_jenkins_slave()180 configure_jenkins_slave()
110 self.assertEqual(write_default_conf.call_args_list, [])181 self.assertFalse(write_default_conf.assert_not_called())
111 self.assertEqual(set_flag.call_args_list, [mock.call('jenkins-slave.blocked')])182 self.assertFalse(set_flag.assert_has_calls([mock.call('jenkins-slave.blocked')], any_order=True))
112 self.assertEqual(clear_flag.call_args_list, [mock.call('jenkins-slave.active')])183 self.assertFalse(clear_flag.assert_has_calls([mock.call('jenkins-slave.active')], any_order=True))
113184
114 @mock.patch('charms.reactive.clear_flag')185 @mock.patch('charms.reactive.clear_flag')
115 @mock.patch('charms.reactive.set_flag')186 @mock.patch('charms.reactive.set_flag')
@@ -122,10 +193,10 @@ class TestSetDefaultConf(unittest.TestCase):
122 config.return_value = {'master_url': 'http://10.1.1.1:8080'}193 config.return_value = {'master_url': 'http://10.1.1.1:8080'}
123 unitdata_kv.return_value = {}194 unitdata_kv.return_value = {}
124 configure_jenkins_slave()195 configure_jenkins_slave()
125 self.assertEqual(write_default_conf.call_args_list, [mock.call('http://10.1.1.1:8080')])196 self.assertFalse(write_default_conf.assert_called_once_with('http://10.1.1.1:8080'))
126 self.assertEqual(set_flag.call_args_list, [mock.call('jenkins-slave.configured')])197 self.assertFalse(set_flag.assert_has_calls([mock.call('jenkins-slave.configured')], any_order=True))
127 expected = [mock.call('jenkins-slave.active'), mock.call('nagios-nrpe.configured')]198 expected = [mock.call('jenkins-slave.active'), mock.call('nagios-nrpe.configured')]
128 self.assertEqual(clear_flag.call_args_list, expected)199 self.assertFalse(clear_flag.assert_has_calls(expected, any_order=True))
129200
130 @mock.patch('charms.reactive.clear_flag')201 @mock.patch('charms.reactive.clear_flag')
131 @mock.patch('charms.reactive.set_flag')202 @mock.patch('charms.reactive.set_flag')
@@ -138,11 +209,107 @@ class TestSetDefaultConf(unittest.TestCase):
138 config.return_value = {}209 config.return_value = {}
139 unitdata_kv.return_value = {'url': 'http://10.22.22.22:8080'}210 unitdata_kv.return_value = {'url': 'http://10.22.22.22:8080'}
140 configure_jenkins_slave()211 configure_jenkins_slave()
141 print(write_default_conf.call_args_list)212 self.assertTrue(write_default_conf.called_once_with('http://10.22.22.22:8080'))
142 self.assertEqual(write_default_conf.call_args_list, [mock.call('http://10.22.22.22:8080')])213 self.assertFalse(set_flag.assert_has_calls([mock.call('jenkins-slave.configured')], any_order=True))
143 self.assertEqual(set_flag.call_args_list, [mock.call('jenkins-slave.configured')])
144 expected = [mock.call('jenkins-slave.active'), mock.call('nagios-nrpe.configured')]214 expected = [mock.call('jenkins-slave.active'), mock.call('nagios-nrpe.configured')]
145 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())
146313
147 def test_write_default_conf_update(self):314 def test_write_default_conf_update(self):
148 write_default_conf('http://10.1.1.1:8080', self.user, self.group, self.conf_file)315 write_default_conf('http://10.1.1.1:8080', self.user, self.group, self.conf_file)
@@ -196,6 +363,17 @@ class TestSetDefaultConf(unittest.TestCase):
196 self.assertEqual(grp.getgrgid(os.stat(dest).st_gid).gr_name, self.group)363 self.assertEqual(grp.getgrgid(os.stat(dest).st_gid).gr_name, self.group)
197 self.assertFalse(os.access(dest, os.X_OK))364 self.assertFalse(os.access(dest, os.X_OK))
198365
366 def test_file_to_units_with_perms(self):
367 source = os.path.join(self.charm_dir, 'tests/unit/files/somefile')
368 dest = os.path.join(self.tmpdir, os.path.basename(source))
369 file_to_units(source, dest, owner=self.user, group=self.group, perms=0o640)
370 with open(dest, 'rb') as fh:
371 want = fh.read().decode('utf-8')
372 self.assertTrue(conf_equals(source, want))
373 self.assertEqual(pwd.getpwuid(os.stat(dest).st_uid).pw_name, self.user)
374 self.assertEqual(grp.getgrgid(os.stat(dest).st_gid).gr_name, self.group)
375 self.assertEqual(os.stat(dest)[stat.ST_MODE], 0o100640)
376
199377
200def conf_equals(conf_file, want):378def conf_equals(conf_file, want):
201 with open(conf_file, 'rb') as conf:379 with open(conf_file, 'rb') as conf:
diff --git a/tox.ini b/tox.ini
index ee5dbeb..d6b7675 100644
--- a/tox.ini
+++ b/tox.ini
@@ -9,7 +9,7 @@ setenv =
9 PYTHONPATH = .9 PYTHONPATH = .
1010
11[testenv:unit]11[testenv:unit]
12commands = pytest -v --ignore {toxinidir}/tests/functional --cov=lib --cov=reactive --cov=actions --cov-report=term12commands = pytest -v --ignore {toxinidir}/tests/functional --cov=lib --cov=reactive --cov=actions --cov-report=term-missing --cov-branch
13deps = -r{toxinidir}/tests/unit/requirements.txt13deps = -r{toxinidir}/tests/unit/requirements.txt
14 -r{toxinidir}/requirements.txt14 -r{toxinidir}/requirements.txt
15setenv = PYTHONPATH={toxinidir}/lib15setenv = PYTHONPATH={toxinidir}/lib
@@ -32,6 +32,5 @@ exclude =
32 .git,32 .git,
33 __pycache__,33 __pycache__,
34 .tox,34 .tox,
35 hooks/install.d/,
36max-line-length = 12035max-line-length = 120
37max-complexity = 1036max-complexity = 10

Subscribers

People subscribed via source and target branches