Merge lp:~jonas-drange/ubuntu-system-settings/hotspots-change-test-backend into lp:ubuntu-system-settings

Proposed by Jonas G. Drange
Status: Merged
Merged at revision: 1501
Proposed branch: lp:~jonas-drange/ubuntu-system-settings/hotspots-change-test-backend
Merge into: lp:ubuntu-system-settings
Prerequisite: lp:~pete-woods/ubuntu-system-settings/hotspots-via-indicator
Diff against target: 558 lines (+297/-172)
4 files modified
tests/autopilot/ubuntu_system_settings/tests/__init__.py (+38/-84)
tests/autopilot/ubuntu_system_settings/tests/connectivity.py (+131/-0)
tests/autopilot/ubuntu_system_settings/tests/indicatornetwork.py (+52/-0)
tests/autopilot/ubuntu_system_settings/tests/test_cellular.py (+76/-88)
To merge this branch: bzr merge lp:~jonas-drange/ubuntu-system-settings/hotspots-change-test-backend
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Needs Fixing
Ken VanDine Approve
Review via email: mp+265530@code.launchpad.net

Commit message

[cellular] mock Connectivity and indicator-network so that we can test the Wi-Fi Hotspots backend

Description of the change

[cellular] mock Connectivity and indicator-network so that we can test the Wi-Fi Hotspots backend

We also remove the ConnectivityMixing now that there's a mock for it.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Ken VanDine (ken-vandine) wrote :

There are some unrelated fixes in this merge proposal, please split those out. Otherwise this looks great!

review: Needs Fixing
Revision history for this message
Ken VanDine (ken-vandine) wrote :

Perfect, thanks!

review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'tests/autopilot/ubuntu_system_settings/tests/__init__.py'
2--- tests/autopilot/ubuntu_system_settings/tests/__init__.py 2015-07-24 20:00:35 +0000
3+++ tests/autopilot/ubuntu_system_settings/tests/__init__.py 2015-07-24 20:00:36 +0000
4@@ -33,6 +33,10 @@
5 from fixtures import EnvironmentVariable
6 from gi.repository import UPowerGlib
7 from testtools.matchers import Equals, NotEquals, GreaterThan
8+from ubuntu_system_settings.tests.connectivity import (
9+ PRIV_OBJ as CTV_PRIV_OBJ, NETS_OBJ as CTV_NETS_OBJ,
10+ MAIN_IFACE as CTV_IFACE
11+)
12
13 ACCOUNTS_IFACE = 'org.freedesktop.Accounts'
14 ACCOUNTS_USER_IFACE = 'org.freedesktop.Accounts.User'
15@@ -380,65 +384,47 @@
16
17 class HotspotBaseTestCase(CellularBaseTestCase):
18
19+ connectivity_parameters = {}
20+
21 @classmethod
22 def setUpClass(cls):
23+ cls.start_session_bus()
24+ cls.dbus_con = cls.get_dbus(False)
25+
26+ # Connectivity API Mock
27+ ctv_tmpl = os.path.join(os.path.dirname(__file__), 'connectivity.py')
28+ (cls.ctv_mock, cls.obj_ctv) = cls.spawn_server_template(
29+ ctv_tmpl, parameters=cls.connectivity_parameters,
30+ stdout=subprocess.PIPE)
31+
32+ cls.ctv_private = dbus.Interface(
33+ cls.dbus_con.get_object(CTV_IFACE, CTV_PRIV_OBJ),
34+ 'org.freedesktop.DBus.Properties')
35+
36+ cls.ctv_nets = dbus.Interface(
37+ cls.dbus_con.get_object(CTV_IFACE, CTV_NETS_OBJ),
38+ 'org.freedesktop.DBus.Properties')
39+
40+ # indicator-network mock
41+ inetwork = os.path.join(
42+ os.path.dirname(__file__), 'indicatornetwork.py'
43+ )
44+ (cls.inetwork_mock, cls.obj_inetwork) = cls.spawn_server_template(
45+ inetwork, stdout=subprocess.PIPE)
46+
47 super(HotspotBaseTestCase, cls).setUpClass()
48- nm_tmpl = os.path.join(os.path.dirname(__file__), 'networkmanager.py')
49- (cls.n_mock, cls.obj_nm) = cls.spawn_server_template(
50- nm_tmpl, stdout=subprocess.PIPE)
51- (cls.u_mock, cls.obj_urf) = cls.spawn_server_template(
52- 'urfkill', stdout=subprocess.PIPE)
53-
54- @classmethod
55- def tearDownClass(cls):
56- cls.n_mock.terminate()
57- cls.n_mock.wait()
58- cls.u_mock.terminate()
59- cls.u_mock.wait()
60- super(HotspotBaseTestCase, cls).tearDownClass()
61-
62- def tearDown(self):
63- self.obj_nm.Reset()
64- self.urfkill_mock.ClearCalls()
65- super(HotspotBaseTestCase, self).tearDown()
66+
67+ def start_network_indicator(self):
68+ subprocess.call(['initctl', 'start', 'indicator-network'])
69+
70+ def stop_network_indicator(self):
71+ subprocess.call(['initctl', 'stop', 'indicator-network'])
72
73 def setUp(self):
74- self.patch_environment("USS_SHOW_ALL_UI", "1")
75- self.nm_mock = dbus.Interface(self.obj_nm, dbusmock.MOCK_IFACE)
76- self.device_path = self.obj_nm.AddWiFiDevice('test0', 'Barbaz', 1)
77- self.device_mock = dbus.Interface(self.dbus_con.get_object(
78- NM_SERVICE, self.device_path),
79- 'org.freedesktop.DBus.Properties')
80- self.urfkill_mock = dbus.Interface(self.obj_urf, dbusmock.MOCK_IFACE)
81+ self.stop_network_indicator()
82+ self.addCleanup(self.start_network_indicator)
83 super(HotspotBaseTestCase, self).setUp()
84
85- def add_hotspot(self, name, password, secured=True, enabled=False):
86- settings = {
87- 'connection': {
88- 'id': dbus.String('Test AP', variant_level=1),
89- 'type': dbus.String('802-11-wireless', variant_level=1), },
90- '802-11-wireless': {
91- 'mode': dbus.String('ap', variant_level=1),
92- 'ssid': dbus.String(name, variant_level=1),
93- }
94- }
95-
96- if secured:
97- settings['802-11-wireless']['security'] = dbus.String(
98- '802-11-wireless-security', variant_level=1)
99- settings['802-11-wireless-security'] = {
100- 'auth-alg': dbus.String('shared', variant_level=1),
101- 'key-mgmt': dbus.String('wpa-psk', variant_level=1),
102- 'psk': dbus.String(password, variant_level=1),
103- }
104-
105- if enabled:
106- settings['connection']['autoconnect'] = True
107-
108- connection_path = self.obj_nm.SettingsAddConnection(settings)
109-
110- return connection_path
111-
112
113 class BluetoothBaseTestCase(UbuntuSystemSettingsTestCase):
114
115@@ -831,38 +817,6 @@
116 super(ResetBaseTestCase, self).tearDown()
117
118
119-class ConnectivityMixin(dbusmock.DBusTestCase):
120- @classmethod
121- def setUpClass(cls):
122- cls.start_session_bus()
123- cls.connectivity_dbus = cls.get_dbus()
124- cls.connectivity_server = cls.spawn_server(CON_SERVICE,
125- CON_PATH,
126- CON_IFACE,
127- system_bus=False,
128- stdout=subprocess.PIPE)
129-
130- cls.connectivity_mock = dbus.Interface(
131- cls.connectivity_dbus.get_object(CON_SERVICE,
132- CON_PATH),
133- dbusmock.MOCK_IFACE)
134-
135- cls.connectivity_mock.AddMethod('', 'UnlockModem', 's', '', '')
136- super(ConnectivityMixin, cls).setUpClass()
137-
138- def setUp(self):
139- self.wait_for_bus_object(CON_SERVICE,
140- CON_PATH,
141- system_bus=False)
142- super(ConnectivityMixin, self).setUp()
143-
144- @classmethod
145- def tearDownClass(cls):
146- cls.connectivity_server.terminate()
147- cls.connectivity_server.wait()
148- super(ConnectivityMixin, cls).tearDownClass()
149-
150-
151 class SecurityBaseTestCase(UbuntuSystemSettingsOfonoTestCase):
152 """ Base class for security and privacy settings tests"""
153
154
155=== added file 'tests/autopilot/ubuntu_system_settings/tests/connectivity.py'
156--- tests/autopilot/ubuntu_system_settings/tests/connectivity.py 1970-01-01 00:00:00 +0000
157+++ tests/autopilot/ubuntu_system_settings/tests/connectivity.py 2015-07-24 20:00:36 +0000
158@@ -0,0 +1,131 @@
159+'''connectivity D-BUS mock template'''
160+
161+# This program is free software; you can redistribute it and/or modify it under
162+# the terms of the GNU Lesser General Public License as published by the Free
163+# Software Foundation; either version 3 of the License, or (at your option) any
164+# later version. See http://www.gnu.org/copyleft/lgpl.html for the full text
165+# of the license.
166+
167+__author__ = 'Jonas G. Drange'
168+__email__ = 'jonas.drange@canonical.com'
169+__copyright__ = '(c) 2015 Canonical Ltd.'
170+__license__ = 'LGPL 3+'
171+
172+
173+import dbus
174+import dbusmock
175+
176+BUS_NAME = 'com.ubuntu.connectivity1'
177+MAIN_IFACE = 'com.ubuntu.connectivity1'
178+MAIN_OBJ = '/'
179+SYSTEM_BUS = False
180+
181+PRIV_IFACE = 'com.ubuntu.connectivity1.Private'
182+PRIV_OBJ = '/com/ubuntu/connectivity1/Private'
183+
184+NETS_IFACE = 'com.ubuntu.connectivity1.NetworkingStatus'
185+NETS_OBJ = '/com/ubuntu/connectivity1/NetworkingStatus'
186+
187+NOT_IMPLEMENTED = '''raise dbus.exceptions.DBusException(
188+ "org.ofono.Error.NotImplemented")'''
189+
190+_parameters = {}
191+
192+
193+def set_hotspot_enabled(self, value):
194+ self.SetProperty(NETS_OBJ, NETS_IFACE, 'HotspotEnabled', value)
195+
196+ # Set HotspotStored = True if not stored and we're enabling it.
197+ stored = dbusmock.get_object(NETS_OBJ).Get(NETS_IFACE, 'HotspotStored')
198+ if value and not bool(stored):
199+ self.SetProperty(NETS_OBJ, NETS_IFACE, 'HotspotStored', True)
200+
201+
202+def set_hotspot_ssid(self, value):
203+ self.SetProperty(NETS_OBJ, NETS_IFACE, 'HotspotSsid', value)
204+
205+
206+def set_hotspot_password(self, value):
207+ self.SetProperty(PRIV_OBJ, PRIV_IFACE, 'HotspotPassword', value)
208+
209+
210+def load(mock, parameters):
211+ global _parameters
212+ _parameters = parameters
213+
214+ mock.set_hotspot_enabled = set_hotspot_enabled
215+ mock.set_hotspot_ssid = set_hotspot_ssid
216+ mock.set_hotspot_password = set_hotspot_password
217+
218+ mock.modems = [] # path to boolean e.g. ril_0: False
219+ mock.flight_mode = False
220+ mock.wifi_enabled = False
221+
222+ mock.AddObject(
223+ NETS_OBJ,
224+ NETS_IFACE,
225+ {
226+ 'HotspotSsid': _parameters.get(
227+ 'HotspotSsid', dbus.ByteArray('Ubuntu'.encode('UTF-8'))),
228+ 'HotspotEnabled': _parameters.get(
229+ 'HotspotEnabled', dbus.Boolean(False)),
230+ 'HotspotMode': _parameters.get('HotspotMode', dbus.String('ap')),
231+ 'HotspotStored': _parameters.get(
232+ 'HotspotStored', dbus.Boolean(False)
233+ ),
234+ 'UnstoppableOperationHappening': dbus.Boolean(False),
235+ },
236+ []
237+ )
238+
239+ mock.AddObject(
240+ PRIV_OBJ,
241+ PRIV_IFACE,
242+ {
243+ 'HotspotPassword': _parameters.get(
244+ 'HotspotPassword', dbus.String('abcdefgh')
245+ )
246+ },
247+ [
248+ (
249+ 'UnlockAllModems', '', '',
250+ ''
251+ ),
252+ (
253+ 'UnlockModem', 's', '',
254+ ''
255+ ),
256+ (
257+ 'SetFlightMode', 'b', '',
258+ ''
259+ ),
260+ (
261+ 'SetWifiEnabled', 'b', '',
262+ ''
263+ ),
264+ (
265+ 'SetHotspotSsid', 'ay', '',
266+ 'objects["/"].set_hotspot_ssid(self, args[0])'
267+ ),
268+ (
269+ 'SetHotspotPassword', 's', '',
270+ 'objects["/"].set_hotspot_password(self, args[0])'
271+ ),
272+ (
273+ 'SetHotspotEnabled', 'b', '',
274+ 'objects["/"].set_hotspot_enabled(self, args[0])'
275+ ),
276+ (
277+ 'SetHotspotMode', 's', '',
278+ ''
279+ )
280+ ]
281+ )
282+
283+
284+@dbus.service.method(dbusmock.MOCK_IFACE,
285+ in_signature='sssv', out_signature='')
286+def SetProperty(self, path, iface, name, value):
287+ obj = dbusmock.get_object(path)
288+ obj.Set(iface, name, value)
289+ obj.EmitSignal(iface, 'PropertiesChanged', 'a{sv}', [{name: value}])
290
291=== added file 'tests/autopilot/ubuntu_system_settings/tests/indicatornetwork.py'
292--- tests/autopilot/ubuntu_system_settings/tests/indicatornetwork.py 1970-01-01 00:00:00 +0000
293+++ tests/autopilot/ubuntu_system_settings/tests/indicatornetwork.py 2015-07-24 20:00:36 +0000
294@@ -0,0 +1,52 @@
295+'''indicator-network D-BUS mock template'''
296+
297+# This program is free software; you can redistribute it and/or modify it under
298+# the terms of the GNU Lesser General Public License as published by the Free
299+# Software Foundation; either version 3 of the License, or (at your option) any
300+# later version. See http://www.gnu.org/copyleft/lgpl.html for the full text
301+# of the license.
302+
303+__author__ = 'Jonas G. Drange'
304+__email__ = 'jonas.drange@canonical.com'
305+__copyright__ = '(c) 2015 Canonical Ltd.'
306+__license__ = 'LGPL 3+'
307+
308+
309+BUS_NAME = 'com.canonical.indicator.network'
310+MAIN_IFACE = 'org.gtk.Actions'
311+MAIN_OBJ = '/com/canonical/indicator/network'
312+SYSTEM_BUS = False
313+
314+NOT_IMPLEMENTED = '''raise dbus.exceptions.DBusException(
315+ "org.ofono.Error.NotImplemented")'''
316+
317+_parameters = {}
318+
319+
320+def load(mock, parameters):
321+ global _parameters
322+ _parameters = parameters
323+
324+ mock.AddMethods(
325+ MAIN_IFACE,
326+ [
327+ (
328+ 'Activate', 'sava{sv}', '', ''
329+ ),
330+ (
331+ 'Describe', 's', '(bgav)',
332+ 'if args[0] == "wifi.enable":'
333+ ' ret = (True, "", [True])'
334+ ),
335+ (
336+ 'DescribeAll', '', 'a{s(bgav)}',
337+ 'ret = {"wifi.enable": (True, "", [True])}'
338+ ),
339+ (
340+ 'List', '', 'as',
341+ 'ret = ["wifi.enable"]'
342+ ),
343+ (
344+ 'SetState', 'sva{sv}', '', ''
345+ )
346+ ])
347
348=== modified file 'tests/autopilot/ubuntu_system_settings/tests/test_cellular.py'
349--- tests/autopilot/ubuntu_system_settings/tests/test_cellular.py 2015-07-24 20:00:35 +0000
350+++ tests/autopilot/ubuntu_system_settings/tests/test_cellular.py 2015-07-24 20:00:36 +0000
351@@ -5,7 +5,6 @@
352 # under the terms of the GNU General Public License version 3, as published
353 # by the Free Software Foundation.
354
355-import dbus
356 from gi.repository import Gio, GLib
357 from time import sleep
358
359@@ -17,8 +16,9 @@
360 CellularBaseTestCase, HotspotBaseTestCase, CONNMAN_IFACE, RDO_IFACE,
361 NETREG_IFACE)
362
363-from ubuntu_system_settings.tests.networkmanager import (
364- CSETTINGS_IFACE, MAIN_OBJ as NM_PATH, MAIN_IFACE as NM_IFACE,
365+
366+from ubuntu_system_settings.tests.connectivity import (
367+ PRIV_IFACE as CTV_PRIV_IFACE, NETS_IFACE as CTV_NETS_IFACE
368 )
369
370 DEV_IFACE = 'org.freedesktop.NetworkManager.Device'
371@@ -298,114 +298,102 @@
372 Eventually(Equals('/ril_1')))
373
374
375-class HotspotTestCase(HotspotBaseTestCase):
376+class HotspotNonExistantTestCase(HotspotBaseTestCase):
377+
378+ connectivity_parameters = {
379+ 'HotspotEnabled': False,
380+ 'HotspotStored': False
381+ }
382
383 def test_setup(self):
384- if not self.cellular_page.have_hotspot():
385- self.skipTest('Cannot test hotspot since wifi is disabled.')
386-
387- ssid = 'Ubuntu'
388- password = 'abcdefgh'
389- config = {'password': password}
390- active_con_path = NM_PATH + '/ActiveConnection/0'
391- con_path = NM_PATH + '/Settings/0'
392+ sleep(10)
393+ ssid = 'bar'
394+ password = 'zomgzomg'
395+ config = {'ssid': ssid, 'password': password}
396
397 hotspot_page = self.cellular_page.setup_hotspot(config)
398
399 # Assert that the switch is on.
400 self.assertTrue(hotspot_page.get_hotspot_status())
401
402- # Assert that Block on Urfkill is called twice.
403- self.assertThat(
404- lambda: len(self.urfkill_mock.GetMethodCalls('Block')),
405- Eventually(Equals(2))
406- )
407-
408- # Assert that we get one active connection.
409- self.assertThat(
410- lambda: len(self.obj_nm.GetAll(NM_IFACE)['ActiveConnections']),
411- Eventually(Equals(1))
412- )
413-
414- # Assert that the active connection has a certain path.
415- self.assertThat(
416- lambda: self.obj_nm.GetAll(NM_IFACE)['ActiveConnections'][0],
417- Eventually(Equals(active_con_path))
418- )
419-
420- # Assert the device's active connection
421- self.assertThat(
422- lambda: self.device_mock.Get(DEV_IFACE, 'ActiveConnection'),
423- Eventually(Equals(active_con_path))
424- )
425-
426- connection_mock = dbus.Interface(self.dbus_con.get_object(
427- NM_IFACE, con_path), CSETTINGS_IFACE)
428-
429- settings = connection_mock.GetSettings()
430-
431- # Assert that autoconnect is true, that ssid and password is what we
432- # expect them to be.
433- self.assertTrue(settings['connection']['autoconnect'])
434-
435- s_ssid = bytearray(settings['802-11-wireless']['ssid']).decode('utf-8')
436- self.assertEqual(s_ssid, ssid)
437- self.assertEqual(settings['802-11-wireless-security']['psk'], password)
438+ self.assertThat(
439+ lambda: self.ctv_nets.Get(CTV_NETS_IFACE, 'HotspotEnabled'),
440+ Eventually(Equals(True))
441+ )
442+
443+ self.assertThat(
444+ lambda: bytearray(
445+ self.ctv_nets.Get(CTV_NETS_IFACE, 'HotspotSsid')
446+ ).decode('UTF-8'),
447+ Eventually(Equals(ssid))
448+ )
449+
450+ self.assertThat(
451+ lambda: self.ctv_private.Get(CTV_PRIV_IFACE, 'HotspotPassword'),
452+ Eventually(Equals(password))
453+ )
454+
455+ self.assertThat(
456+ lambda: self.ctv_nets.Get(CTV_NETS_IFACE, 'HotspotStored'),
457+ Eventually(Equals(True))
458+ )
459+
460+
461+class HotspotExistsTestCase(HotspotBaseTestCase):
462+
463+ connectivity_parameters = {
464+ 'HotspotStored': True
465+ }
466
467 def test_enabling(self):
468- if not self.cellular_page.have_hotspot():
469- self.skipTest('Cannot test hotspot since wifi is disabled.')
470-
471- self.add_hotspot('foo', 'abcdefgh', enabled=False)
472-
473 self.assertThat(
474- lambda: len(self.obj_nm.GetAll(NM_IFACE)['ActiveConnections']),
475- Eventually(Equals(0))
476+ lambda: self.ctv_nets.Get(CTV_NETS_IFACE, 'HotspotEnabled'),
477+ Eventually(Equals(False))
478 )
479
480 self.cellular_page.enable_hotspot()
481
482 self.assertThat(
483- lambda: len(self.obj_nm.GetAll(NM_IFACE)['ActiveConnections']),
484- Eventually(Equals(1))
485- )
486+ lambda: self.ctv_nets.Get(CTV_NETS_IFACE, 'HotspotEnabled'),
487+ Eventually(Equals(True))
488+ )
489+
490+ def test_changing(self):
491+ ssid = 'bar'
492+ password = 'zomgzomg'
493+ config = {'ssid': ssid, 'password': password}
494+ self.cellular_page.setup_hotspot(config)
495+
496+ self.assertThat(
497+ lambda: bytearray(
498+ self.ctv_nets.Get(CTV_NETS_IFACE, 'HotspotSsid')
499+ ).decode('UTF-8'),
500+ Eventually(Equals(ssid))
501+ )
502+
503+ self.assertThat(
504+ lambda: self.ctv_private.Get(CTV_PRIV_IFACE, 'HotspotPassword'),
505+ Eventually(Equals(password))
506+ )
507+
508+
509+class HotspotEnabledTestCase(HotspotBaseTestCase):
510+
511+ connectivity_parameters = {
512+ 'HotspotStored': True,
513+ 'HotspotEnabled': True
514+ }
515
516 def test_disabling(self):
517- if not self.cellular_page.have_hotspot():
518- self.skipTest('Cannot test hotspot since wifi is disabled.')
519-
520- self.add_hotspot('foo', 'abcdefgh', enabled=True)
521
522 self.assertThat(
523- lambda: len(self.obj_nm.GetAll(NM_IFACE)['ActiveConnections']),
524- Eventually(Equals(1))
525+ lambda: self.ctv_nets.Get(CTV_NETS_IFACE, 'HotspotEnabled'),
526+ Eventually(Equals(True))
527 )
528
529 self.cellular_page.disable_hotspot()
530
531 self.assertThat(
532- lambda: len(self.obj_nm.GetAll(NM_IFACE)['ActiveConnections']),
533- Eventually(Equals(0))
534+ lambda: self.ctv_nets.Get(CTV_NETS_IFACE, 'HotspotEnabled'),
535+ Eventually(Equals(False))
536 )
537-
538- def test_changing(self):
539- if not self.cellular_page.have_hotspot():
540- self.skipTest('Cannot test hotspot since wifi is disabled.')
541-
542- con_path = self.add_hotspot('foo', 'abcdefgh', enabled=True)
543-
544- ssid = 'bar'
545- password = 'zomgzomg'
546- config = {'ssid': ssid, 'password': password}
547- self.cellular_page.setup_hotspot(config)
548-
549- con_path = NM_PATH + '/Settings/0'
550-
551- con_mock = dbus.Interface(self.dbus_con.get_object(
552- NM_IFACE, con_path), CSETTINGS_IFACE)
553-
554- settings = con_mock.GetSettings()
555-
556- s_ssid = bytearray(settings['802-11-wireless']['ssid']).decode('utf-8')
557- self.assertEqual(s_ssid, ssid)
558- self.assertEqual(settings['802-11-wireless-security']['psk'], password)

Subscribers

People subscribed via source and target branches