Merge lp:~dobey/ubuntuone-control-panel/update-4-0 into lp:ubuntuone-control-panel/stable-4-0
- update-4-0
- Merge into stable-4-0
Proposed by
dobey
on 2012-10-02
| Status: | Merged |
|---|---|
| Approved by: | dobey on 2012-10-03 |
| Approved revision: | 343 |
| Merged at revision: | 343 |
| Proposed branch: | lp:~dobey/ubuntuone-control-panel/update-4-0 |
| Merge into: | lp:ubuntuone-control-panel/stable-4-0 |
| Diff against target: |
1026 lines (+715/-31) 8 files modified
ubuntuone/controlpanel/gui/qt/share_file.py (+7/-5) ubuntuone/controlpanel/gui/qt/share_links.py (+23/-9) ubuntuone/controlpanel/gui/qt/share_links_search.py (+14/-3) ubuntuone/controlpanel/gui/qt/tests/test_share_file.py (+5/-2) ubuntuone/controlpanel/gui/qt/tests/test_share_links.py (+18/-0) ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py (+47/-12) ubuntuone/controlpanel/utils/darwin.py (+375/-0) ubuntuone/controlpanel/utils/tests/test_darwin.py (+226/-0) |
| To merge this branch: | bzr merge lp:~dobey/ubuntuone-control-panel/update-4-0 |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| Mike McCracken (community) | 2012-10-02 | Approve on 2012-10-02 | |
|
Review via email:
|
|||
Commit Message
[Diego Sarmentero]
- Accept mouse events in shares search pop-up list. (LP: #1056192)
[Mike McCracken]
- Use darwin ServiceManagement API to install/
[Brian Curtin]
- Use os.path.join to construct platform-safe paths in share links tests.
Description of the Change
To post a comment you must log in.
review:
Approve
| Ubuntu One Auto Pilot (otto-pilot) wrote : | # |
Download full text (192.8 KiB)
Preview Diff
[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
| 1 | === modified file 'ubuntuone/controlpanel/gui/qt/share_file.py' |
| 2 | --- ubuntuone/controlpanel/gui/qt/share_file.py 2012-08-27 15:00:01 +0000 |
| 3 | +++ ubuntuone/controlpanel/gui/qt/share_file.py 2012-10-02 18:05:36 +0000 |
| 4 | @@ -20,6 +20,8 @@ |
| 5 | |
| 6 | from PyQt4 import QtGui, QtCore |
| 7 | |
| 8 | +from ubuntuone.platform import expand_user |
| 9 | + |
| 10 | from ubuntuone.controlpanel import cache |
| 11 | from ubuntuone.controlpanel.gui.qt.share_links_search import ( |
| 12 | get_system_icon_for_filename, |
| 13 | @@ -40,11 +42,12 @@ |
| 14 | super(ShareFileWidget, self).__init__(*args, **kwargs) |
| 15 | self.ui = share_file_ui.Ui_Form() |
| 16 | self.ui.setupUi(self) |
| 17 | - self.file_path = file_path |
| 18 | + full_path = expand_user(file_path.encode('utf-8')) |
| 19 | + self.file_path = full_path.decode('utf-8') |
| 20 | |
| 21 | self.ui.lbl_filename.setText(os.path.basename(file_path)) |
| 22 | self.ui.lbl_path.setText(file_path) |
| 23 | - icon = get_system_icon_for_filename(os.path.expanduser(file_path)) |
| 24 | + icon = get_system_icon_for_filename(full_path) |
| 25 | pixmap = icon.pixmap(24) |
| 26 | self.ui.lbl_icon.setPixmap(pixmap) |
| 27 | |
| 28 | @@ -53,11 +56,10 @@ |
| 29 | |
| 30 | def open_file(self): |
| 31 | """Open the specified file.""" |
| 32 | - path = u'file://%s' % self.file_path |
| 33 | + path = 'file://%s' % self.file_path |
| 34 | QtGui.QDesktopServices.openUrl(QtCore.QUrl(path)) |
| 35 | |
| 36 | def disable_link(self, val): |
| 37 | """Change the access of the file to Not Public.""" |
| 38 | - self.backend.change_public_access( |
| 39 | - os.path.expanduser(self.file_path), False) |
| 40 | + self.backend.change_public_access(self.file_path, False) |
| 41 | self.linkDisabled.emit() |
| 42 | |
| 43 | === modified file 'ubuntuone/controlpanel/gui/qt/share_links.py' |
| 44 | --- ubuntuone/controlpanel/gui/qt/share_links.py 2012-08-27 15:00:01 +0000 |
| 45 | +++ ubuntuone/controlpanel/gui/qt/share_links.py 2012-10-02 18:05:36 +0000 |
| 46 | @@ -21,6 +21,8 @@ |
| 47 | from PyQt4 import QtGui, QtCore |
| 48 | from twisted.internet.defer import inlineCallbacks |
| 49 | |
| 50 | +from ubuntuone.platform import expand_user |
| 51 | + |
| 52 | from ubuntuone.controlpanel.logger import setup_logging |
| 53 | from ubuntuone.controlpanel.gui import ( |
| 54 | COPY_LINK, |
| 55 | @@ -57,6 +59,7 @@ |
| 56 | logger = logger |
| 57 | _enhanced_line = None |
| 58 | home_dir = '' |
| 59 | + _shared_files = {} |
| 60 | |
| 61 | def _setup(self): |
| 62 | """Do some extra setupping for the UI.""" |
| 63 | @@ -91,17 +94,26 @@ |
| 64 | @inlineCallbacks |
| 65 | def share_file(self, file_path): |
| 66 | """Clean the previous file share details and publish file_path.""" |
| 67 | + file_path = unicode(file_path) |
| 68 | if self.ui.hbox_share_file.count() > 0: |
| 69 | widget = self.ui.hbox_share_file.takeAt(0).widget() |
| 70 | widget.close() |
| 71 | - self.is_processing = True |
| 72 | - file_path = unicode(file_path) |
| 73 | - share_file_widget = ShareFileWidget(file_path) |
| 74 | - self.ui.hbox_share_file.addWidget(share_file_widget) |
| 75 | - share_file_widget.linkDisabled.connect( |
| 76 | - lambda: self.ui.line_copy_link.setText('')) |
| 77 | - yield self.backend.change_public_access( |
| 78 | - os.path.expanduser(file_path), True) |
| 79 | + full_path = expand_user(file_path.encode('utf-8')).decode('utf-8') |
| 80 | + if full_path not in self._shared_files: |
| 81 | + self.is_processing = True |
| 82 | + share_file_widget = ShareFileWidget(file_path) |
| 83 | + self.ui.hbox_share_file.addWidget(share_file_widget) |
| 84 | + share_file_widget.linkDisabled.connect( |
| 85 | + lambda: self.ui.line_copy_link.setText('')) |
| 86 | + yield self.backend.change_public_access( |
| 87 | + full_path, True) |
| 88 | + else: |
| 89 | + share_file_widget = ShareFileWidget(file_path) |
| 90 | + self.ui.hbox_share_file.addWidget(share_file_widget) |
| 91 | + share_file_widget.linkDisabled.connect( |
| 92 | + lambda: self.ui.line_copy_link.setText('')) |
| 93 | + self.ui.line_copy_link.setText(self._shared_files[full_path]) |
| 94 | + self.ui.stacked_widget.setCurrentIndex(1) |
| 95 | |
| 96 | def _file_shared(self, info): |
| 97 | """Receive the notification that the file has been published.""" |
| 98 | @@ -135,17 +147,19 @@ |
| 99 | def _load_public_files(self, publicfiles): |
| 100 | """Load the list of public files.""" |
| 101 | self.ui.tree_shared_files.clear() |
| 102 | + self._shared_files = {} |
| 103 | for pfile in publicfiles: |
| 104 | item = QtGui.QTreeWidgetItem() |
| 105 | path = pfile['path'] |
| 106 | public_url = pfile['public_url'] |
| 107 | + self._shared_files[path] = public_url |
| 108 | name = os.path.basename(path) |
| 109 | item.setText(FILE_NAME_COL, name) |
| 110 | tooltip = path |
| 111 | if tooltip.startswith(self.home_dir): |
| 112 | tooltip = tooltip.replace(self.home_dir, '~', 1) |
| 113 | item.setToolTip(FILE_NAME_COL, tooltip) |
| 114 | - icon = get_system_icon_for_filename(path) |
| 115 | + icon = get_system_icon_for_filename(path.encode('utf-8')) |
| 116 | item.setIcon(FILE_NAME_COL, icon) |
| 117 | |
| 118 | self.ui.tree_shared_files.setColumnWidth(PUBLIC_LINK_COL, 300) |
| 119 | |
| 120 | === modified file 'ubuntuone/controlpanel/gui/qt/share_links_search.py' |
| 121 | --- ubuntuone/controlpanel/gui/qt/share_links_search.py 2012-09-14 20:23:13 +0000 |
| 122 | +++ ubuntuone/controlpanel/gui/qt/share_links_search.py 2012-10-02 18:05:36 +0000 |
| 123 | @@ -21,6 +21,8 @@ |
| 124 | from PyQt4 import QtGui, QtCore |
| 125 | from twisted.internet.defer import inlineCallbacks |
| 126 | |
| 127 | +from ubuntuone.platform import expand_user |
| 128 | + |
| 129 | from ubuntuone.controlpanel import cache |
| 130 | from ubuntuone.controlpanel.logger import setup_logging |
| 131 | |
| 132 | @@ -29,7 +31,7 @@ |
| 133 | |
| 134 | def get_system_icon_for_filename(file_path): |
| 135 | """Return the icon used for the system to represent this file.""" |
| 136 | - fileinfo = QtCore.QFileInfo(os.path.expanduser(file_path)) |
| 137 | + fileinfo = QtCore.QFileInfo(expand_user(file_path)) |
| 138 | icon_provider = QtGui.QFileIconProvider() |
| 139 | icon = icon_provider.icon(fileinfo) |
| 140 | return icon |
| 141 | @@ -65,10 +67,19 @@ |
| 142 | QtCore.Qt.Key_Enter: self._key_return_pressed, |
| 143 | } |
| 144 | |
| 145 | + self.popup.list_widget.itemPressed.connect(self._set_selected_item) |
| 146 | + self.popup.list_widget.verticalScrollBar().valueChanged.connect( |
| 147 | + self._scroll_fetch_more) |
| 148 | self.textChanged.connect(self.filter) |
| 149 | |
| 150 | self._get_volumes_info() |
| 151 | |
| 152 | + def _scroll_fetch_more(self, value): |
| 153 | + """Fetch more items into the list on scroll.""" |
| 154 | + if self.popup.list_widget.verticalScrollBar().maximum() == value: |
| 155 | + filenames = self._get_filtered_list(self.temp_u1_files) |
| 156 | + self.popup.fetch_more(filenames) |
| 157 | + |
| 158 | @inlineCallbacks |
| 159 | def _get_volumes_info(self): |
| 160 | """Get the volumes info.""" |
| 161 | @@ -260,7 +271,7 @@ |
| 162 | file_widget = FileItem(file_) |
| 163 | self.list_widget.addItem(item) |
| 164 | self.list_widget.setItemWidget(item, file_widget) |
| 165 | - icon = get_system_icon_for_filename(file_) |
| 166 | + icon = get_system_icon_for_filename(file_.encode('utf-8')) |
| 167 | item.setIcon(icon) |
| 168 | if file_items: |
| 169 | self.list_widget.setCurrentRow(0) |
| 170 | @@ -272,7 +283,7 @@ |
| 171 | file_widget = FileItem(file_) |
| 172 | self.list_widget.addItem(item) |
| 173 | self.list_widget.setItemWidget(item, file_widget) |
| 174 | - icon = get_system_icon_for_filename(file_) |
| 175 | + icon = get_system_icon_for_filename(file_.encode('utf-8')) |
| 176 | item.setIcon(icon) |
| 177 | |
| 178 | def showEvent(self, event): |
| 179 | |
| 180 | === modified file 'ubuntuone/controlpanel/gui/qt/tests/test_share_file.py' |
| 181 | --- ubuntuone/controlpanel/gui/qt/tests/test_share_file.py 2012-08-28 00:32:41 +0000 |
| 182 | +++ ubuntuone/controlpanel/gui/qt/tests/test_share_file.py 2012-10-02 18:05:36 +0000 |
| 183 | @@ -20,6 +20,8 @@ |
| 184 | |
| 185 | from PyQt4 import QtGui, QtCore |
| 186 | |
| 187 | +from ubuntuone.platform import expand_user |
| 188 | + |
| 189 | from ubuntuone.controlpanel.gui.qt import share_file as gui |
| 190 | from ubuntuone.controlpanel.gui.qt.tests import ( |
| 191 | BaseTestCase, |
| 192 | @@ -54,7 +56,7 @@ |
| 193 | self.patch(QtGui, "QDesktopServices", fake_desktop_service) |
| 194 | self.ui.ui.btn_open.click() |
| 195 | |
| 196 | - expected = QtCore.QUrl(u'file://%s' % self.file_path) |
| 197 | + expected = QtCore.QUrl('file://%s' % self.file_path) |
| 198 | self.assertEqual(expected, fake_desktop_service.opened_url) |
| 199 | |
| 200 | def test_disable_link(self): |
| 201 | @@ -71,6 +73,7 @@ |
| 202 | fake_change_access) |
| 203 | self.ui.ui.btn_disable.click() |
| 204 | |
| 205 | - expected = [os.path.expanduser(self.file_path), False] |
| 206 | + expected = [expand_user( |
| 207 | + self.file_path.encode('utf-8')).decode('utf-8'), False] |
| 208 | self.assertEqual(data, expected) |
| 209 | self.assertEqual(self._called, ((), {})) |
| 210 | |
| 211 | === modified file 'ubuntuone/controlpanel/gui/qt/tests/test_share_links.py' |
| 212 | --- ubuntuone/controlpanel/gui/qt/tests/test_share_links.py 2012-08-28 00:32:41 +0000 |
| 213 | +++ ubuntuone/controlpanel/gui/qt/tests/test_share_links.py 2012-10-02 18:05:36 +0000 |
| 214 | @@ -78,6 +78,24 @@ |
| 215 | self.assertEqual(self.ui.ui.stacked_widget.currentIndex(), 1) |
| 216 | self.assertFalse(self.ui.is_processing) |
| 217 | |
| 218 | + def test_file_already_shared(self): |
| 219 | + """Check the behavior of the widgets when there is a shared file.""" |
| 220 | + data = [] |
| 221 | + |
| 222 | + def fake_method(self, *args): |
| 223 | + """Fake callback.""" |
| 224 | + data.append((args)) |
| 225 | + |
| 226 | + self.patch(self.ui.backend, "change_public_access", fake_method) |
| 227 | + path = '/home/user/Ubuntu One/file1.txt' |
| 228 | + shared = { |
| 229 | + '/home/user/Ubuntu One/file1.txt': 'http://ubuntuone.com/asd123'} |
| 230 | + self.ui._shared_files = shared |
| 231 | + self.ui.share_file(path) |
| 232 | + self.assertEqual(self.ui.ui.line_copy_link.text(), shared[path]) |
| 233 | + self.assertEqual(self.ui.ui.stacked_widget.currentIndex(), 1) |
| 234 | + self.assertEqual(data, []) |
| 235 | + |
| 236 | def test_open_in_browser(self): |
| 237 | """Test the execution of open_in_browser.""" |
| 238 | fake_desktop_service = FakeDesktopService() |
| 239 | |
| 240 | === modified file 'ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py' |
| 241 | --- ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py 2012-09-14 20:23:13 +0000 |
| 242 | +++ ubuntuone/controlpanel/gui/qt/tests/test_share_links_search.py 2012-10-02 18:05:36 +0000 |
| 243 | @@ -18,6 +18,7 @@ |
| 244 | |
| 245 | import os |
| 246 | |
| 247 | +from dirspec import utils |
| 248 | from twisted.internet import defer |
| 249 | |
| 250 | from ubuntuone.controlpanel.gui.tests import USER_HOME |
| 251 | @@ -27,6 +28,7 @@ |
| 252 | |
| 253 | # pylint: disable=W0212 |
| 254 | |
| 255 | + |
| 256 | class SearchBoxTestCase(BaseTestCase): |
| 257 | """Test the qt control panel.""" |
| 258 | |
| 259 | @@ -36,6 +38,7 @@ |
| 260 | def setUp(self): |
| 261 | yield super(SearchBoxTestCase, self).setUp() |
| 262 | |
| 263 | + self.patch(utils, "user_home", USER_HOME) |
| 264 | self.patch(self.ui._thread_explore, "get_folder_info", |
| 265 | self.fake_get_folder_info) |
| 266 | self.patch(self.ui._thread_explore, "start", |
| 267 | @@ -52,6 +55,11 @@ |
| 268 | os.path.join(USER_HOME, 'blabla', 'iop'), |
| 269 | ] |
| 270 | } |
| 271 | + self._slot_item = None |
| 272 | + |
| 273 | + def fake_slot(self, item): |
| 274 | + """Fake function to be called when itemSelected is emitted.""" |
| 275 | + self._slot_item = item |
| 276 | |
| 277 | def fake_get_folder_info(self, folder): |
| 278 | """Fake get_folder_info.""" |
| 279 | @@ -132,7 +140,33 @@ |
| 280 | self.ui._process_volumes_info([(0, 0, data1), (0, 0, data2)]) |
| 281 | self.ui.popup.list_widget.setCurrentRow(1) |
| 282 | current = self.ui.popup.list_widget.currentRow() |
| 283 | + self.ui.itemSelected.connect(self.fake_slot) |
| 284 | self.ui._key_return_pressed(current) |
| 285 | + expected = unicode(self._slot_item).replace('~', USER_HOME) |
| 286 | + self.assertEqual(expected, self.folder_info['folder2'][2]) |
| 287 | + |
| 288 | + def test_mouse_click_pressed(self): |
| 289 | + """Check the proper actions are executed when click is pressed.""" |
| 290 | + data1 = [{'path': 'folder1'}] |
| 291 | + data2 = [{'realpath': 'folder2'}] |
| 292 | + self.ui._process_volumes_info([(0, 0, data1), (0, 0, data2)]) |
| 293 | + self.ui.popup.list_widget.setCurrentRow(1) |
| 294 | + current = self.ui.popup.list_widget.currentItem() |
| 295 | + self.ui.itemSelected.connect(self.fake_slot) |
| 296 | + self.ui.popup.list_widget.itemPressed.emit(current) |
| 297 | + expected = unicode(self._slot_item).replace('~', USER_HOME) |
| 298 | + self.assertEqual(expected, self.folder_info['folder2'][2]) |
| 299 | + |
| 300 | + def test_mouse_scroll(self): |
| 301 | + """Check that fetch_more is called when we reach the end of scroll.""" |
| 302 | + data1 = [{'path': 'folder1'}] |
| 303 | + data2 = [{'realpath': 'folder2'}] |
| 304 | + self.ui._process_volumes_info([(0, 0, data1), (0, 0, data2)]) |
| 305 | + self.patch(self.ui.popup, "fetch_more", self._set_called) |
| 306 | + self.ui._scroll_fetch_more( |
| 307 | + self.ui.popup.list_widget.verticalScrollBar().maximum()) |
| 308 | + expected = (([],), {}) |
| 309 | + self.assertEqual(expected, self._called) |
| 310 | |
| 311 | def test_key_space_pressed(self): |
| 312 | """Check the proper actions are executed on key space pressed.""" |
| 313 | @@ -163,12 +197,12 @@ |
| 314 | data2 = [{'realpath': 'folder2'}] |
| 315 | self.ui._process_volumes_info([(0, 0, data1), (0, 0, data2)]) |
| 316 | expected = [ |
| 317 | - 'other_path/test/qwe', |
| 318 | - '~/blabla/iop', |
| 319 | - '~/one/file3', |
| 320 | - '~/test/asd', |
| 321 | - '~/ubuntu/file1', |
| 322 | - '~/ubuntu/file2'] |
| 323 | + os.path.join('other_path', 'test', 'qwe'), |
| 324 | + os.path.join('~', 'blabla', 'iop'), |
| 325 | + os.path.join('~', 'one', 'file3'), |
| 326 | + os.path.join('~', 'test', 'asd'), |
| 327 | + os.path.join('~', 'ubuntu', 'file1'), |
| 328 | + os.path.join('~', 'ubuntu', 'file2')] |
| 329 | self.assertEqual(self.ui._thread_explore.u1_files, expected) |
| 330 | self.assertEqual(self.ui.popup.list_widget.count(), 6) |
| 331 | |
| 332 | @@ -183,15 +217,15 @@ |
| 333 | data2 = [{'realpath': 'folder2'}] |
| 334 | self.ui._process_volumes_info([(0, 0, data1), (0, 0, data2)]) |
| 335 | self.ui.filter('p') |
| 336 | - expected = ['~/blabla/iop'] |
| 337 | + expected = [os.path.join('~', 'blabla', 'iop')] |
| 338 | self.assertEqual(expected, self.ui.temp_u1_files) |
| 339 | |
| 340 | self.ui.filter('i') |
| 341 | expected = [ |
| 342 | - '~/blabla/iop', |
| 343 | - '~/one/file3', |
| 344 | - '~/ubuntu/file1', |
| 345 | - '~/ubuntu/file2'] |
| 346 | + os.path.join('~', 'blabla', 'iop'), |
| 347 | + os.path.join('~', 'one', 'file3'), |
| 348 | + os.path.join('~', 'ubuntu', 'file1'), |
| 349 | + os.path.join('~', 'ubuntu', 'file2')] |
| 350 | self.assertEqual(expected, self.ui.temp_u1_files) |
| 351 | |
| 352 | def test_set_selected_item(self): |
| 353 | @@ -204,7 +238,8 @@ |
| 354 | self.ui.popup.list_widget.setCurrentRow(0) |
| 355 | item = self.ui.popup.list_widget.currentItem() |
| 356 | self.ui._set_selected_item(item) |
| 357 | - self.assertEqual(self._called, ((u'other_path/test/qwe',), {})) |
| 358 | + self.assertEqual(self._called, |
| 359 | + ((os.path.join(u'other_path', u'test', u'qwe'),), {})) |
| 360 | self.assertEqual(self.ui.text(), '') |
| 361 | |
| 362 | |
| 363 | |
| 364 | === modified file 'ubuntuone/controlpanel/utils/darwin.py' |
| 365 | --- ubuntuone/controlpanel/utils/darwin.py 2012-08-06 16:47:44 +0000 |
| 366 | +++ ubuntuone/controlpanel/utils/darwin.py 2012-10-02 18:05:36 +0000 |
| 367 | @@ -20,15 +20,322 @@ |
| 368 | import shutil |
| 369 | import sys |
| 370 | |
| 371 | +from ctypes import ( |
| 372 | + byref, |
| 373 | + CDLL, |
| 374 | + POINTER, |
| 375 | + Structure, |
| 376 | + c_bool, |
| 377 | + c_char_p, |
| 378 | + c_double, |
| 379 | + c_int32, |
| 380 | + c_long, |
| 381 | + c_uint32, |
| 382 | + c_void_p) |
| 383 | + |
| 384 | +from ctypes.util import find_library |
| 385 | + |
| 386 | from twisted.internet import defer |
| 387 | |
| 388 | from dirspec.basedir import save_config_path |
| 389 | from ubuntuone.controlpanel.logger import setup_logging |
| 390 | |
| 391 | logger = setup_logging('utils.darwin') |
| 392 | + |
| 393 | AUTOUPDATE_BIN_NAME = 'autoupdate-darwin' |
| 394 | UNINSTALL_BIN_NAME = 'uninstall-darwin' |
| 395 | |
| 396 | +FSEVENTSD_JOB_LABEL = "com.ubuntu.one.fsevents" |
| 397 | + |
| 398 | +# pylint: disable=C0103 |
| 399 | +CFPath = find_library("CoreFoundation") |
| 400 | +CF = CDLL(CFPath) |
| 401 | + |
| 402 | +CFRelease = CF.CFRelease |
| 403 | +CFRelease.restype = None |
| 404 | +CFRelease.argtypes = [c_void_p] |
| 405 | + |
| 406 | +kCFStringEncodingUTF8 = 0x08000100 |
| 407 | + |
| 408 | + |
| 409 | +class CFRange(Structure): |
| 410 | + """CFRange Struct""" |
| 411 | + _fields_ = [("location", c_long), |
| 412 | + ("length", c_long)] |
| 413 | + |
| 414 | +CFShow = CF.CFShow |
| 415 | +CFShow.argtypes = [c_void_p] |
| 416 | +CFShow.restype = None |
| 417 | + |
| 418 | +CFStringCreateWithCString = CF.CFStringCreateWithCString |
| 419 | +CFStringCreateWithCString.restype = c_void_p |
| 420 | +CFStringCreateWithCString.argtypes = [c_void_p, c_void_p, c_uint32] |
| 421 | + |
| 422 | +kCFAllocatorDefault = c_void_p() |
| 423 | + |
| 424 | +CFErrorCopyDescription = CF.CFErrorCopyDescription |
| 425 | +CFErrorCopyDescription.restype = c_void_p |
| 426 | +CFErrorCopyDescription.argtypes = [c_void_p] |
| 427 | + |
| 428 | +CFDictionaryGetValue = CF.CFDictionaryGetValue |
| 429 | +CFDictionaryGetValue.restype = c_void_p |
| 430 | +CFDictionaryGetValue.argtypes = [c_void_p, c_void_p] |
| 431 | + |
| 432 | +CFArrayGetValueAtIndex = CF.CFArrayGetValueAtIndex |
| 433 | +CFArrayGetValueAtIndex.restype = c_void_p |
| 434 | +CFArrayGetValueAtIndex.argtypes = [c_void_p, c_uint32] |
| 435 | + |
| 436 | +CFURLCreateWithFileSystemPath = CF.CFURLCreateWithFileSystemPath |
| 437 | +CFURLCreateWithFileSystemPath.restype = c_void_p |
| 438 | +CFURLCreateWithFileSystemPath.argtypes = [c_void_p, c_void_p, c_uint32, c_bool] |
| 439 | + |
| 440 | +CFBundleCopyInfoDictionaryForURL = CF.CFBundleCopyInfoDictionaryForURL |
| 441 | +CFBundleCopyInfoDictionaryForURL.restype = c_void_p |
| 442 | +CFBundleCopyInfoDictionaryForURL.argtypes = [c_void_p] |
| 443 | + |
| 444 | +CFStringGetDoubleValue = CF.CFStringGetDoubleValue |
| 445 | +CFStringGetDoubleValue.restype = c_double |
| 446 | +CFStringGetDoubleValue.argtypes = [c_void_p] |
| 447 | + |
| 448 | +SecurityPath = find_library("Security") |
| 449 | +Security = CDLL(SecurityPath) |
| 450 | + |
| 451 | +AuthorizationCreate = Security.AuthorizationCreate |
| 452 | +AuthorizationCreate.restype = c_int32 |
| 453 | +AuthorizationCreate.argtypes = [c_void_p, c_void_p, c_int32, c_void_p] |
| 454 | + |
| 455 | +AuthorizationFree = Security.AuthorizationFree |
| 456 | +AuthorizationFree.restype = c_uint32 |
| 457 | +AuthorizationFree.argtypes = [c_void_p, c_uint32] |
| 458 | + |
| 459 | +kAuthorizationFlagDefaults = 0 |
| 460 | +kAuthorizationFlagInteractionAllowed = 1 << 0 |
| 461 | +kAuthorizationFlagExtendRights = 1 << 1 |
| 462 | +kAuthorizationFlagDestroyRights = 1 << 3 |
| 463 | +kAuthorizationFlagPreAuthorize = 1 << 4 |
| 464 | + |
| 465 | +kAuthorizationEmptyEnvironment = None |
| 466 | + |
| 467 | +errAuthorizationSuccess = 0 |
| 468 | +errAuthorizationDenied = -60005 |
| 469 | +errAuthorizationCanceled = -60006 |
| 470 | +errAuthorizationInteractionNotAllowed = -60007 |
| 471 | + |
| 472 | +ServiceManagementPath = find_library("ServiceManagement") |
| 473 | +ServiceManagement = CDLL(ServiceManagementPath) |
| 474 | + |
| 475 | +kSMRightBlessPrivilegedHelper = "com.apple.ServiceManagement.blesshelper" |
| 476 | +kSMRightModifySystemDaemons = "com.apple.ServiceManagement.daemons.modify" |
| 477 | + |
| 478 | +# pylint: disable=E1101 |
| 479 | +# c_void_p has no "in_dll" member: |
| 480 | +kSMDomainSystemLaunchd = c_void_p.in_dll(ServiceManagement, |
| 481 | + "kSMDomainSystemLaunchd") |
| 482 | +# pylint: enable=E1101 |
| 483 | + |
| 484 | +SMJobBless = ServiceManagement.SMJobBless |
| 485 | +SMJobBless.restype = c_bool |
| 486 | +SMJobBless.argtypes = [c_void_p, c_void_p, c_void_p, POINTER(c_void_p)] |
| 487 | + |
| 488 | +SMJobRemove = ServiceManagement.SMJobRemove |
| 489 | +SMJobRemove.restype = c_bool |
| 490 | +SMJobRemove.argtypes = [c_void_p, c_void_p, c_void_p, c_bool, |
| 491 | + POINTER(c_void_p)] |
| 492 | + |
| 493 | +SMJobCopyDictionary = ServiceManagement.SMJobCopyDictionary |
| 494 | +SMJobCopyDictionary.restype = c_void_p |
| 495 | +SMJobCopyDictionary.argtypes = [c_void_p, c_void_p] |
| 496 | + |
| 497 | + |
| 498 | +class AuthorizationItem(Structure): |
| 499 | + """AuthorizationItem Struct""" |
| 500 | + _fields_ = [("name", c_char_p), |
| 501 | + ("valueLength", c_uint32), |
| 502 | + ("value", c_void_p), |
| 503 | + ("flags", c_uint32)] |
| 504 | + |
| 505 | + |
| 506 | +class AuthorizationRights(Structure): |
| 507 | + """AuthorizationRights Struct""" |
| 508 | + _fields_ = [("count", c_uint32), |
| 509 | + # * 1 here is specific to our use below |
| 510 | + ("items", POINTER(AuthorizationItem))] |
| 511 | + |
| 512 | + |
| 513 | +class DaemonInstallException(Exception): |
| 514 | + """Error securely installing daemon.""" |
| 515 | + |
| 516 | + |
| 517 | +class AuthUserCanceledException(Exception): |
| 518 | + """The user canceled the authorization.""" |
| 519 | + |
| 520 | + |
| 521 | +class AuthFailedException(Exception): |
| 522 | + """The authorization faild for some reason.""" |
| 523 | + |
| 524 | + |
| 525 | +class DaemonRemoveException(Exception): |
| 526 | + """Error removing existing daemon.""" |
| 527 | + |
| 528 | + |
| 529 | +class DaemonVersionMismatchException(Exception): |
| 530 | + """Incompatible version of the daemon found.""" |
| 531 | + |
| 532 | + |
| 533 | +def create_cfstr(s): |
| 534 | + """Creates a CFString from a python string. |
| 535 | + |
| 536 | + Note - because this is a "create" function, you have to CFRelease |
| 537 | + the returned string. |
| 538 | + """ |
| 539 | + return CFStringCreateWithCString(kCFAllocatorDefault, |
| 540 | + s.encode('utf8'), |
| 541 | + kCFStringEncodingUTF8) |
| 542 | + |
| 543 | + |
| 544 | +def get_bundle_version(path_cfstr): |
| 545 | + """Returns a float version from the plist of the given bundle. |
| 546 | + |
| 547 | + path_cfstr must be a CFStringRef. |
| 548 | + """ |
| 549 | + cfurl = CFURLCreateWithFileSystemPath(None, # use default allocator |
| 550 | + path_cfstr, |
| 551 | + 0, # POSIX style path |
| 552 | + False) |
| 553 | + plist = CFBundleCopyInfoDictionaryForURL(cfurl) |
| 554 | + ver_key_cfstr = create_cfstr("CFBundleVersion") |
| 555 | + version_cfstr = CFDictionaryGetValue(plist, ver_key_cfstr) |
| 556 | + |
| 557 | + version = CFStringGetDoubleValue(version_cfstr) |
| 558 | + |
| 559 | + CFRelease(cfurl) |
| 560 | + CFRelease(plist) |
| 561 | + CFRelease(ver_key_cfstr) |
| 562 | + |
| 563 | + return version |
| 564 | + |
| 565 | + |
| 566 | +def get_fsevents_daemon_installed_version(): |
| 567 | + """Returns helper version or None if helper is not installed.""" |
| 568 | + |
| 569 | + label_cfstr = create_cfstr(FSEVENTSD_JOB_LABEL) |
| 570 | + job_data_cfdict = SMJobCopyDictionary(kSMDomainSystemLaunchd, |
| 571 | + label_cfstr) |
| 572 | + CFRelease(label_cfstr) |
| 573 | + |
| 574 | + if job_data_cfdict is not None: |
| 575 | + key_cfstr = create_cfstr("ProgramArguments") |
| 576 | + args_cfarray = CFDictionaryGetValue(job_data_cfdict, key_cfstr) |
| 577 | + |
| 578 | + path_cfstr = CFArrayGetValueAtIndex(args_cfarray, 0) |
| 579 | + version = get_bundle_version(path_cfstr) |
| 580 | + |
| 581 | + # only release things "copied" or "created", not "got". |
| 582 | + CFRelease(job_data_cfdict) |
| 583 | + CFRelease(key_cfstr) |
| 584 | + return version |
| 585 | + |
| 586 | + return None |
| 587 | + |
| 588 | + |
| 589 | +def get_authorization(): |
| 590 | + """Get authorization to remove and/or install daemons.""" |
| 591 | + |
| 592 | + # pylint: disable=W0201 |
| 593 | + authItemBless = AuthorizationItem() |
| 594 | + authItemBless.name = kSMRightBlessPrivilegedHelper |
| 595 | + authItemBless.valueLength = 0 |
| 596 | + authItemBless.value = None |
| 597 | + authItemBless.flags = 0 |
| 598 | + |
| 599 | + authRights = AuthorizationRights() |
| 600 | + authRights.count = 1 |
| 601 | + authRights.items = (AuthorizationItem * 1)(authItemBless) |
| 602 | + |
| 603 | + flags = (kAuthorizationFlagDefaults | |
| 604 | + kAuthorizationFlagInteractionAllowed | |
| 605 | + kAuthorizationFlagPreAuthorize | |
| 606 | + kAuthorizationFlagExtendRights) |
| 607 | + |
| 608 | + authRef = c_void_p() |
| 609 | + |
| 610 | + status = AuthorizationCreate(byref(authRights), |
| 611 | + kAuthorizationEmptyEnvironment, |
| 612 | + flags, |
| 613 | + byref(authRef)) |
| 614 | + |
| 615 | + if status != errAuthorizationSuccess: |
| 616 | + |
| 617 | + if status == errAuthorizationInteractionNotAllowed: |
| 618 | + raise AuthFailedException("Authorization failed: " |
| 619 | + "interaction not allowed.") |
| 620 | + |
| 621 | + elif status == errAuthorizationDenied: |
| 622 | + raise AuthFailedException("Authorization failed: auth denied.") |
| 623 | + |
| 624 | + else: |
| 625 | + raise AuthUserCanceledException() |
| 626 | + |
| 627 | + if authRef is None: |
| 628 | + raise AuthFailedException("No authRef from AuthorizationCreate: %r" |
| 629 | + % status) |
| 630 | + return authRef |
| 631 | + |
| 632 | + |
| 633 | +def install_fsevents_daemon(authRef): |
| 634 | + """Call SMJobBless to install daemon. |
| 635 | + |
| 636 | + No return, raises on error. |
| 637 | + """ |
| 638 | + |
| 639 | + desc_cfstr = None |
| 640 | + |
| 641 | + try: |
| 642 | + error = c_void_p() |
| 643 | + |
| 644 | + label_cfstr = create_cfstr(FSEVENTSD_JOB_LABEL) |
| 645 | + ok = SMJobBless(kSMDomainSystemLaunchd, |
| 646 | + label_cfstr, |
| 647 | + authRef, |
| 648 | + byref(error)) |
| 649 | + CFRelease(label_cfstr) |
| 650 | + |
| 651 | + if not ok: |
| 652 | + desc_cfstr = CFErrorCopyDescription(error) |
| 653 | + CFShow(desc_cfstr) |
| 654 | + raise DaemonInstallException("SMJobBless error (see above)") |
| 655 | + |
| 656 | + finally: |
| 657 | + if desc_cfstr: |
| 658 | + CFRelease(desc_cfstr) |
| 659 | + |
| 660 | + |
| 661 | +def remove_fsevents_daemon(authRef): |
| 662 | + """Call SMJobRemove to remove daemon. |
| 663 | + |
| 664 | + No return, raises on error. |
| 665 | + """ |
| 666 | + desc_cfstr = None |
| 667 | + try: |
| 668 | + error = c_void_p() |
| 669 | + |
| 670 | + label_cfstr = create_cfstr(FSEVENTSD_JOB_LABEL) |
| 671 | + ok = SMJobRemove(kSMDomainSystemLaunchd, |
| 672 | + label_cfstr, |
| 673 | + authRef, |
| 674 | + True, |
| 675 | + byref(error)) |
| 676 | + |
| 677 | + CFRelease(label_cfstr) |
| 678 | + |
| 679 | + if not ok: |
| 680 | + desc_cfstr = CFErrorCopyDescription(error) |
| 681 | + CFShow(desc_cfstr) |
| 682 | + raise DaemonRemoveException("SMJobRemove error (see above)") |
| 683 | + finally: |
| 684 | + if desc_cfstr: |
| 685 | + CFRelease(desc_cfstr) |
| 686 | + |
| 687 | |
| 688 | def add_to_autostart(): |
| 689 | """Add syncdaemon to the session's autostart.""" |
| 690 | @@ -50,6 +357,72 @@ |
| 691 | return folders |
| 692 | |
| 693 | |
| 694 | +def check_and_install_fsevents_daemon(main_app_dir): |
| 695 | + """Checks version of running daemon, maybe installs. |
| 696 | + |
| 697 | + 'main_app_dir' is the path to the running app. |
| 698 | + |
| 699 | + This will securely install the daemon bundled with the running app |
| 700 | + if there is no currently installed one, or upgrade it if the |
| 701 | + installed one is old. If the installed one is newer, it raises |
| 702 | + a DaemonVersionMismatchException. |
| 703 | + """ |
| 704 | + |
| 705 | + daemon_path = os.path.join(main_app_dir, 'Contents', |
| 706 | + 'Library', 'LaunchServices', |
| 707 | + FSEVENTSD_JOB_LABEL) |
| 708 | + bundled_version = get_bundle_version(create_cfstr(daemon_path)) |
| 709 | + |
| 710 | + installed_version = get_fsevents_daemon_installed_version() |
| 711 | + |
| 712 | + if installed_version == bundled_version: |
| 713 | + logger.info("Current fsevents daemon already installed: version %r" % |
| 714 | + installed_version) |
| 715 | + return |
| 716 | + |
| 717 | + if installed_version > bundled_version: |
| 718 | + desc = ("Found newer fsevents daemon:" |
| 719 | + " installed %r > bundled version %r." % |
| 720 | + (installed_version, bundled_version)) |
| 721 | + logger.error(desc) |
| 722 | + raise DaemonVersionMismatchException(desc) |
| 723 | + |
| 724 | + authRef = get_authorization() |
| 725 | + |
| 726 | + if (installed_version is not None and |
| 727 | + installed_version < bundled_version): |
| 728 | + logger.info("Found installed daemon version %r < %r, removing." % |
| 729 | + (installed_version, bundled_version)) |
| 730 | + |
| 731 | + try: |
| 732 | + remove_fsevents_daemon(authRef) |
| 733 | + except DaemonRemoveException, e: |
| 734 | + logger.exception("Problem removing running daemon: %r" % e) |
| 735 | + |
| 736 | + AuthorizationFree(authRef, kAuthorizationFlagDestroyRights) |
| 737 | + |
| 738 | + logger.info("Installing daemon version %r" % bundled_version) |
| 739 | + |
| 740 | + try: |
| 741 | + authRef = get_authorization() |
| 742 | + install_fsevents_daemon(authRef) |
| 743 | + |
| 744 | + installed_version = get_fsevents_daemon_installed_version() |
| 745 | + |
| 746 | + if installed_version: |
| 747 | + logger.info("Installed fsevents daemon successfully: version %r" % |
| 748 | + installed_version) |
| 749 | + else: |
| 750 | + logger.error("Error installing fsevents daemon, see system.log.") |
| 751 | + |
| 752 | + except DaemonInstallException, e: |
| 753 | + logger.exception("Exception in fsevents daemon installation: %r" % e) |
| 754 | + raise e |
| 755 | + |
| 756 | + finally: |
| 757 | + AuthorizationFree(authRef, kAuthorizationFlagDestroyRights) |
| 758 | + |
| 759 | + |
| 760 | def install_config_and_daemons(): |
| 761 | """Install required data files and fsevents daemon. |
| 762 | |
| 763 | @@ -80,6 +453,8 @@ |
| 764 | if not os.path.exists(dest_path): |
| 765 | shutil.copyfile(src_path, dest_path) |
| 766 | |
| 767 | + check_and_install_fsevents_daemon(main_app_dir) |
| 768 | + |
| 769 | |
| 770 | def perform_update(): |
| 771 | """Spawn the autoupdate process and call the stop function.""" |
| 772 | |
| 773 | === modified file 'ubuntuone/controlpanel/utils/tests/test_darwin.py' |
| 774 | --- ubuntuone/controlpanel/utils/tests/test_darwin.py 2012-08-06 16:47:44 +0000 |
| 775 | +++ ubuntuone/controlpanel/utils/tests/test_darwin.py 2012-10-02 18:05:36 +0000 |
| 776 | @@ -19,6 +19,9 @@ |
| 777 | import os |
| 778 | import sys |
| 779 | |
| 780 | +from collections import defaultdict |
| 781 | +from functools import partial |
| 782 | + |
| 783 | from twisted.internet import defer |
| 784 | |
| 785 | from ubuntuone.controlpanel import utils |
| 786 | @@ -28,6 +31,28 @@ |
| 787 | # pylint: disable=W0212 |
| 788 | |
| 789 | |
| 790 | +class CallRecordingTestCase(TestCase): |
| 791 | + """Base class with multi-call checker.""" |
| 792 | + |
| 793 | + @defer.inlineCallbacks |
| 794 | + def setUp(self): |
| 795 | + """Set up call checker.""" |
| 796 | + yield super(CallRecordingTestCase, self).setUp() |
| 797 | + self._called = defaultdict(list) |
| 798 | + |
| 799 | + def _patch_and_track(self, module, funcs): |
| 800 | + """Record calls along with function name.""" |
| 801 | + |
| 802 | + def record_call(fname, retval, *args, **kwargs): |
| 803 | + """Wrapper for a single function.""" |
| 804 | + self._called[fname].append((args, kwargs)) |
| 805 | + return retval |
| 806 | + |
| 807 | + for (fname, retval) in funcs: |
| 808 | + wrapper = partial(record_call, fname, retval) |
| 809 | + self.patch(module, fname, wrapper) |
| 810 | + |
| 811 | + |
| 812 | class InstallConfigTestCase(TestCase): |
| 813 | """Test install_config_and_daemons.""" |
| 814 | |
| 815 | @@ -37,6 +62,10 @@ |
| 816 | yield super(InstallConfigTestCase, self).setUp() |
| 817 | self._called = [] |
| 818 | |
| 819 | + self.patch(utils.darwin, |
| 820 | + 'check_and_install_fsevents_daemon', |
| 821 | + lambda _: None) |
| 822 | + |
| 823 | def _set_called(self, *args, **kwargs): |
| 824 | """Store 'args' and 'kwargs for test assertions.""" |
| 825 | self._called.append((args, kwargs)) |
| 826 | @@ -76,3 +105,200 @@ |
| 827 | """When frozen, we do not copy the conf files if they do exist.""" |
| 828 | self._test_copying_conf_files(True) |
| 829 | self.assertEqual(self._called, []) |
| 830 | + |
| 831 | + |
| 832 | +class InstallDaemonTestCase(CallRecordingTestCase): |
| 833 | + """Test fsevents daemon installation.""" |
| 834 | + |
| 835 | + @defer.inlineCallbacks |
| 836 | + def setUp(self): |
| 837 | + """Set up patched & tracked calls.""" |
| 838 | + yield super(InstallDaemonTestCase, self).setUp() |
| 839 | + |
| 840 | + self._patch_and_track(utils.darwin, |
| 841 | + [('get_authorization', 'Fake AuthRef'), |
| 842 | + ('remove_fsevents_daemon', None), |
| 843 | + ('install_fsevents_daemon', None), |
| 844 | + ('AuthorizationFree', None)]) |
| 845 | + |
| 846 | + def _patch_versions(self, installed, bundled): |
| 847 | + """Convenience to patch the version-getting functions.""" |
| 848 | + self.patch(utils.darwin, |
| 849 | + "get_bundle_version", |
| 850 | + lambda _: bundled) |
| 851 | + self.patch(utils.darwin, |
| 852 | + "get_fsevents_daemon_installed_version", |
| 853 | + lambda: installed) |
| 854 | + |
| 855 | + def test_check_and_install_current_version(self): |
| 856 | + """Test that we do nothing on current version""" |
| 857 | + |
| 858 | + self._patch_versions(installed=47.0, bundled=47.0) |
| 859 | + |
| 860 | + utils.darwin.check_and_install_fsevents_daemon('NOT A REAL DIR') |
| 861 | + |
| 862 | + self.assertEqual(self._called.keys(), []) |
| 863 | + |
| 864 | + def test_check_and_install_upgrade(self): |
| 865 | + """Test removing old daemon and installing new one.""" |
| 866 | + |
| 867 | + self._patch_versions(installed=35.0, bundled=35.1) |
| 868 | + |
| 869 | + utils.darwin.check_and_install_fsevents_daemon('NOT A REAL DIR') |
| 870 | + |
| 871 | + self.assertEqual(self._called['get_authorization'], |
| 872 | + [((), {}), ((), {})]) |
| 873 | + self.assertEqual(self._called['remove_fsevents_daemon'], |
| 874 | + [(('Fake AuthRef',), {})]) |
| 875 | + self.assertEqual(self._called['install_fsevents_daemon'], |
| 876 | + [(('Fake AuthRef',), {})]) |
| 877 | + self.assertEqual(self._called['AuthorizationFree'], |
| 878 | + [(('Fake AuthRef', |
| 879 | + utils.darwin.kAuthorizationFlagDestroyRights), {}), |
| 880 | + (('Fake AuthRef', |
| 881 | + utils.darwin.kAuthorizationFlagDestroyRights), {}) |
| 882 | + ]) |
| 883 | + |
| 884 | + def test_check_and_install_mismatch(self): |
| 885 | + """Test raising when we're older than the daemon.""" |
| 886 | + |
| 887 | + self._patch_versions(installed=102.5, bundled=66.0) |
| 888 | + |
| 889 | + self.assertRaises(utils.darwin.DaemonVersionMismatchException, |
| 890 | + utils.darwin.check_and_install_fsevents_daemon, |
| 891 | + 'NOT A REAL DIR') |
| 892 | + self.assertEqual(self._called.keys(), []) |
| 893 | + |
| 894 | + |
| 895 | +class CFCallsTestCase(CallRecordingTestCase): |
| 896 | + """Test functions that call CoreFoundation API.""" |
| 897 | + |
| 898 | + @defer.inlineCallbacks |
| 899 | + def setUp(self): |
| 900 | + """Set up call checker.""" |
| 901 | + yield super(CFCallsTestCase, self).setUp() |
| 902 | + self._called = defaultdict(list) |
| 903 | + self.patch(utils.darwin, 'create_cfstr', |
| 904 | + lambda s: s) |
| 905 | + self.patch(utils.darwin, 'CFShow', |
| 906 | + lambda _: None) |
| 907 | + self.patch(utils.darwin, 'kSMDomainSystemLaunchd', |
| 908 | + 'not a c_void_p') |
| 909 | + |
| 910 | + def test_remove_daemon_ok(self): |
| 911 | + """Test that we call SMJobRemove and don't raise when it returns OK.""" |
| 912 | + self._patch_and_track(utils.darwin, [('SMJobRemove', True), |
| 913 | + ('c_void_p', 'notaptr'), |
| 914 | + ('byref', 'not a **'), |
| 915 | + ('CFRelease', 'ignore')]) |
| 916 | + |
| 917 | + utils.darwin.remove_fsevents_daemon('not an authref') |
| 918 | + self.assertEqual(self._called['SMJobRemove'], |
| 919 | + [(('not a c_void_p', |
| 920 | + utils.darwin.FSEVENTSD_JOB_LABEL, |
| 921 | + 'not an authref', |
| 922 | + True, 'not a **'), {})]) |
| 923 | + self.assertEqual(self._called['CFRelease'], |
| 924 | + [((utils.darwin.FSEVENTSD_JOB_LABEL,), {})]) |
| 925 | + |
| 926 | + def test_remove_daemon_not_ok(self): |
| 927 | + """Test that we raise when SMJobRemove returns not OK.""" |
| 928 | + self._patch_and_track(utils.darwin, [('SMJobRemove', False), |
| 929 | + ('c_void_p', 'notaptr'), |
| 930 | + ('byref', 'not a **'), |
| 931 | + ('CFRelease', 'ignore'), |
| 932 | + ('CFErrorCopyDescription', |
| 933 | + 'Houston, we have a problem')]) |
| 934 | + |
| 935 | + self.assertRaises(utils.darwin.DaemonRemoveException, |
| 936 | + utils.darwin.remove_fsevents_daemon, |
| 937 | + 'not an authref') |
| 938 | + |
| 939 | + def test_install_daemon_ok(self): |
| 940 | + """Test that we call SMJobBless and don't raise when it returns OK.""" |
| 941 | + self._patch_and_track(utils.darwin, [('SMJobBless', True), |
| 942 | + ('c_void_p', 'notaptr'), |
| 943 | + ('byref', 'not a **'), |
| 944 | + ('CFRelease', 'ignore')]) |
| 945 | + |
| 946 | + utils.darwin.install_fsevents_daemon('not an authref') |
| 947 | + self.assertEqual(self._called['SMJobBless'], |
| 948 | + [(('not a c_void_p', |
| 949 | + utils.darwin.FSEVENTSD_JOB_LABEL, |
| 950 | + 'not an authref', |
| 951 | + 'not a **'), {})]) |
| 952 | + self.assertEqual(self._called['CFRelease'], |
| 953 | + [((utils.darwin.FSEVENTSD_JOB_LABEL,), {})]) |
| 954 | + |
| 955 | + def test_install_daemon_not_ok(self): |
| 956 | + """Test that we raise when SMJobBless returns not OK.""" |
| 957 | + self._patch_and_track(utils.darwin, [('SMJobBless', False), |
| 958 | + ('c_void_p', 'notaptr'), |
| 959 | + ('byref', 'not a **'), |
| 960 | + ('CFRelease', 'ignore'), |
| 961 | + ('CFErrorCopyDescription', |
| 962 | + 'Houston, we have a problem')]) |
| 963 | + |
| 964 | + self.assertRaises(utils.darwin.DaemonInstallException, |
| 965 | + utils.darwin.install_fsevents_daemon, |
| 966 | + 'not an authref') |
| 967 | + |
| 968 | + def test_get_bundle_version(self): |
| 969 | + """Simple test of #calls in get_bundle_version.""" |
| 970 | + # This list includes expected counts. This test is mostly good |
| 971 | + # to check that we match the number of 'create's with the |
| 972 | + # number of 'releases', which is 3 (note create_cfstr is |
| 973 | + # patched in setUp.) |
| 974 | + to_track = [('CFURLCreateWithFileSystemPath', |
| 975 | + 'url', 1), |
| 976 | + ('CFBundleCopyInfoDictionaryForURL', |
| 977 | + 'dict', 1), |
| 978 | + ('CFDictionaryGetValue', 'val', 1), |
| 979 | + ('CFStringGetDoubleValue', 102.5, 1), |
| 980 | + ('CFRelease', 'ignore', 3)] |
| 981 | + self._patch_and_track(utils.darwin, [(n, r) for (n, r, _) in to_track]) |
| 982 | + |
| 983 | + utils.darwin.get_bundle_version("not a cfstr") |
| 984 | + for (name, _, num) in to_track: |
| 985 | + self.assertEqual(len(self._called[name]), num) |
| 986 | + |
| 987 | + def test_get_fsevents_daemon_installed_version_ok(self): |
| 988 | + """Test that we return the version if the dictionary is there.""" |
| 989 | + to_track = [('SMJobCopyDictionary', 'not none'), |
| 990 | + ('CFRelease', 'None'), |
| 991 | + ('CFDictionaryGetValue', 'val from dict'), |
| 992 | + ('CFArrayGetValueAtIndex', 'val'), |
| 993 | + ('get_bundle_version', 1.0)] |
| 994 | + self._patch_and_track(utils.darwin, to_track) |
| 995 | + utils.darwin.get_fsevents_daemon_installed_version() |
| 996 | + |
| 997 | + self.assertEqual(self._called['CFDictionaryGetValue'], |
| 998 | + [(('not none', "ProgramArguments"), {})]) |
| 999 | + self.assertEqual(self._called['CFArrayGetValueAtIndex'], |
| 1000 | + [(('val from dict', 0), {})]) |
| 1001 | + self.assertEqual(self._called['get_bundle_version'], |
| 1002 | + [(('val',), {})]) |
| 1003 | + |
| 1004 | + def test_get_fsevents_daemon_installed_version_not_found(self): |
| 1005 | + """Test that we return None if the dictionary is not there.""" |
| 1006 | + to_track = [('SMJobCopyDictionary', None), |
| 1007 | + ('CFRelease', 'None'), |
| 1008 | + ('CFDictionaryGetValue', 'val from dict'), |
| 1009 | + ('CFArrayGetValueAtIndex', 'val'), |
| 1010 | + ('get_bundle_version', 1.0)] |
| 1011 | + self._patch_and_track(utils.darwin, to_track) |
| 1012 | + utils.darwin.get_fsevents_daemon_installed_version() |
| 1013 | + |
| 1014 | + self.assertTrue('CFDictionaryGetValue' not in self._called.keys()) |
| 1015 | + self.assertTrue('CFArrayGetValueAtIndex' not in self._called.keys()) |
| 1016 | + self.assertTrue('get_bundle_version' not in self._called.keys()) |
| 1017 | + |
| 1018 | + def test_get_authorization_ok(self): |
| 1019 | + """Test successful call of AuthorizationCreate does not raise.""" |
| 1020 | + to_track = [('c_void_p', 'not void p'), |
| 1021 | + ('byref', 'not **'), |
| 1022 | + ('AuthorizationCreate', |
| 1023 | + utils.darwin.errAuthorizationSuccess)] |
| 1024 | + self._patch_and_track(utils.darwin, to_track) |
| 1025 | + auth_ref = utils.darwin.get_authorization() |
| 1026 | + self.assertEqual(auth_ref, 'not void p') |


The attempt to merge lp:~dobey/ubuntuone-control-panel/update-4-0 into lp:ubuntuone-control-panel/stable-4-0 failed. Below is the output from the failed tests.
*** Running DBus test suite *** controlpanel. dbustests. test_dbus_ service inTestCase dbus_service_ cant_register ... Control panel backend already running.
[OK] dbus_service_ main ... [OK] stCase cant_register_ twice ... [SKIPPED] dbus_busname_ created ... [OK] error_handler_ default ... [OK] error_handler_ with_exception ... [OK] error_handler_ with_failure ... [OK] error_handler_ with_non_ string_ dict ... [OK] error_handler_ with_string_ dict ... [OK] register_ service ... [OK] file_sync_ status_ changed ... [OK] file_sync_ status_ disabled ... [OK] file_sync_ status_ disconnected ... [OK] file_sync_ status_ error ... [OK] file_sync_ status_ idle ... [OK] file_sync_ status_ starting ... [OK] file_sync_ status_ stopped ... [OK] file_sync_ status_ syncing ... [OK] file_sync_ status_ unknown ... [OK] status_ changed_ handler ... [OK] status_ changed_ handler_ after_status_ requested ... [OK] status_ changed_ handler_ after_status_ requested_ twice ... [OK] hErrorTestCase account_ info_returned ... [OK] change_ device_ settings ... [OK] change_ replication_ settings ... [OK] change_ volume_ settings ... [OK] connect_ files ... [OK] devices_ info_returned ... [OK] disable_ files ... [OK] disconnect_ files ... [OK] enable_ files ... [OK] remove_ device ... [OK] replications_ info ... [OK] restart_ files ... [OK]
ubuntuone.
BaseTestCase
runTest ... [OK]
DBusServiceMa
test_
test_
DBusServiceTe
test_
test_
test_
test_
test_
test_
test_
test_
FileSyncTestCase
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
OperationsAut
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
test_
t...