Merge lp:~nataliabidart/ubuntuone-control-panel/start-dc-on-backend into lp:ubuntuone-control-panel

Proposed by Natalia Bidart
Status: Merged
Approved by: Natalia Bidart
Approved revision: 56
Merged at revision: 45
Proposed branch: lp:~nataliabidart/ubuntuone-control-panel/start-dc-on-backend
Merge into: lp:ubuntuone-control-panel
Prerequisite: lp:~nataliabidart/ubuntuone-control-panel/replication-to-the-backend
Diff against target: 932 lines (+326/-242)
7 files modified
data/install.ui (+7/-2)
po/POTFILES.in (+2/-1)
pylintrc (+1/-1)
ubuntuone/controlpanel/gtk/gui.py (+114/-81)
ubuntuone/controlpanel/gtk/tests/__init__.py (+14/-13)
ubuntuone/controlpanel/gtk/tests/test_gui.py (+187/-140)
ubuntuone/controlpanel/logger.py (+1/-4)
To merge this branch: bzr merge lp:~nataliabidart/ubuntuone-control-panel/start-dc-on-backend
Reviewer Review Type Date Requested Status
Roberto Alsina (community) Approve
Eric Casteleijn (community) Approve
Review via email: mp+45448@code.launchpad.net

This proposal supersedes a proposal from 2011-01-06.

Commit message

Desktopcouch replication service is accessed through the backend using its DBus service (LP: #696782).

Description of the change

To test, first check that all the desktopcouch related packages are uninstalled, and that no desktopcouch nor couchdb process is running. This meaning:

sudo apt-cache search couch

should show no package installed, and:

ps aux | grep couch

should return no results. Then, from this branch, run:

terminal 1: DEBUG=True PYTHONPATH=. ./bin/ubuntuone-control-panel-backend
terminal 2: DEBUG=True PYTHONPATH=. ./bin/ubuntuone-control-panel-gtk

And go to the third tab and install dc-u1 from there, and keep playing. You should get no hangs at all in the UI.

To post a comment you must log in.
55. By Natalia Bidart

Avoiding conflicts.

56. By Natalia Bidart

Merged dependency branch in.

Revision history for this message
Eric Casteleijn (thisfred) wrote :

Works as advertised, code looks good. I don't like the desktopcouch pairing functionality as it stands, but that's in the design, so we'll discuss elsewhere.

review: Approve
Revision history for this message
Roberto Alsina (ralsina) wrote :

Does what it says. +1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'data/install.ui'
--- data/install.ui 2010-12-24 14:53:03 +0000
+++ data/install.ui 2011-01-06 21:03:06 +0000
@@ -23,11 +23,12 @@
23 <property name="visible">True</property>23 <property name="visible">True</property>
24 <child>24 <child>
25 <object class="GtkButton" id="install_button">25 <object class="GtkButton" id="install_button">
26 <property name="label">gtk-ok</property>26 <property name="label" translatable="yes">_Install now</property>
27 <property name="visible">True</property>27 <property name="visible">True</property>
28 <property name="can_focus">True</property>28 <property name="can_focus">True</property>
29 <property name="receives_default">True</property>29 <property name="receives_default">True</property>
30 <property name="use_stock">True</property>30 <property name="image">image1</property>
31 <property name="use_underline">True</property>
31 <signal name="clicked" handler="on_install_button_clicked"/>32 <signal name="clicked" handler="on_install_button_clicked"/>
32 </object>33 </object>
33 <packing>34 <packing>
@@ -43,4 +44,8 @@
43 </packing>44 </packing>
44 </child>45 </child>
45 </object>46 </object>
47 <object class="GtkImage" id="image1">
48 <property name="visible">True</property>
49 <property name="stock">gtk-ok</property>
50 </object>
46</interface>51</interface>
4752
=== modified file 'po/POTFILES.in'
--- po/POTFILES.in 2011-01-04 16:12:56 +0000
+++ po/POTFILES.in 2011-01-06 21:03:06 +0000
@@ -1,8 +1,9 @@
1ubuntuone-control-panel-gtk.desktop.in1ubuntuone-control-panel-gtk.desktop.in
2ubuntuone/controlpanel/gtk/gui.py2ubuntuone/controlpanel/gtk/gui.py
3[type: gettext/glade] data/dashboard.ui3[type: gettext/glade] data/dashboard.ui
4[type: gettext/glade] data/services.ui
5[type: gettext/glade] data/device.ui4[type: gettext/glade] data/device.ui
6[type: gettext/glade] data/devices.ui5[type: gettext/glade] data/devices.ui
6[type: gettext/glade] data/install.ui
7[type: gettext/glade] data/management.ui7[type: gettext/glade] data/management.ui
8[type: gettext/glade] data/overview.ui8[type: gettext/glade] data/overview.ui
9[type: gettext/glade] data/services.ui
910
=== modified file 'pylintrc'
--- pylintrc 2010-10-13 18:55:23 +0000
+++ pylintrc 2011-01-06 21:03:06 +0000
@@ -272,7 +272,7 @@
272max-line-length=79272max-line-length=79
273273
274# Maximum number of lines in a module274# Maximum number of lines in a module
275max-module-lines=2000275max-module-lines=2500
276276
277# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1277# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
278# tab).278# tab).
279279
=== modified file 'ubuntuone/controlpanel/gtk/gui.py'
--- ubuntuone/controlpanel/gtk/gui.py 2010-12-26 15:02:52 +0000
+++ ubuntuone/controlpanel/gtk/gui.py 2011-01-06 21:03:06 +0000
@@ -78,6 +78,12 @@
78VALUE_ERROR = _('Value could not be retrieved.')78VALUE_ERROR = _('Value could not be retrieved.')
79WARNING_MARKUP = '<span foreground="%s"><b>%%s</b></span>' % ORANGE79WARNING_MARKUP = '<span foreground="%s"><b>%%s</b></span>' % ORANGE
80KILOBYTES = 102480KILOBYTES = 1024
81NO_OP = lambda *a, **kw: None
82
83
84def error_handler(*args, **kwargs):
85 """Log errors when calling D-Bus methods in a async way."""
86 logger.error('Error handler received: %r, %r', args, kwargs)
8187
8288
83def filter_by_app_name(f):89def filter_by_app_name(f):
@@ -352,7 +358,8 @@
352 settings = {TC_URL_KEY: U1_TC_URL, HELP_TEXT_KEY: U1_DESCRIPTION,358 settings = {TC_URL_KEY: U1_TC_URL, HELP_TEXT_KEY: U1_DESCRIPTION,
353 WINDOW_ID_KEY: str(self._window_id),359 WINDOW_ID_KEY: str(self._window_id),
354 PING_URL_KEY: U1_PING_URL}360 PING_URL_KEY: U1_PING_URL}
355 self.sso_backend.register(U1_APP_NAME, settings)361 self.sso_backend.register(U1_APP_NAME, settings,
362 reply_handler=NO_OP, error_handler=error_handler)
356 self.set_property('greyed', True)363 self.set_property('greyed', True)
357 self.warning_label.set_text('')364 self.warning_label.set_text('')
358365
@@ -361,7 +368,8 @@
361 settings = {TC_URL_KEY: U1_TC_URL, HELP_TEXT_KEY: U1_DESCRIPTION,368 settings = {TC_URL_KEY: U1_TC_URL, HELP_TEXT_KEY: U1_DESCRIPTION,
362 WINDOW_ID_KEY: str(self._window_id),369 WINDOW_ID_KEY: str(self._window_id),
363 PING_URL_KEY: U1_PING_URL}370 PING_URL_KEY: U1_PING_URL}
364 self.sso_backend.login(U1_APP_NAME, settings)371 self.sso_backend.login(U1_APP_NAME, settings,
372 reply_handler=NO_OP, error_handler=error_handler)
365 self.set_property('greyed', True)373 self.set_property('greyed', True)
366 self.warning_label.set_text('')374 self.warning_label.set_text('')
367375
@@ -408,7 +416,8 @@
408 else:416 else:
409 self.set_sensitive(True)417 self.set_sensitive(True)
410 self.warning_label.set_text('')418 self.warning_label.set_text('')
411 self.sso_backend.find_credentials(U1_APP_NAME, {})419 self.sso_backend.find_credentials(U1_APP_NAME, {},
420 reply_handler=NO_OP, error_handler=error_handler)
412421
413422
414class DashboardPanel(UbuntuOneBin, ControlPanelMixin):423class DashboardPanel(UbuntuOneBin, ControlPanelMixin):
@@ -516,11 +525,13 @@
516 volume_id = checkbutton.get_label()525 volume_id = checkbutton.get_label()
517 subscribed = bool_str(checkbutton.get_active())526 subscribed = bool_str(checkbutton.get_active())
518 self.backend.change_volume_settings(volume_id,527 self.backend.change_volume_settings(volume_id,
519 {'subscribed': subscribed})528 {'subscribed': subscribed},
529 reply_handler=NO_OP, error_handler=error_handler)
520530
521 def load(self):531 def load(self):
522 """Load the volume list."""532 """Load the volume list."""
523 self.backend.volumes_info()533 self.backend.volumes_info(reply_handler=NO_OP,
534 error_handler=error_handler)
524 self.message.start()535 self.message.start()
525536
526537
@@ -565,7 +576,8 @@
565 # Not disabling the GUI to avoid annyong twitchings576 # Not disabling the GUI to avoid annyong twitchings
566 #self.set_sensitive(False)577 #self.set_sensitive(False)
567 self.warning_label.set_text('')578 self.warning_label.set_text('')
568 self.backend.change_device_settings(self.id, self.__dict__)579 self.backend.change_device_settings(self.id, self.__dict__,
580 reply_handler=NO_OP, error_handler=error_handler)
569581
570 def _block_signals(f):582 def _block_signals(f):
571 """Execute 'f' while having the _updating flag set."""583 """Execute 'f' while having the _updating flag set."""
@@ -591,7 +603,8 @@
591603
592 def on_remove_clicked(self, widget):604 def on_remove_clicked(self, widget):
593 """Remove button was clicked or activated."""605 """Remove button was clicked or activated."""
594 self.backend.remove_device(self.id)606 self.backend.remove_device(self.id,
607 reply_handler=NO_OP, error_handler=error_handler)
595 self.set_sensitive(False)608 self.set_sensitive(False)
596609
597 @_block_signals610 @_block_signals
@@ -747,7 +760,8 @@
747760
748 def load(self):761 def load(self):
749 """Load the device list."""762 """Load the device list."""
750 self.backend.devices_info()763 self.backend.devices_info(reply_handler=NO_OP,
764 error_handler=error_handler)
751 self.message.start()765 self.message.start()
752766
753767
@@ -818,12 +832,18 @@
818class Service(gtk.VBox, ControlPanelMixin):832class Service(gtk.VBox, ControlPanelMixin):
819 """A service."""833 """A service."""
820834
821 def __init__(self, name, localized_name, *args, **kwargs):835 CHANGE_ERROR = _('The settings could not be changed,\n'
836 'previous values were restored.')
837
838 def __init__(self, service_id, name, *args, **kwargs):
822 gtk.VBox.__init__(self)839 gtk.VBox.__init__(self)
823 ControlPanelMixin.__init__(self)840 ControlPanelMixin.__init__(self)
824 self.service_name = name841 self.id = service_id
825842
826 self.button = gtk.CheckButton(label=localized_name)843 self.warning_label = gtk.Label()
844 self.pack_start(self.warning_label, expand=False)
845
846 self.button = gtk.CheckButton(label=name)
827 self.pack_start(self.button, expand=False)847 self.pack_start(self.button, expand=False)
828848
829 self.show_all()849 self.show_all()
@@ -835,15 +855,18 @@
835 FILES_SERVICE_NAME = _('Files')855 FILES_SERVICE_NAME = _('Files')
836856
837 def __init__(self):857 def __init__(self):
838 Service.__init__(self, name='files',858 Service.__init__(self, service_id='files',
839 localized_name=self.FILES_SERVICE_NAME)859 name=self.FILES_SERVICE_NAME)
840860
841 self.set_sensitive(False)861 self.set_sensitive(False)
862
842 self.backend.connect_to_signal('FileSyncStatusChanged',863 self.backend.connect_to_signal('FileSyncStatusChanged',
843 self.on_file_sync_status_changed)864 self.on_file_sync_status_changed)
844 self.backend.connect_to_signal('FilesEnabled', self.on_files_enabled)865 self.backend.connect_to_signal('FilesEnabled', self.on_files_enabled)
845 self.backend.connect_to_signal('FilesDisabled', self.on_files_disabled)866 self.backend.connect_to_signal('FilesDisabled', self.on_files_disabled)
846 self.backend.file_sync_status()867
868 self.backend.file_sync_status(reply_handler=NO_OP,
869 error_handler=error_handler)
847870
848 @log_call(logger.debug)871 @log_call(logger.debug)
849 def on_file_sync_status_changed(self, status):872 def on_file_sync_status_changed(self, status):
@@ -869,19 +892,24 @@
869 """Button was toggled, exclude/replicate the service properly."""892 """Button was toggled, exclude/replicate the service properly."""
870 logger.info('File sync enabled? %r', self.button.get_active())893 logger.info('File sync enabled? %r', self.button.get_active())
871 if self.button.get_active():894 if self.button.get_active():
872 self.backend.enable_files()895 self.backend.enable_files(reply_handler=NO_OP,
896 error_handler=error_handler)
873 else:897 else:
874 self.backend.disable_files()898 self.backend.disable_files(reply_handler=NO_OP,
899 error_handler=error_handler)
875900
876901
877class DesktopcouchService(Service):902class DesktopcouchService(Service):
878 """A desktopcouch service."""903 """A desktopcouch service."""
879904
880 def __init__(self, name, localized_name,905 def __init__(self, service_id, name, enabled, dependency=None):
881 replication_service, dependency=None):906 Service.__init__(self, service_id, name)
882 Service.__init__(self, name, localized_name)907
883 self.replication_service = replication_service908 self.backend.connect_to_signal('ReplicationSettingsChanged',
884 enabled = name not in self.replication_service.all_exclusions()909 self.on_replication_settings_changed)
910 self.backend.connect_to_signal('ReplicationSettingsChangeError',
911 self.on_replication_settings_change_error)
912
885 self.button.set_active(enabled)913 self.button.set_active(enabled)
886914
887 self.dependency = None915 self.dependency = None
@@ -903,11 +931,27 @@
903 def on_button_toggled(self, button):931 def on_button_toggled(self, button):
904 """Button was toggled, exclude/replicate the service properly."""932 """Button was toggled, exclude/replicate the service properly."""
905 logger.info('Starting replication for %r? %r',933 logger.info('Starting replication for %r? %r',
906 self.service_name, self.button.get_active())934 self.id, self.button.get_active())
907 if self.button.get_active():935
908 self.replication_service.replicate(self.service_name)936 args = {'enabled': bool_str(self.button.get_active())}
909 else:937 self.backend.change_replication_settings(self.id, args,
910 self.replication_service.exclude(self.service_name)938 reply_handler=NO_OP, error_handler=error_handler)
939
940 @log_call(logger.info)
941 def on_replication_settings_changed(self, replication_id):
942 """The change of settings for this replication succeded."""
943 if replication_id != self.id:
944 return
945 self.warning_label.set_text('')
946
947 @log_call(logger.error)
948 def on_replication_settings_change_error(self, replication_id,
949 error_dict=None):
950 """The change of settings for this replication failed."""
951 if replication_id != self.id:
952 return
953 self.button.set_active(not self.button.get_active())
954 self._set_warning(self.CHANGE_ERROR, self.warning_label)
911955
912956
913class ServicesPanel(UbuntuOneBin, ControlPanelMixin):957class ServicesPanel(UbuntuOneBin, ControlPanelMixin):
@@ -916,32 +960,33 @@
916 TITLE = _('Ubuntu One services including data sync are enabled for the '960 TITLE = _('Ubuntu One services including data sync are enabled for the '
917 'data types and services listed below.')961 'data types and services listed below.')
918 CHOOSE_SERVICES = _('Choose services to synchronize with this computer:')962 CHOOSE_SERVICES = _('Choose services to synchronize with this computer:')
919 DESKTOPCOUCH_PKG = 'desktopcouch'963 DESKTOPCOUCH_PKG = 'desktopcouch-ubuntuone'
920 BINDWOOD_PKG = 'xul-ext-bindwood'
921 EVOCOUCH_PKG = 'evolution-couchdb'
922 BOOKMARKS = _('Bookmarks (Firefox)')964 BOOKMARKS = _('Bookmarks (Firefox)')
923 CONTACTS = _('Contacts (Evolution)')965 CONTACTS = _('Contacts (Evolution)')
924 NO_PAIRING_RECORD = _('There is no Ubuntu One pairing record.')966 NO_PAIRING_RECORD = _('There is no Ubuntu One pairing record.')
925967
926 def __init__(self, replication_exclusion_class=None):968 def __init__(self):
927 UbuntuOneBin.__init__(self)969 UbuntuOneBin.__init__(self)
928 ControlPanelMixin.__init__(self, filename='services.ui')970 ControlPanelMixin.__init__(self, filename='services.ui')
929 self.add(self.itself)971 self.add(self.itself)
930972
931 self.replication_exclusion_class = replication_exclusion_class
932 self.replication_service = None
933 self.has_desktopcouch = False
934 self.has_bindwood = False
935 self.has_evocouch = False
936 self.package_manager = package_manager.PackageManager()973 self.package_manager = package_manager.PackageManager()
937 self.install_box = None974 self.install_box = None
938 self.bookmarks = None975
939 self.contacts = None976 self.backend.connect_to_signal('ReplicationsInfoReady',
977 self.on_replications_info_ready)
978 self.backend.connect_to_signal('ReplicationsInfoError',
979 self.on_replications_info_error)
940980
941 self.files.pack_start(FilesService(), expand=False)981 self.files.pack_start(FilesService(), expand=False)
942982
943 self.show()983 self.show()
944984
985 @property
986 def has_desktopcouch(self):
987 """Is desktopcouch installed?"""
988 return self.package_manager.is_installed(self.DESKTOPCOUCH_PKG)
989
945 @log_call(logger.debug)990 @log_call(logger.debug)
946 def load(self):991 def load(self):
947 """Load info."""992 """Load info."""
@@ -949,16 +994,7 @@
949 self.itself.remove(self.install_box)994 self.itself.remove(self.install_box)
950 self.install_box = None995 self.install_box = None
951996
952 self.has_desktopcouch = \997 logger.info('load: has_desktopcouch? %r', self.has_desktopcouch)
953 self.package_manager.is_installed(self.DESKTOPCOUCH_PKG)
954 self.has_bindwood = \
955 self.package_manager.is_installed(self.BINDWOOD_PKG)
956 self.has_evocouch = \
957 self.package_manager.is_installed(self.EVOCOUCH_PKG)
958
959 logger.info('load: has_desktopcouch? %r has_bindwood? %s '
960 'has_evocouch? %s', self.has_desktopcouch,
961 self.has_bindwood, self.has_evocouch)
962 if not self.has_desktopcouch:998 if not self.has_desktopcouch:
963 self.message.set_text('')999 self.message.set_text('')
964 self.replications.hide()1000 self.replications.hide()
@@ -975,46 +1011,41 @@
975 @log_call(logger.debug)1011 @log_call(logger.debug)
976 def load_replications(self, *args):1012 def load_replications(self, *args):
977 """Load replications info."""1013 """Load replications info."""
1014 # ask replications to the backend
1015 self.message.start()
1016 self.backend.replications_info(reply_handler=NO_OP,
1017 error_handler=error_handler)
1018
1019 @log_call(logger.debug)
1020 def on_replications_info_ready(self, info):
1021 """The replication info is ready."""
1022 self.on_success(self.CHOOSE_SERVICES)
1023
978 self.replications.show()1024 self.replications.show()
9791025
980 if self.install_box is not None:1026 if self.install_box is not None:
981 self.itself.remove(self.install_box)1027 self.itself.remove(self.install_box)
982 self.install_box = None1028 self.install_box = None
9831029
984 self.message.set_text(self.CHOOSE_SERVICES)
985 for child in self.replications.get_children():1030 for child in self.replications.get_children():
986 self.replications.remove(child)1031 self.replications.remove(child)
9871032
988 # Unable to import 'desktopcouch.application.replication_services'1033 for item in info:
989 # pylint: disable=F04011034 pkg = item['dependency']
990 if self.replication_exclusion_class is None:1035 child = DesktopcouchService(service_id=item['replication_id'],
991 from desktopcouch.application.replication_services import \1036 name=item['name'], # self.BOOKMARKS,
992 ubuntuone as u1rep1037 enabled=bool(item['enabled']),
993 self.replication_exclusion_class = u1rep.ReplicationExclusion1038 dependency=pkg if pkg else None)
9941039 self.replications.pack_start(child, expand=False)
995 if self.replication_service is None:1040
996 try:1041 @log_call(logger.error)
997 self.replication_service = self.replication_exclusion_class()1042 def on_replications_info_error(self, error_dict=None):
998 except ValueError:1043 """The replication info can not be retrieved."""
999 logger.exception('Can not load replications:')1044 if error_dict is not None and \
1000 self._set_warning(self.NO_PAIRING_RECORD, self.message)1045 error_dict.get('error_type', None) == 'NoPairingRecord':
1001 return1046 self.on_error(self.NO_PAIRING_RECORD)
10021047 else:
1003 pkg = None1048 self.on_error()
1004 if not self.has_bindwood:
1005 pkg = self.BINDWOOD_PKG
1006 self.bookmarks = DesktopcouchService('bookmarks', self.BOOKMARKS,
1007 self.replication_service,
1008 dependency=pkg)
1009 self.replications.pack_start(self.bookmarks, expand=False)
1010
1011 pkg = None
1012 if not self.has_evocouch:
1013 pkg = self.EVOCOUCH_PKG
1014 self.contacts = DesktopcouchService('contacts', self.CONTACTS,
1015 self.replication_service,
1016 dependency=pkg)
1017 self.replications.pack_start(self.contacts, expand=False)
10181049
10191050
1020class ManagementPanel(gtk.VBox, ControlPanelMixin):1051class ManagementPanel(gtk.VBox, ControlPanelMixin):
@@ -1107,8 +1138,10 @@
11071138
1108 def load(self):1139 def load(self):
1109 """Load the account info and file sync status list."""1140 """Load the account info and file sync status list."""
1110 self.backend.account_info()1141 self.backend.account_info(reply_handler=NO_OP,
1111 self.backend.file_sync_status()1142 error_handler=error_handler)
1143 self.backend.file_sync_status(reply_handler=NO_OP,
1144 error_handler=error_handler)
1112 self.dashboard_button.clicked()1145 self.dashboard_button.clicked()
11131146
1114 @log_call(logger.debug)1147 @log_call(logger.debug)
11151148
=== modified file 'ubuntuone/controlpanel/gtk/tests/__init__.py'
--- ubuntuone/controlpanel/gtk/tests/__init__.py 2010-12-23 19:17:53 +0000
+++ ubuntuone/controlpanel/gtk/tests/__init__.py 2011-01-06 21:03:06 +0000
@@ -53,6 +53,15 @@
53 'max_upload_speed': '1000', 'max_download_speed': '72548'}, # local53 'max_upload_speed': '1000', 'max_download_speed': '72548'}, # local
54]54]
5555
56FAKE_REPLICATIONS_INFO = [
57 {'replication_id': 'foo', 'name': 'Bar',
58 'enabled': 'True', 'dependency': ''},
59 {'replication_id': 'yadda', 'name': 'Foo',
60 'enabled': '', 'dependency': 'a very weird one'},
61 {'replication_id': 'yoda', 'name': 'Figthers',
62 'enabled': 'True', 'dependency': 'other dep'},
63]
64
5665
57class FakedObject(object):66class FakedObject(object):
58 """Fake an object, record every call."""67 """Fake an object, record every call."""
@@ -117,9 +126,11 @@
117 object_path = gui.DBUS_PREFERENCES_PATH126 object_path = gui.DBUS_PREFERENCES_PATH
118 iface = gui.DBUS_PREFERENCES_IFACE127 iface = gui.DBUS_PREFERENCES_IFACE
119 exposed_methods = [128 exposed_methods = [
120 'account_info', 'devices_info', 'change_device_settings',129 'account_info', # account
121 'volumes_info', 'change_volume_settings', 'file_sync_status',130 'devices_info', 'change_device_settings', 'remove_device', # devices
122 'remove_device', 'enable_files', 'disable_files',131 'volumes_info', 'change_volume_settings', # volumes
132 'replications_info', 'change_replication_settings', # replications
133 'file_sync_status', 'enable_files', 'disable_files', # files
123 ]134 ]
124135
125136
@@ -157,13 +168,3 @@
157 yield168 yield
158 self._installed[package_name] = True169 self._installed[package_name] = True
159 gui.package_manager.return_value(FakedTransaction([package_name]))170 gui.package_manager.return_value(FakedTransaction([package_name]))
160
161
162class FakedReplication(object):
163 """Faked a DC replication exclusion."""
164
165 def __init__(self):
166 self._exclusions = set()
167 self.all_exclusions = lambda: self._exclusions
168 self.replicate = self._exclusions.remove
169 self.exclude = self._exclusions.add
170171
=== modified file 'ubuntuone/controlpanel/gtk/tests/test_gui.py'
--- ubuntuone/controlpanel/gtk/tests/test_gui.py 2010-12-26 15:02:52 +0000
+++ ubuntuone/controlpanel/gtk/tests/test_gui.py 2011-01-06 21:03:06 +0000
@@ -26,9 +26,10 @@
2626
27from ubuntuone.controlpanel.gtk import gui27from ubuntuone.controlpanel.gtk import gui
28from ubuntuone.controlpanel.gtk.tests import (FAKE_ACCOUNT_INFO,28from ubuntuone.controlpanel.gtk.tests import (FAKE_ACCOUNT_INFO,
29 FAKE_VOLUMES_INFO, FAKE_DEVICE_INFO, FAKE_DEVICES_INFO,29 FAKE_DEVICE_INFO, FAKE_DEVICES_INFO,
30 FAKE_VOLUMES_INFO, FAKE_REPLICATIONS_INFO,
30 FakedNMState, FakedSSOBackend, FakedSessionBus, FakedInterface,31 FakedNMState, FakedSSOBackend, FakedSessionBus, FakedInterface,
31 FakedPackageManager, FakedReplication,32 FakedPackageManager,
32)33)
33from ubuntuone.controlpanel.tests import TOKEN, TestCase34from ubuntuone.controlpanel.tests import TOKEN, TestCase
34from ubuntuone.controlpanel.gtk.tests.test_package_manager import (35from ubuntuone.controlpanel.gtk.tests.test_package_manager import (
@@ -37,6 +38,8 @@
3738
38# Attribute 'yyy' defined outside __init__, access to a protected member39# Attribute 'yyy' defined outside __init__, access to a protected member
39# pylint: disable=W0201, W021240# pylint: disable=W0201, W0212
41# Too many lines in module
42# pylint: disable=C0302
4043
4144
42class BaseTestCase(TestCase):45class BaseTestCase(TestCase):
@@ -96,7 +99,9 @@
96 if backend is None:99 if backend is None:
97 backend = self.ui.backend100 backend = self.ui.backend
98 self.assertIn(method_name, backend._called)101 self.assertIn(method_name, backend._called)
99 self.assertEqual(backend._called[method_name], (args, {}))102 kwargs = {'reply_handler': gui.NO_OP,
103 'error_handler': gui.error_handler}
104 self.assertEqual(backend._called[method_name], (args, kwargs))
100105
101 def assert_warning_correct(self, warning, text):106 def assert_warning_correct(self, warning, text):
102 """Check that 'warning' is visible, showing 'text'."""107 """Check that 'warning' is visible, showing 'text'."""
@@ -1361,9 +1366,9 @@
1361 """The test suite for a service."""1366 """The test suite for a service."""
13621367
1363 klass = gui.Service1368 klass = gui.Service
1364 name = 'dc_test'1369 service_id = 'dc_test'
1365 localized_name = u'Qué lindo test!'1370 name = u'Qué lindo test!'
1366 kwargs = {'name': 'dc_test', 'localized_name': u'Qué lindo test!'}1371 kwargs = {'service_id': service_id, 'name': name}
13671372
1368 def test_is_an_box(self):1373 def test_is_an_box(self):
1369 """Inherits from gtk.VBox."""1374 """Inherits from gtk.VBox."""
@@ -1373,30 +1378,35 @@
1373 """Is visible."""1378 """Is visible."""
1374 self.assertTrue(self.ui.get_visible())1379 self.assertTrue(self.ui.get_visible())
13751380
1381 def test_warning_label_is_cleared(self):
1382 """The warning label is cleared."""
1383 self.assertEqual(self.ui.warning_label.get_text(), '')
1384
1385 def test_warning_label_packed(self):
1386 """The warning label is packed as child."""
1387 self.assertIn(self.ui.warning_label, self.ui.get_children())
1388
1376 def test_check_button_packed(self):1389 def test_check_button_packed(self):
1377 """A check button is packed as only child."""1390 """A check button is packed as child."""
1378 self.assertIn(self.ui.button, self.ui.get_children())1391 self.assertIn(self.ui.button, self.ui.get_children())
13791392
1380 def test_label(self):1393 def test_label(self):
1381 """The label is set."""1394 """The label is set."""
1382 self.assertEqual(self.localized_name, self.ui.button.get_label())1395 self.assertEqual(self.name, self.ui.button.get_label())
13831396
1384 def test_service_name(self):1397 def test_service_id(self):
1385 """The service_name is set."""1398 """The service id is set."""
1386 self.assertEqual(self.name, self.ui.service_name)1399 self.assertEqual(self.service_id, self.ui.id)
13871400
13881401
1389class FilesServiceTestCase(ServiceTestCase):1402class FilesServiceTestCase(ServiceTestCase):
1390 """The test suite for the file sync service."""1403 """The test suite for the file sync service."""
13911404
1392 klass = gui.FilesService1405 klass = gui.FilesService
1406 service_id = 'files'
1407 name = gui.FilesService.FILES_SERVICE_NAME
1393 kwargs = {}1408 kwargs = {}
13941409
1395 def setUp(self):
1396 self.name = 'files'
1397 self.localized_name = gui.FilesService.FILES_SERVICE_NAME
1398 super(FilesServiceTestCase, self).setUp()
1399
1400 def test_backend_account_signals(self):1410 def test_backend_account_signals(self):
1401 """The proper signals are connected to the backend."""1411 """The proper signals are connected to the backend."""
1402 self.assertEqual(self.ui.backend._signals['FileSyncStatusChanged'],1412 self.assertEqual(self.ui.backend._signals['FileSyncStatusChanged'],
@@ -1461,35 +1471,36 @@
1461 """The test suite for a desktopcouch service."""1471 """The test suite for a desktopcouch service."""
14621472
1463 klass = gui.DesktopcouchService1473 klass = gui.DesktopcouchService
1474 enabled = True
14641475
1465 def setUp(self):1476 def setUp(self):
1466 self.replication = FakedReplication()1477 self.kwargs['enabled'] = self.enabled
1467 self.name = self.kwargs['name']
1468 self.kwargs['replication_service'] = self.replication
1469 super(DesktopcouchServiceTestCase, self).setUp()1478 super(DesktopcouchServiceTestCase, self).setUp()
14701479
1480 def modify_settings(self):
1481 """Modify settings so values actually change."""
1482 self.ui.button.set_active(not self.ui.button.get_active())
1483
1484 def test_backend_account_signals(self):
1485 """The proper signals are connected to the backend."""
1486 self.assertEqual(
1487 self.ui.backend._signals['ReplicationSettingsChanged'],
1488 [self.ui.on_replication_settings_changed])
1489 self.assertEqual(
1490 self.ui.backend._signals['ReplicationSettingsChangeError'],
1491 [self.ui.on_replication_settings_change_error])
1492
1471 def test_active(self):1493 def test_active(self):
1472 """Is active since replication has an empty database."""1494 """Is active if enabled."""
1473 self.assertTrue(self.ui.button.get_active())1495 self.assertEqual(self.enabled, self.ui.button.get_active())
1474
1475 def test_not_active(self):
1476 """Is not active since 'name' is excluded on replication database."""
1477 self.replication.exclude(self.name)
1478 self.ui = self.klass(**self.kwargs)
1479 self.assertFalse(self.ui.button.get_active())
14801496
1481 def test_on_button_toggled(self):1497 def test_on_button_toggled(self):
1482 """When toggling the button, the DC exclude list is updated."""1498 """When toggling the button, the DC exclude list is updated."""
1483 assert self.ui.button.get_active()
1484 self.ui.button.set_active(not self.ui.button.get_active())1499 self.ui.button.set_active(not self.ui.button.get_active())
1485 self.assertEqual(set([self.name]), self.replication.all_exclusions())
14861500
1487 def test_on_button_toggled_twice(self):1501 args = (self.service_id,
1488 """When toggling the button twice, the DC exclude list is updated."""1502 {'enabled': gui.bool_str(self.ui.button.get_active())})
1489 assert self.ui.button.get_active()1503 self.assert_backend_called('change_replication_settings', args)
1490 self.ui.button.set_active(not self.ui.button.get_active())
1491 self.ui.button.set_active(not self.ui.button.get_active())
1492 self.assertEqual(set(), self.replication.all_exclusions())
14931504
1494 def test_dependency(self):1505 def test_dependency(self):
1495 """The dependency box is None."""1506 """The dependency box is None."""
@@ -1499,6 +1510,72 @@
1499 """The check button is sensitive."""1510 """The check button is sensitive."""
1500 self.assertTrue(self.ui.button.get_sensitive())1511 self.assertTrue(self.ui.button.get_sensitive())
15011512
1513 def test_on_replication_settings_changed(self):
1514 """When settings were changed for this replication, enable it."""
1515 new_val = not self.ui.button.get_active()
1516 self.ui.button.set_active(new_val)
1517
1518 self.ui.on_replication_settings_changed(replication_id=self.ui.id)
1519
1520 self.assertEqual(self.ui.warning_label.get_text(), '')
1521 self.assertEqual(new_val, self.ui.button.get_active())
1522
1523 def test_on_replication_settings_changed_after_error(self):
1524 """Change success after error."""
1525 self.ui.button.set_active(not self.ui.button.get_active())
1526 self.ui.on_replication_settings_change_error(replication_id=self.ui.id)
1527
1528 self.test_on_replication_settings_changed()
1529
1530 def test_on_replication_settings_changed_different_id(self):
1531 """When settings were changed for other rep, nothing changes."""
1532 self.ui.button.set_active(not self.ui.button.get_active())
1533 self.ui.on_replication_settings_changed(replication_id='yadda')
1534
1535 self.assertEqual(self.ui.warning_label.get_text(), '')
1536
1537 def test_on_replication_settings_changed_different_id_after_error(self):
1538 """When settings were changed for other + error, nothing changes."""
1539 self.ui.on_replication_settings_change_error(replication_id=self.ui.id)
1540 self.ui.on_replication_settings_changed(replication_id='yadda')
1541
1542 self.assert_warning_correct(self.ui.warning_label,
1543 self.ui.CHANGE_ERROR)
1544
1545 def test_on_replication_settings_change_error(self):
1546 """When settings were not changed, notify the user.
1547
1548 Also, confirm that old value was restored.
1549
1550 """
1551 old_val = self.ui.button.get_active()
1552 self.ui.button.set_active(not old_val)
1553 self.ui.on_replication_settings_change_error(replication_id=self.ui.id)
1554
1555 self.assert_warning_correct(self.ui.warning_label,
1556 self.ui.CHANGE_ERROR)
1557 self.assertEqual(old_val, self.ui.button.get_active())
1558
1559 def test_on_replication_settings_change_error_after_success(self):
1560 """Change error after success."""
1561 self.ui.button.set_active(not self.ui.button.get_active())
1562 self.ui.on_replication_settings_changed(replication_id=self.ui.id)
1563
1564 self.test_on_replication_settings_change_error()
1565
1566 def test_on_replication_settings_change_error_different_id(self):
1567 """When settings were not changed for other replication, do nothing."""
1568 self.ui.button.set_active(not self.ui.button.get_active())
1569 self.ui.on_replication_settings_change_error(replication_id='yudo')
1570
1571 self.assertEqual(self.ui.warning_label.get_text(), '')
1572
1573
1574class DesktopcouchServiceDisabledAtStartupTestCase(ServiceTestCase):
1575 """The test suite for a desktopcouch service when enabled=False."""
1576
1577 enabled = False
1578
15021579
1503class DesktopcouchServiceWithDependencyTestCase(DesktopcouchServiceTestCase):1580class DesktopcouchServiceWithDependencyTestCase(DesktopcouchServiceTestCase):
1504 """The test suite for a desktopcouch service when it needs a dependency."""1581 """The test suite for a desktopcouch service when it needs a dependency."""
@@ -1532,7 +1609,8 @@
1532 self.ui.dependency.emit('finished')1609 self.ui.dependency.emit('finished')
15331610
1534 self.assertTrue(self.ui.dependency is None)1611 self.assertTrue(self.ui.dependency is None)
1535 self.assertEqual(self.ui.get_children(), [self.ui.button])1612 self.assertEqual(sorted(self.ui.get_children()),
1613 sorted([self.ui.button, self.ui.warning_label]))
15361614
15371615
1538class ServicesTestCase(ControlPanelMixinTestCase):1616class ServicesTestCase(ControlPanelMixinTestCase):
@@ -1562,6 +1640,13 @@
1562 """The install box is None."""1640 """The install box is None."""
1563 self.assertTrue(self.ui.install_box is None)1641 self.assertTrue(self.ui.install_box is None)
15641642
1643 def test_backend_signals(self):
1644 """The proper signals are connected to the backend."""
1645 self.assertEqual(self.ui.backend._signals['ReplicationsInfoReady'],
1646 [self.ui.on_replications_info_ready])
1647 self.assertEqual(self.ui.backend._signals['ReplicationsInfoError'],
1648 [self.ui.on_replications_info_error])
1649
15651650
1566class ServicesFilesTestCase(ServicesTestCase):1651class ServicesFilesTestCase(ServicesTestCase):
1567 """The test suite for the services panel (files section)."""1652 """The test suite for the services panel (files section)."""
@@ -1589,22 +1674,10 @@
1589 self.assertFalse(self.ui.message.active)1674 self.assertFalse(self.ui.message.active)
1590 self.assertEqual(self.ui.message.get_text(), '')1675 self.assertEqual(self.ui.message.get_text(), '')
15911676
1592 def test_replication_service(self):
1593 """Has a replication service."""
1594 self.assertEqual(self.ui.replication_service, None)
1595
1596 def test_has_desktopcouch(self):1677 def test_has_desktopcouch(self):
1597 """Has desktopcouch installed?"""1678 """Has desktopcouch installed?"""
1598 self.assertFalse(self.ui.has_desktopcouch)1679 self.assertFalse(self.ui.has_desktopcouch)
15991680
1600 def test_has_bindwood(self):
1601 """Has bindwood installed?"""
1602 self.assertFalse(self.ui.has_bindwood)
1603
1604 def test_has_evocouch(self):
1605 """Has evocouch installed?"""
1606 self.assertFalse(self.ui.has_evocouch)
1607
1608 def test_install_box_is_hidden(self):1681 def test_install_box_is_hidden(self):
1609 """The install box is not hidden."""1682 """The install box is not hidden."""
1610 self.assertTrue(self.ui.install_box.get_visible())1683 self.assertTrue(self.ui.install_box.get_visible())
@@ -1629,41 +1702,27 @@
16291702
1630 self.assertEqual(self._called, ((self.ui.install_box,), {}))1703 self.assertEqual(self._called, ((self.ui.install_box,), {}))
16311704
1705 def test_load_replications(self):
1706 """The load_replications starts the spinner and calls the backend."""
1707 self.ui.load_replications()
1708
1709 self.assertTrue(self.ui.message.active)
1710 self.assert_backend_called('replications_info', ())
1711
16321712
1633class ServicesWithDesktopcouchTestCase(ServicesTestCase):1713class ServicesWithDesktopcouchTestCase(ServicesTestCase):
1634 """The test suite for the services panel."""1714 """The test suite for the services panel."""
16351715
1636 kwargs = {'replication_exclusion_class': FakedReplication}
1637
1638 def setUp(self):1716 def setUp(self):
1639 super(ServicesWithDesktopcouchTestCase, self).setUp()1717 super(ServicesWithDesktopcouchTestCase, self).setUp()
1640 self.ui.package_manager._installed[self.ui.DESKTOPCOUCH_PKG] = True1718 self.ui.package_manager._installed[self.ui.DESKTOPCOUCH_PKG] = True
1641 self.ui.load()1719 self.ui.on_replications_info_ready(info=FAKE_REPLICATIONS_INFO)
16421720
1643 def test_message(self):1721 def test_message(self):
1644 """Global load message is stopped and proper test is shown."""1722 """Global load message is stopped and proper test is shown."""
1645 self.assertFalse(self.ui.message.active)1723 self.assertFalse(self.ui.message.active)
1646 self.assertEqual(self.ui.message.get_text(), self.ui.CHOOSE_SERVICES)1724 self.assertEqual(self.ui.message.get_text(), self.ui.CHOOSE_SERVICES)
16471725
1648 def test_replication_service(self):
1649 """Has a replication service."""
1650 self.assertIsInstance(self.ui.replication_service, FakedReplication)
1651
1652 def test_no_pairing_record(self):
1653 """The pairing record is not in place."""
1654
1655 def no_pairing_record(*a):
1656 """Fake a ReplicationExclusion with no pairing record."""
1657 raise ValueError("No pairing record for ubuntuone.")
1658
1659 self.ui.replication_exclusion_class = no_pairing_record
1660 self.ui.replication_service = None
1661 self.ui.load()
1662
1663 self.assertEqual(self.ui.replications.get_children(), [])
1664 self.assertFalse(self.ui.message.active)
1665 self.assert_warning_correct(self.ui.message, self.ui.NO_PAIRING_RECORD)
1666
1667 def test_has_desktopcouch(self):1726 def test_has_desktopcouch(self):
1668 """Has desktopcouch installed?"""1727 """Has desktopcouch installed?"""
1669 self.assertTrue(self.ui.has_desktopcouch)1728 self.assertTrue(self.ui.has_desktopcouch)
@@ -1673,79 +1732,67 @@
1673 self.assertTrue(self.ui.replications.get_visible())1732 self.assertTrue(self.ui.replications.get_visible())
16741733
1675 children = self.ui.replications.get_children()1734 children = self.ui.replications.get_children()
1676 self.assertEqual(len(children), 2)1735 self.assertEqual(len(children), len(FAKE_REPLICATIONS_INFO))
1677 for child in children:1736 for expected, child in zip(FAKE_REPLICATIONS_INFO, children):
1678 self.assertIsInstance(child, gui.DesktopcouchService)1737 self.assertIsInstance(child, gui.DesktopcouchService)
16791738 self.assertEqual(expected['replication_id'], child.id)
1680 self.assertTrue(self.ui.bookmarks is children[0])1739 self.assertEqual(expected['name'], child.button.get_label())
1681 self.assertTrue(self.ui.contacts is children[1])1740 self.assertEqual(bool(expected['enabled']),
16821741 child.button.get_active())
1683 def test_replications_after_loading_twice(self):1742
1684 """Has proper child after loading twice."""1743 if expected['dependency']:
1685 self.ui.load()1744 self.assertTrue(child.dependency is not None)
1745 self.assertEqual(expected['dependency'],
1746 child.dependency.package_name)
1747 else:
1748 self.assertTrue(child.dependency is None)
1749
1750 def test_replications_after_getting_info_twice(self):
1751 """Has proper child after getting backend info twice."""
1752 self.ui.on_replications_info_ready(info=FAKE_REPLICATIONS_INFO)
1686 self.test_replications()1753 self.test_replications()
16871754
1688 def test_bookmarks(self):1755
1689 """The bookmarks is correct."""1756class ServicesWithDesktopcouchErrorTestCase(ServicesTestCase):
1690 self.assertEqual(self.ui.bookmarks.service_name, 'bookmarks')1757 """The test suite for the services panel."""
1691 self.assertEqual(self.ui.bookmarks.button.get_label(),1758
1692 self.ui.BOOKMARKS)1759 def setUp(self):
1693 self.assertTrue(self.ui.bookmarks.replication_service is1760 super(ServicesWithDesktopcouchErrorTestCase, self).setUp()
1694 self.ui.replication_service)1761 self.ui.package_manager._installed[self.ui.DESKTOPCOUCH_PKG] = True
16951762
1696 def test_bookmarks_dependency(self):1763 def test_no_pairing_record(self):
1697 """The bookmarks dependency is correct."""1764 """The pairing record is not in place."""
1698 self.assertTrue(self.ui.bookmarks.dependency is not None)1765 error_dict = {'error_type': 'NoPairingRecord'}
1699 self.assertEqual(self.ui.bookmarks.dependency.package_name,1766 self.ui.on_replications_info_error(error_dict)
1700 self.ui.BINDWOOD_PKG)1767
17011768 self.assertEqual(self.ui.replications.get_children(), [])
1702 def test_contacts(self):1769 self.assertFalse(self.ui.message.active)
1703 """The contacts is correct."""1770 self.assert_warning_correct(self.ui.message, self.ui.NO_PAIRING_RECORD)
1704 self.assertEqual(self.ui.contacts.service_name, 'contacts')1771
1705 self.assertEqual(self.ui.contacts.button.get_label(),1772 def test_other_error(self):
1706 self.ui.CONTACTS)1773 """There was an error other than no pairing record."""
1707 self.assertTrue(self.ui.contacts.replication_service is1774 error_dict = {'error_type': 'OtherError'}
1708 self.ui.replication_service)1775 self.ui.on_replications_info_error(error_dict)
17091776
1710 def test_contacts_dependency(self):1777 self.assertEqual(self.ui.replications.get_children(), [])
1711 """The contacts dependency is correct."""1778 self.assertFalse(self.ui.message.active)
1712 self.assertTrue(self.ui.contacts.dependency is not None)1779 self.assert_warning_correct(self.ui.message, gui.VALUE_ERROR)
1713 self.assertEqual(self.ui.contacts.dependency.package_name,1780
1714 self.ui.EVOCOUCH_PKG)1781 def test_empty_dict(self):
17151782 """Handle empty dicts errors."""
17161783 self.ui.on_replications_info_error(error_dict={})
1717class ServicesWithDCAndBindwoodTestCase(ServicesWithDesktopcouchTestCase):1784
1718 """The test suite for the services panel."""1785 self.assertEqual(self.ui.replications.get_children(), [])
17191786 self.assertFalse(self.ui.message.active)
1720 def setUp(self):1787 self.assert_warning_correct(self.ui.message, gui.VALUE_ERROR)
1721 super(ServicesWithDCAndBindwoodTestCase, self).setUp()1788
1722 self.ui.package_manager._installed[self.ui.BINDWOOD_PKG] = True1789 def test_error_dict_none(self):
1723 self.ui.load()1790 """HGandle empty dicts errors."""
17241791 self.ui.on_replications_info_error(error_dict=None)
1725 def test_has_bindwood(self):1792
1726 """Has bindwood installed?"""1793 self.assertEqual(self.ui.replications.get_children(), [])
1727 self.assertTrue(self.ui.has_bindwood)1794 self.assertFalse(self.ui.message.active)
17281795 self.assert_warning_correct(self.ui.message, gui.VALUE_ERROR)
1729 def test_bookmarks_dependency(self):
1730 """The bookmarks dependency is correct."""
1731 self.assertTrue(self.ui.bookmarks.dependency is None)
1732
1733
1734class ServicesWithDCAndEvocouchTestCase(ServicesWithDesktopcouchTestCase):
1735 """The test suite for the services panel."""
1736
1737 def setUp(self):
1738 super(ServicesWithDCAndEvocouchTestCase, self).setUp()
1739 self.ui.package_manager._installed[self.ui.EVOCOUCH_PKG] = True
1740 self.ui.load()
1741
1742 def test_has_evocouch(self):
1743 """Has evocoucg installed?"""
1744 self.assertTrue(self.ui.has_evocouch)
1745
1746 def test_contacts_dependency(self):
1747 """The bookmarks dependency is correct."""
1748 self.assertTrue(self.ui.contacts.dependency is None)
17491796
17501797
1751class ManagementPanelTestCase(ControlPanelMixinTestCase):1798class ManagementPanelTestCase(ControlPanelMixinTestCase):
17521799
=== modified file 'ubuntuone/controlpanel/logger.py'
--- ubuntuone/controlpanel/logger.py 2010-12-23 18:20:56 +0000
+++ ubuntuone/controlpanel/logger.py 2011-01-06 21:03:06 +0000
@@ -53,10 +53,7 @@
53 logger.addHandler(MAIN_HANDLER)53 logger.addHandler(MAIN_HANDLER)
54 if os.environ.get('DEBUG'):54 if os.environ.get('DEBUG'):
55 debug_handler = logging.StreamHandler(sys.stderr)55 debug_handler = logging.StreamHandler(sys.stderr)
56 if prefix is not None:56 debug_handler.setFormatter(basic_formatter)
57 fmt = prefix + "%(name)s - %(levelname)s\n%(message)s\n"
58 formatter = logging.Formatter(fmt)
59 debug_handler.setFormatter(formatter)
60 logger.addHandler(debug_handler)57 logger.addHandler(debug_handler)
6158
62 return logger59 return logger

Subscribers

People subscribed via source and target branches