Merge lp:~mandel/ubuntuone-dev-tools/fix-squid-tests into lp:ubuntuone-dev-tools

Proposed by Manuel de la Peña on 2012-04-24
Status: Merged
Approved by: Manuel de la Peña on 2012-04-30
Approved revision: 76
Merged at revision: 70
Proposed branch: lp:~mandel/ubuntuone-dev-tools/fix-squid-tests
Merge into: lp:ubuntuone-dev-tools
Diff against target: 509 lines (+266/-33)
9 files modified
data/squid.conf.in (+5/-5)
run-tests (+2/-2)
run-tests.bat (+3/-1)
setup.py (+17/-9)
ubuntuone/devtools/services/squid.py (+54/-9)
ubuntuone/devtools/services/tests/test_squid.py (+9/-6)
ubuntuone/devtools/services/tests/test_squid_linux.py (+67/-0)
ubuntuone/devtools/services/tests/test_squid_windows.py (+108/-0)
ubuntuone/devtools/testcases/tests/test_squid_testcase.py (+1/-1)
To merge this branch: bzr merge lp:~mandel/ubuntuone-dev-tools/fix-squid-tests
Reviewer Review Type Date Requested Status
dobey (community) Approve on 2012-04-27
Eric Casteleijn (community) 2012-04-24 Approve on 2012-04-24
Review via email: mp+103325@code.launchpad.net

Commit Message

- Ensured that the ncsa_auth.exe can be found on windows (LP: #987213).
- Formatted the path correctly so that the squid config can read them (LP: #987225).
- Use win32api to kill the squid process (LP: #987256)
- Use dirspec to place the data files correctly on windows (LP: #988228).

Description of the Change

- Ensured that the ncsa_auth.exe can be found on windows (LP: #987213).
- Formatted the path correctly so that the squid config can read them (LP: #987225).
- Use win32api to kill the squid process (LP: #987256)
- Use dirspec to place the data files correctly on windows (LP: #988228).

In order to tests the branch you have to follow these steps:

1- Download and copy to C:\squid the windows squid binaries from here: http://squid.acmeconsulting.it/
2- Downlaod the apacache msi and install it: http://httpd.apache.org/download.cgi
3- Ensure the following is in your path: C:\squid\sbin;C:\squid\libexec;C:\Program Files\Apache Software Foundation\Apache2.2\bin

All the above will allow you to run the proxy tests on windows.

Note: The path to the apache bins might differ depending on your system.

To post a comment you must log in.
Eric Casteleijn (thisfred) wrote :

98 +if sys.platform == 'win32':
99 + from ubuntuone.devtools.services.squid import windows
100 + source = windows
101 +else:
102 + from ubuntuone.devtools.services.squid import linux
103 + source = linux

why not import windows as source, and import linux as source?

Eric Casteleijn (thisfred) wrote :

Also, what does "source" mean in this context?

Eric Casteleijn (thisfred) wrote :

I would suggest something like "squid_util" or "squid_helpers" instead of source.

review: Needs Information
Eric Casteleijn (thisfred) wrote :

Looks good otherwise, use your own judgement on the above...

review: Approve
dobey (dobey) wrote :

+# Do use doble \ because squids config needs \ to be escaped
+ """Return the path for the formated for the config."""

Some spelling and grammar fixes need to happen. :)

Also, why do we need to split the Linux and Windows pieces up like this? We need to find a way to run all the tests on all platforms, and just skip the ones that are platform-specific on other platforms.

review: Needs Fixing
Manuel de la Peña (mandel) wrote :

> +# Do use doble \ because squids config needs \ to be escaped
> + """Return the path for the formated for the config."""
>
> Some spelling and grammar fixes need to happen. :)

Fixing!

>
> Also, why do we need to split the Linux and Windows pieces up like this? We
> need to find a way to run all the tests on all platforms, and just skip the
> ones that are platform-specific on other platforms.

Well this is not splitting the tests according to the platform but working around the fact that squid on windows is not a good citizen. The following is a list of the workarounds we have to do to ensure that we can run all squid tests on windows:

* os.kill does not work on windows, we need to use the win api or we will leave the proxy running.
* The squid config is stupid, it needs paths to be C:\\path\\to\\cache rather than C:\path\to\cache, if you don't do that squid wont start.
* The location of the ncsa_auth is diff.

With that fixed we are able to run all tests as long as squid and htpasswd are present in the path.

Manuel de la Peña (mandel) wrote :

> I would suggest something like "squid_util" or "squid_helpers" instead of
> source.

Fixing!

dobey (dobey) :
review: Approve
Ubuntu One Auto Pilot (otto-pilot) wrote :

Attempt to merge into lp:ubuntuone-dev-tools failed due to conflicts:

text conflict in run-tests.bat

76. By Manuel de la Peña on 2012-04-30

Fixed conflicts issues with trunk.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'data/squid.conf.in'
2--- data/squid.conf.in 2012-01-05 16:42:37 +0000
3+++ data/squid.conf.in 2012-04-30 11:07:20 +0000
4@@ -80,19 +80,19 @@
5 cache_dir ufs ${spool_temp} 1000 16 256
6
7 # access log settings
8-access_log ${squid_temp}/access.log squid
9+access_log ${squid_temp}access.log squid
10
11 # cache log settings
12-cache_log ${squid_temp}/cache.log
13+cache_log ${squid_temp}cache.log
14
15 # cache store log settings
16-cache_store_log ${squid_temp}/store.log
17+cache_store_log ${squid_temp}store.log
18
19 # mime table conf
20 # mime_table /usr/share/squid/mime.conf
21
22 #Default pid file name
23-pid_filename ${squid_temp}/squid.pid
24+pid_filename ${squid_temp}squid.pid
25
26 # debug options (Full debugging)
27 debug_options ALL,1
28@@ -117,4 +117,4 @@
29 hosts_file /etc/hosts
30
31 # Leave coredumps in the first cache dir
32-coredump_dir ${spool_temp}/squid
33+coredump_dir ${spool_temp}squid
34
35=== modified file 'run-tests'
36--- run-tests 2012-01-12 12:08:22 +0000
37+++ run-tests 2012-04-30 11:07:20 +0000
38@@ -16,8 +16,8 @@
39 # with this program. If not, see <http://www.gnu.org/licenses/>.
40 set -e
41
42-bin/u1trial -c ubuntuone
43-bin/u1trial --reactor=twisted ubuntuone
44+bin/u1trial -i "test_squid_windows.py" -c ubuntuone
45+bin/u1trial --reactor=twisted -i "test_squid_windows.py" ubuntuone
46 echo "Running style checks..."
47 bin/u1lint
48 pep8 --repeat . bin/* --exclude=*.bat
49
50=== modified file 'run-tests.bat'
51--- run-tests.bat 2012-04-25 12:33:48 +0000
52+++ run-tests.bat 2012-04-30 11:07:20 +0000
53@@ -28,6 +28,8 @@
54 :: files in the program, then also delete it here.
55 @ECHO off
56
57+SET IGNORED="test_squid_linux.py"
58+
59 ECHO Checking for Python on the path
60 :: Look for Python from buildout
61 FOR %%A in (python.exe) do (SET PYTHONEXEPATH=%%~$PATH:A)
62@@ -67,7 +69,7 @@
63
64 ECHO Python found at %PYTHONEXEPATH%, executing the tests...
65 :: execute the tests with a number of ignored linux only modules
66-"%PYTHONEXEPATH%" "%TRIALPATH%" --reactor=twisted -c %PARAMS% ubuntuone
67+"%PYTHONEXEPATH%" bin/u1trial --reactor=twisted -i %IGNORED% -c %PARAMS% ubuntuone
68
69 IF %SKIPLINT% == 1 (
70 ECHO Skipping style checks
71
72=== modified file 'setup.py'
73--- setup.py 2012-03-30 17:44:03 +0000
74+++ setup.py 2012-04-30 11:07:20 +0000
75@@ -28,10 +28,12 @@
76 # files in the program, then also delete it here.
77 """setup.py"""
78
79+import os
80 import subprocess
81 import sys
82
83 from distutils.core import setup, Command
84+from dirspec import basedir
85
86 PACKAGE = 'ubuntuone-dev-tools'
87 VERSION = '3.1'
88@@ -65,6 +67,20 @@
89 if sys.platform == 'win32':
90 # lets add the .bat so that windows users are happy
91 scripts.extend(['bin/u1lint.bat', 'bin/u1trial.bat'])
92+ DATA_FILES = [(os.path.join(basedir.default_data_path, PACKAGE),
93+ ['pylintrc',
94+ 'data/dbus-session.conf.in',
95+ 'data/squid.conf.in']),
96+ ]
97+else:
98+ DATA_FILES = [('share/%s' % PACKAGE,
99+ ['pylintrc',
100+ 'data/dbus-session.conf.in',
101+ 'data/squid.conf.in']),
102+ ('share/man/man1',
103+ ['man/u1lint.1',
104+ 'man/u1trial.1']),
105+ ]
106
107 setup(name=PACKAGE,
108 version=VERSION,
109@@ -78,15 +94,7 @@
110 'ubuntuone.devtools.testcases'],
111 extra_path='ubuntuone-dev-tools',
112 scripts=scripts,
113- data_files=[('share/%s' % PACKAGE,
114- ['pylintrc',
115- 'data/dbus-session.conf.in',
116- 'data/squid.conf.in']),
117- ('share/man/man1',
118- ['man/u1lint.1',
119- 'man/u1trial.1']),
120- ],
121-
122+ data_files=DATA_FILES,
123 cmdclass={
124 'lint': Lint,
125 },
126
127=== modified file 'ubuntuone/devtools/services/squid.py'
128--- ubuntuone/devtools/services/squid.py 2012-04-20 17:00:51 +0000
129+++ ubuntuone/devtools/services/squid.py 2012-04-30 11:07:20 +0000
130@@ -33,10 +33,11 @@
131 import string
132 # pylint:enable=W0402
133 import subprocess
134+import sys
135 import time
136
137 from json import dumps, loads
138-from os import environ, makedirs, kill, unlink
139+from os import environ, kill, makedirs, unlink
140 from os.path import abspath, exists, join
141
142 from distutils.spawn import find_executable
143@@ -46,24 +47,52 @@
144 get_arbitrary_port,
145 )
146
147+# pylint: disable=C0103
148+if sys.platform == 'win32':
149+ AUTH_PROCESS_PATH = r'C:\squid\libexec\ncsa_auth.exe'
150+ SQUID_START_ARGS = ['-f']
151+else:
152+ AUTH_PROCESS_PATH = '/usr/lib/%s/ncsa_auth'
153+ SQUID_START_ARGS = ['-N', '-X', '-f']
154+# pylint: enable=C0103
155+
156 SQUID_CONFIG_FILE = 'squid.conf.in'
157 SQUID_DIR = 'squid'
158-AUTH_PROCESS_PATH = '/usr/lib/%s/ncsa_auth'
159 SPOOL_DIR = 'spool'
160 AUTH_FILE = 'htpasswd'
161 PROXY_ENV_VAR = 'SQUID_PROXY_SETTINGS'
162
163
164+def format_config_path(path):
165+ """Return the path correctly formatted for the config file."""
166+ # squid cannot handle correctly paths with a single \
167+ return path.replace('\\', '\\\\')
168+
169+
170+def get_auth_process_path(squid_version):
171+ """Return the path to the auth executable."""
172+ if sys.platform == 'win32':
173+ path = find_executable('ncsa_auth')
174+ if path is None:
175+ path = AUTH_PROCESS_PATH
176+ return format_config_path(path)
177+ else:
178+ squid = 'squid3' if squid_version == 3 else 'squid'
179+ path = AUTH_PROCESS_PATH % squid
180+ return path
181+
182+
183 def get_squid_executable():
184 """Return the squid executable of the system."""
185 # try with squid and if not present try with squid3 for newer systems
186 # (Ubuntu P). We also return the path to the auth process so that we can
187 # point to the correct one.
188 squid = find_executable('squid3')
189- auth_process = AUTH_PROCESS_PATH % 'squid3'
190+ version = 3
191 if squid is None:
192+ version = 2
193 squid = find_executable('squid')
194- auth_process = AUTH_PROCESS_PATH % 'squid'
195+ auth_process = get_auth_process_path(version)
196 return squid, auth_process
197
198
199@@ -72,6 +101,21 @@
200 return find_executable('htpasswd')
201
202
203+def kill_squid(squid_pid):
204+ """Kill the squid process."""
205+ if sys.platform == 'win32':
206+ # pylint: disable=F0401
207+ import win32api
208+ import win32con
209+ # pylint: enable=F0401
210+
211+ handle = win32api.OpenProcess(win32con.PROCESS_TERMINATE, 0, squid_pid)
212+ win32api.TerminateProcess(handle, 0)
213+ win32api.CloseHandle(handle)
214+ else:
215+ kill(squid_pid, signal.SIGKILL)
216+
217+
218 def _make_random_string(count):
219 """Make a random string of the given length."""
220 entropy = random.SystemRandom()
221@@ -95,7 +139,7 @@
222 path = abspath(path)
223 if not exists(path):
224 makedirs(path)
225- return path
226+ return format_config_path(path)
227
228
229 def _get_squid_temp_path(tempdir=''):
230@@ -105,7 +149,7 @@
231 path = abspath(path)
232 if not exists(path):
233 makedirs(path)
234- return path
235+ return format_config_path(join(path, ''))
236
237
238 def _get_auth_temp_path(tempdir=''):
239@@ -114,7 +158,7 @@
240 auth_file = join(basedir, AUTH_FILE)
241 if not exists(basedir):
242 makedirs(basedir)
243- return auth_file
244+ return format_config_path(auth_file)
245
246
247 def store_proxy_settings(settings):
248@@ -238,7 +282,8 @@
249 self._generate_auth_file(tempdir)
250 self._generate_config_file(tempdir)
251 self._generate_swap(self.config_file)
252- squid_args = ['-N', '-X', '-f', self.config_file]
253+ squid_args = SQUID_START_ARGS
254+ squid_args.append(self.config_file)
255 sp = subprocess.Popen([self.squid] + squid_args,
256 stdout=subprocess.PIPE,
257 stderr=subprocess.PIPE)
258@@ -255,7 +300,7 @@
259
260 def stop_service(self):
261 """Stop our proxy,"""
262- kill(self.squid_pid, signal.SIGKILL)
263+ kill_squid(self.squid_pid)
264 delete_proxy_settings()
265 self.running = False
266 unlink(self.config_file)
267
268=== modified file 'ubuntuone/devtools/services/tests/test_squid.py'
269--- ubuntuone/devtools/services/tests/test_squid.py 2012-04-20 16:42:54 +0000
270+++ ubuntuone/devtools/services/tests/test_squid.py 2012-04-30 11:07:20 +0000
271@@ -72,6 +72,7 @@
272 self.patch(squid, '_get_basedir', fake_basedir_fn)
273 self.patch(squid, 'makedirs', fake_makedirs)
274 self.patch(squid, 'exists', fake_exists)
275+ self.patch(squid, 'format_config_path', lambda path: path)
276
277 def test_get_basedir_missing(self):
278 """Test the base dir creation."""
279@@ -116,24 +117,26 @@
280
281 def test_get_squid_temp_path_missing(self):
282 """Test the squid path creation."""
283- expected_path = os.path.join(self.basedir, squid.SQUID_DIR)
284+ expected_path = os.path.join(self.basedir, squid.SQUID_DIR, '')
285+ abspath = os.path.abspath(expected_path)
286 result = squid._get_squid_temp_path()
287 self.assertEqual(expected_path, result)
288 self.assertTrue(('fake_basedir_fn', '') in self.called)
289- self.assertTrue(('fake_makedirs', expected_path) in self.called)
290- self.assertTrue(expected_path in self.created_paths)
291- self.assertTrue(('fake_exists', expected_path) in self.called)
292+ self.assertTrue(('fake_makedirs', abspath) in self.called)
293+ self.assertTrue(abspath in self.created_paths)
294+ self.assertTrue(('fake_exists', abspath) in self.called)
295
296 def test_get_squid_temp_path_present(self):
297 """Test the squid path creation."""
298 self.path_exists = True
299- expected_path = os.path.join(self.basedir, squid.SQUID_DIR)
300+ expected_path = os.path.join(self.basedir, squid.SQUID_DIR, '')
301 result = squid._get_squid_temp_path()
302 self.assertEqual(expected_path, result)
303 self.assertTrue(('fake_basedir_fn', '') in self.called)
304 self.assertTrue(('fake_makedirs', expected_path) not in self.called)
305 self.assertTrue(expected_path not in self.created_paths)
306- self.assertTrue(('fake_exists', expected_path) in self.called)
307+ self.assertTrue(('fake_exists',
308+ os.path.abspath(expected_path)) in self.called)
309
310 def test_get_auth_temp_path(self):
311 """Test the creation of the auth path."""
312
313=== added file 'ubuntuone/devtools/services/tests/test_squid_linux.py'
314--- ubuntuone/devtools/services/tests/test_squid_linux.py 1970-01-01 00:00:00 +0000
315+++ ubuntuone/devtools/services/tests/test_squid_linux.py 2012-04-30 11:07:20 +0000
316@@ -0,0 +1,67 @@
317+#
318+# Copyright 2012 Canonical Ltd.
319+#
320+# This program is free software: you can redistribute it and/or modify it
321+# under the terms of the GNU General Public License version 3, as published
322+# by the Free Software Foundation.
323+#
324+# This program is distributed in the hope that it will be useful, but
325+# WITHOUT ANY WARRANTY; without even the implied warranties of
326+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
327+# PURPOSE. See the GNU General Public License for more details.
328+#
329+# You should have received a copy of the GNU General Public License along
330+# with this program. If not, see <http://www.gnu.org/licenses/>.
331+#
332+# In addition, as a special exception, the copyright holders give
333+# permission to link the code of portions of this program with the
334+# OpenSSL library under certain conditions as described in each
335+# individual source file, and distribute linked combinations
336+# including the two.
337+# You must obey the GNU General Public License in all respects
338+# for all of the code used other than OpenSSL. If you modify
339+# file(s) with this exception, you may extend this exception to your
340+# version of the file(s), but you are not obligated to do so. If you
341+# do not wish to do so, delete this exception statement from your
342+# version. If you delete this exception statement from all source
343+# files in the program, then also delete it here.
344+"""Tests for the linux squid bits."""
345+
346+import signal
347+
348+from twisted.trial.unittest import TestCase
349+
350+from ubuntuone.devtools.services import squid
351+
352+
353+class SquidLinuxTestCase(TestCase):
354+ """"Test the different linux bits."""
355+
356+ def test_get_auth_process_3(self):
357+ """Test getting the auth process for squid3."""
358+ expected = squid.AUTH_PROCESS_PATH % 'squid3'
359+ self.assertEqual(expected, squid.get_auth_process_path(3))
360+
361+ def test_get_auth_process(self):
362+ """Test getting the auth process."""
363+ expected = squid.AUTH_PROCESS_PATH % 'squid'
364+ self.assertEqual(expected, squid.get_auth_process_path(2))
365+
366+ def test_format_config_path(self):
367+ """Test formating a config path."""
368+ path = '/a/config/path'
369+ self.assertEqual(path, squid.format_config_path(path))
370+
371+ def test_kill_squid(self):
372+ """Test killing squid."""
373+ called = []
374+
375+ def fake_kill(pid, kill_signal):
376+ """Fake os.kill."""
377+ called.append(('kill', pid, kill_signal))
378+
379+ self.patch(squid, 'kill', fake_kill)
380+
381+ squid_pid = 4
382+ squid.kill_squid(squid_pid)
383+ self.assertIn(('kill', squid_pid, signal.SIGKILL), called)
384
385=== added file 'ubuntuone/devtools/services/tests/test_squid_windows.py'
386--- ubuntuone/devtools/services/tests/test_squid_windows.py 1970-01-01 00:00:00 +0000
387+++ ubuntuone/devtools/services/tests/test_squid_windows.py 2012-04-30 11:07:20 +0000
388@@ -0,0 +1,108 @@
389+#
390+# Copyright 2012 Canonical Ltd.
391+#
392+# This program is free software: you can redistribute it and/or modify it
393+# under the terms of the GNU General Public License version 3, as published
394+# by the Free Software Foundation.
395+#
396+# This program is distributed in the hope that it will be useful, but
397+# WITHOUT ANY WARRANTY; without even the implied warranties of
398+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
399+# PURPOSE. See the GNU General Public License for more details.
400+#
401+# You should have received a copy of the GNU General Public License along
402+# with this program. If not, see <http://www.gnu.org/licenses/>.
403+#
404+# In addition, as a special exception, the copyright holders give
405+# permission to link the code of portions of this program with the
406+# OpenSSL library under certain conditions as described in each
407+# individual source file, and distribute linked combinations
408+# including the two.
409+# You must obey the GNU General Public License in all respects
410+# for all of the code used other than OpenSSL. If you modify
411+# file(s) with this exception, you may extend this exception to your
412+# version of the file(s), but you are not obligated to do so. If you
413+# do not wish to do so, delete this exception statement from your
414+# version. If you delete this exception statement from all source
415+# files in the program, then also delete it here.
416+"""Tests for the windows squid bits."""
417+
418+# pylint: disable=F0401
419+import win32api
420+import win32con
421+# pylint: enable=F0401
422+
423+from twisted.trial.unittest import TestCase
424+
425+from ubuntuone.devtools.services import squid
426+
427+
428+class SquidWindowsTestCase(TestCase):
429+ """"Test the different windows bits."""
430+
431+ def test_get_auth_process(self):
432+ """Test getting the auth process for squid3."""
433+ called = []
434+
435+ self.patch(squid, 'find_executable', lambda _: None)
436+
437+ def fake_format(path):
438+ """Fake format of a config path."""
439+ called.append(('format', path))
440+ return path
441+
442+ self.patch(squid, 'format_config_path', fake_format)
443+ self.assertEqual(squid.AUTH_PROCESS_PATH,
444+ squid.get_auth_process_path(3))
445+ self.assertIn(('format', squid.AUTH_PROCESS_PATH), called)
446+
447+ def test_get_auth_process_path(self):
448+ """Test getting the auth process."""
449+ called = []
450+
451+ exec_path = '/path/to/exec'
452+ self.patch(squid, 'find_executable', lambda _: exec_path)
453+
454+ def fake_format(path):
455+ """Fake format of a config path."""
456+ called.append(('format', path))
457+ return path
458+
459+ self.patch(squid, 'format_config_path', fake_format)
460+ self.assertEqual(exec_path, squid.get_auth_process_path(3))
461+ self.assertIn(('format', exec_path), called)
462+
463+ def test_format_config_path(self):
464+ """Test formating a config path."""
465+ path = '\\a\\config\\path'
466+ expected = path.replace('\\', '\\\\')
467+ self.assertEqual(expected, squid.format_config_path(path))
468+
469+ def test_kill_squid(self):
470+ """Test killing squid."""
471+ called = []
472+
473+ def fake_open_process(access, inherit, pid):
474+ """A fake open process."""
475+ called.append(('OpenProcess', access, inherit, pid))
476+ return pid
477+
478+ self.patch(win32api, 'OpenProcess', fake_open_process)
479+
480+ def fake_terminate(handle, exit_code):
481+ """Fake terminate the process."""
482+ called.append(('TerminateProcess', handle, exit_code))
483+
484+ self.patch(win32api, 'TerminateProcess', fake_terminate)
485+
486+ def fake_close_handle(handle):
487+ """Fale closing a handle."""
488+ called.append(('CloseHandle', handle))
489+
490+ self.patch(win32api, 'CloseHandle', fake_close_handle)
491+ squid_pid = 4
492+ squid.kill_squid(squid_pid)
493+ self.assertIn(('OpenProcess', win32con.PROCESS_TERMINATE, 0,
494+ squid_pid), called)
495+ self.assertIn(('TerminateProcess', squid_pid, 0), called)
496+ self.assertIn(('CloseHandle', squid_pid), called)
497
498=== modified file 'ubuntuone/devtools/testcases/tests/test_squid_testcase.py'
499--- ubuntuone/devtools/testcases/tests/test_squid_testcase.py 2012-03-30 17:44:03 +0000
500+++ ubuntuone/devtools/testcases/tests/test_squid_testcase.py 2012-04-30 11:07:20 +0000
501@@ -192,7 +192,7 @@
502 """Build the url for this mock server."""
503 #pylint: disable=W0212
504 port_num = self.tcpserver._port.getHost().port
505- return "http://localhost:%d/" % port_num
506+ return "http://127.0.0.1:%d/" % port_num
507
508 @defer.inlineCallbacks
509 def stop(self):

Subscribers

People subscribed via source and target branches

to all changes: