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
Status: | Merged |
---|---|
Approved by: | dobey |
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) | Approve | ||
Review via email: mp+127559@code.launchpad.net |
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.
Revision history for this message
Mike McCracken (mikemc) : | # |
review:
Approve
Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote : | # |
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...