Merge lp:~ralsina/ubuntuone-windows-installer/local-folder-fixes into lp:ubuntuone-windows-installer

Proposed by Roberto Alsina
Status: Merged
Approved by: Roberto Alsina
Approved revision: 65
Merged at revision: 35
Proposed branch: lp:~ralsina/ubuntuone-windows-installer/local-folder-fixes
Merge into: lp:ubuntuone-windows-installer
Diff against target: 1091 lines (+749/-190)
4 files modified
data/qt/local_folders.ui (+1/-8)
ubuntuone_installer/gui/qt/local_folders.py (+148/-64)
ubuntuone_installer/gui/qt/tests/test_gui.py (+15/-118)
ubuntuone_installer/gui/qt/tests/test_local_folders.py (+585/-0)
To merge this branch: bzr merge lp:~ralsina/ubuntuone-windows-installer/local-folder-fixes
Reviewer Review Type Date Requested Status
John Lenton (community) Approve
Natalia Bidart (community) Approve
Review via email: mp+71857@code.launchpad.net

Commit message

Implement the "Computer-to-cloud" page of the installer according to spec and discussions.

Description of the change

Implement the "Computer-to-cloud" page of the installer according to spec and discussions.

To post a comment you must log in.
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

* We should add a new test case for FolderItem in particular, asserting at least the following:
 - the checkstate is unchecked
 - the volume id is the one passed as parameter
 - the thread is started if calculate is True, not started if not
 - size is None if calculate, 0 if not

* Also, I would advice moving the self.thread assignment outside the if-else since is the same one no matter the value of calculate

* 4 double quotes instead of 3 in:

404 + """"Fake volumes info."""

* Always call super() and not the parent class directly. So, in this case:

475 def tearDown(self):
476 """Remove the temporary tree."""
477 shutil.rmtree(self.tmpdir)
478 BaseTestCase.tearDown(self)

we should call super(). But, considering we can move the rmtree to the setUp method as a cleanup function (which is preferred than using tearDown), we can remove this tearDown altogether.

* in test_status_after_initialize, these two assertions:

         self.assertFalse(self.ui.folders_info is None)
         self.assertFalse(self.ui.account_info is None)

will likely fail on slow machines, since get_info() returns a deferred which initializePage() is not waiting for. So, I would advice converting initializePage into an asynchronous method and yielding on this in the test. Or, alternatively, since you are already providing a test for get_info, you can just assert get_info was called, and remove the checks for volume_info, account_info, items, timer, etc.
Basically, test_status_after_initialize should only check:

 - self.ui.offer_frame is not visible
 - self.wizard().overlay is visible
 - get_info was called

* we need another test similar to test_status_post_get_info but testing that all volumes which type is not UDF are ignored.

* I'm confused about the call to item.thread.join() in get_info. Can you please help me understand why is needed at that point?

* this is not correct: self.assertTrue(item.volume_id). Since volume_id is not a boolean, we should assert equality against the value used when mocking volumes_info.

* test_size_calculation is creating a thread when calling CalculateSize but I think that thread is not being joined on. If so, can you please fix that?

* are there tests for change_page? I see one for the timer being stopped, but we need some for the try-except clause, and all the block right after the page_id == self.wizard().SYNC_NOW_OR_LATER_PAGE guard. Also, never have errors passing silently, please add a log message when handling the KeyError in self.wizard().currentIdChanged.disconnect().

I will continue the review once these are fixed.

review: Needs Fixing
Revision history for this message
Roberto Alsina (ralsina) wrote :
Download full text (3.5 KiB)

> * We should add a new test case for FolderItem in particular, asserting at
> least the following:
> - the checkstate is unchecked
> - the volume id is the one passed as parameter
> - the thread is started if calculate is True, not started if not
> - size is None if calculate, 0 if not

Added.

> * Also, I would advice moving the self.thread assignment outside the if-else
> since is the same one no matter the value of calculate

Moved.

> * 4 double quotes instead of 3 in:
>
> 404 + """"Fake volumes info."""

Fixed.

> * Always call super() and not the parent class directly. So, in this case:
>
> 475 def tearDown(self):
> 476 """Remove the temporary tree."""
> 477 shutil.rmtree(self.tmpdir)
> 478 BaseTestCase.tearDown(self)
>
> we should call super(). But, considering we can move the rmtree to the setUp
> method as a cleanup function (which is preferred than using tearDown), we can
> remove this tearDown altogether.

Done.

> * in test_status_after_initialize, these two assertions:
>
> self.assertFalse(self.ui.folders_info is None)
> self.assertFalse(self.ui.account_info is None)
>
> will likely fail on slow machines, since get_info() returns a deferred which
> initializePage() is not waiting for. So, I would advice converting
> initializePage into an asynchronous method and yielding on this in the test.

Sadly that's not possible, because initializePage can't be a generator (it's called by Qt).

> Or, alternatively, since you are already providing a test for get_info, you
> can just assert get_info was called, and remove the checks for volume_info,
> account_info, items, timer, etc.
> Basically, test_status_after_initialize should only check:
>
> - self.ui.offer_frame is not visible
> - self.wizard().overlay is visible
> - get_info was called

Done.

> * we need another test similar to test_status_post_get_info but testing that
> all volumes which type is not UDF are ignored.

Added a volume of type "SHARE" to the fake volume_info, and it's checked to be ignored
(the number of items created stays the same)

> * I'm confused about the call to item.thread.join() in get_info. Can you
> please help me understand why is needed at that point?

It was there to ensure the thread was finished before setting item.size.
Fixed a buglet where I was not setting calculate=False when adding a UDF, so
it's not needed anymore.

> * this is not correct: self.assertTrue(item.volume_id). Since volume_id is not
> a boolean, we should assert equality against the value used when mocking
> volumes_info.

Changed.

> * test_size_calculation is creating a thread when calling CalculateSize but I
> think that thread is not being joined on. If so, can you please fix that?

No, it's not starting a thread, it's calling what the thread would run when started, which
is an ordinary function.

> * are there tests for change_page? I see one for the timer being stopped, but
> we need some for the try-except clause, and all the block right after the
> page_id == self.wizard().SYNC_NOW_OR_LATER_PAGE guard.

There are these, that I think cover the block after the if:

test_changed_page_existing_udf_behaviour (existing ...

Read more...

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

> > * Always call super() and not the parent class directly. So, in this case:
> >
> > 475 def tearDown(self):
> > 476 """Remove the temporary tree."""
> > 477 shutil.rmtree(self.tmpdir)
> > 478 BaseTestCase.tearDown(self)
> >
> > we should call super(). But, considering we can move the rmtree to the setUp
> > method as a cleanup function (which is preferred than using tearDown), we
> can
> > remove this tearDown altogether.
>
> Done.

Great! Small change: can you please call self.addCleanup(shutil.rmtree, self.tmpdir) instead of using a lambda?

> > * we need another test similar to test_status_post_get_info but testing that
> > all volumes which type is not UDF are ignored.
>
> Added a volume of type "SHARE" to the fake volume_info, and it's checked to be
> ignored
> (the number of items created stays the same)

Thanks for adding this. Can you please add an assertion that checks that the SHARE id is not being loaded? because we can have the same amount of created items and yet have a buggy content in items.

Something like: self.assertNotIn(the-share-id, self.ui.items)

I will keep reviewing later today!

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

> Great! Small change: can you please call self.addCleanup(shutil.rmtree,
> self.tmpdir) instead of using a lambda?

Done.

> Thanks for adding this. Can you please add an assertion that checks that the
> SHARE id is not being loaded? because we can have the same amount of created
> items and yet have a buggy content in items.
>
> Something like: self.assertNotIn(the-share-id, self.ui.items)

Added!

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

* The comment added:

# Block until we have server data828944

may be misleading, since the app does not block but shows a loading message until the result from the backend is ready.

* If get_info fails, the overlay is never hid nor the user finds out that something went wrong. I filed bug #828944 so you can fix this in an incoming branch.

* test_exception_on_account_info should yield on get_info so we ensure that the call does not explodes "later". In this test, when solving bug #828944, you should check the overlay was hidden and a message was shown to the user (we may need input from design here).

* there are no tests for the showing/hiding of the overlay inside change_page.

* there are no tests for add_folder when the params validate, calculate and volume_id are other than the defaults. Also, having volume_id defaulting to False may be a typo? volume_id, when defined, is a uuid...

* it seems to me that update_sizes is not completely covered by tests. I think that the try-except for the RuntimeError is not tested, as well as the while True. For that last feature, we should have more than one thing being calculated its size, to show the sum of the sizes.
Also, we should confirm we're testing this case:

"if the user has no UDFs created and selects nothing" -> we should show the quota used only

and that the show_hide_offer method received the proper parameter.

* correct me if I'm wrong, but seems like there is no test for on_folder_list_itemChanged nor on_add_folder_button_clicked.

* question: would you agree to move all the tests for the local_folders module into its own test module, so we can follow the convention? The convention is: per every foo.py, there should be a test_foo.py.

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

> * The comment added:
>
> # Block until we have server data828944
>
> may be misleading, since the app does not block but shows a loading message
> until the result from the backend is ready.

Changed.

> * If get_info fails, the overlay is never hid nor the user finds out that
> something went wrong. I filed bug #828944 so you can fix this in an incoming
> branch.

Thanks, will look into it.

> * test_exception_on_account_info should yield on get_info so we ensure that
> the call does not explodes "later". In this test, when solving bug #828944,
> you should check the overlay was hidden and a message was shown to the user
> (we may need input from design here).

Agreed.

> * there are no tests for the showing/hiding of the overlay inside change_page.

Added asserts to the changed_page tests.

> * there are no tests for add_folder when the params validate, calculate and
> volume_id are other than the defaults. Also, having volume_id defaulting to
> False may be a typo? volume_id, when defined, is a uuid...

I just wanted something to say "if not volume_id". I will default it to None.

> * it seems to me that update_sizes is not completely covered by tests. I think
> that the try-except for the RuntimeError is not tested, as well as the while
> True. For that last feature, we should have more than one thing being
> calculated its size, to show the sum of the sizes.

The RuntimeError only happens when a list item is deleted by Qt before its thread
returns the result (for example, if the user leaves the page and comes back),
and it just means we can ignore that item. I am not sure how to test that, because
if I delete the items manually, it will not cause that error, unless the thread
was running while I did it. It's pretty tricky to reproduce.

The while True breaks when the Queue is empty (which will happen eventually ;-),
or any other exception occurs. You mean add a test to ensure it ends?

> Also, we should confirm we're testing this case:
>
> "if the user has no UDFs created and selects nothing" -> we should show the
> quota used only

This was covered by test_total_size_unchecked (when there are UDFs, it should return the quota used)
and I now added test_total_size_unchecked_no_udfs (nothing checked, no UDFs, no quota used, should be 0)

> and that the show_hide_offer method received the proper parameter.

Added a fake show_hide_offer and asserts to check that.

> * correct me if I'm wrong, but seems like there is no test for
> on_folder_list_itemChanged

added test_on_folder_list_item_changed_col_0 and col_1

> nor on_add_folder_button_clicked.

This was already covered by test_add_folder_clicked_valid and test_add_folder_clicked_invalid.

> * question: would you agree to move all the tests for the local_folders module
> into its own test module, so we can follow the convention? The convention is:
> per every foo.py, there should be a test_foo.py.

Sure. Moved.

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

> > * there are no tests for add_folder when the params validate, calculate and
> > volume_id are other than the defaults. Also, having volume_id defaulting to
> > False may be a typo? volume_id, when defined, is a uuid...
>
> I just wanted something to say "if not volume_id". I will default it to None.

Makes sense.

Also, did you add tests cases for the params not having the default values?

> > * it seems to me that update_sizes is not completely covered by tests. I
> think
> > that the try-except for the RuntimeError is not tested, as well as the while
> > True. For that last feature, we should have more than one thing being
> > calculated its size, to show the sum of the sizes.
>
> The RuntimeError only happens when a list item is deleted by Qt before its
> thread
> returns the result (for example, if the user leaves the page and comes back),
> and it just means we can ignore that item. I am not sure how to test that,
> because
> if I delete the items manually, it will not cause that error, unless the
> thread
> was running while I did it. It's pretty tricky to reproduce.

You can patch item.setText, or humanize, to raise RuntimeError and check that it was handled.

> The while True breaks when the Queue is empty (which will happen eventually
> ;-),
> or any other exception occurs. You mean add a test to ensure it ends?

Nopes, I mean that if we remove the while True, I think the tests keep passing. And I think that is caused becasue no test is ensuring that the whole content of the queue is used to calculate the final size.

Thanks!

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

> > > * there are no tests for add_folder when the params validate, calculate
> and
> > > volume_id are other than the defaults. Also, having volume_id defaulting
> to
> > > False may be a typo? volume_id, when defined, is a uuid...
> >
> > I just wanted something to say "if not volume_id". I will default it to
> None.
>
> Makes sense.
>
> Also, did you add tests cases for the params not having the default values?

Yes, madam!

test_not_validating_invalid_item_addition
test_not_calculating_item_addition
test_volume_id_item_addition

> > > * it seems to me that update_sizes is not completely covered by tests. I
> > think
> > > that the try-except for the RuntimeError is not tested, as well as the
> while
> > > True. For that last feature, we should have more than one thing being
> > > calculated its size, to show the sum of the sizes.
> >
> > The RuntimeError only happens when a list item is deleted by Qt before its
> > thread
> > returns the result (for example, if the user leaves the page and comes
> back),
> > and it just means we can ignore that item. I am not sure how to test that,
> > because
> > if I delete the items manually, it will not cause that error, unless the
> > thread
> > was running while I did it. It's pretty tricky to reproduce.
>
> You can patch item.setText, or humanize, to raise RuntimeError and check that
> it was handled.

Ok, will do!

> > The while True breaks when the Queue is empty (which will happen eventually
> > ;-),
> > or any other exception occurs. You mean add a test to ensure it ends?
>
> Nopes, I mean that if we remove the while True, I think the tests keep
> passing. And I think that is caused becasue no test is ensuring that the whole
> content of the queue is used to calculate the final size.

Actually, it works without the while True, it just does it slower (it may require the timer calling
the function several times). But sure, I could do a test putting some stuff in the queue and checking that it's empty at the end.

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Simple lint and pep8 issues:

ubuntuone_installer/gui/qt/tests/test_gui.py:
    30: [W0611] Unused import TestCase

./ubuntuone_installer/gui/qt/local_folders.py:33:24: E202 whitespace before ')'
./ubuntuone_installer/gui/qt/tests/test_local_folders.py:80:35: E202 whitespace before '}'

review: Needs Fixing
Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Approving lint issues are being fixed ATM.

review: Approve
Revision history for this message
John Lenton (chipaca) wrote :

ok

review: Approve
Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :

Attempt to merge into lp:ubuntuone-windows-installer failed due to conflicts:

text conflict in ubuntuone_installer/gui/qt/tests/test_gui.py

Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

Revision history for this message
Ubuntu One Auto Pilot (otto-pilot) wrote :

Attempt to merge into lp:ubuntuone-windows-installer failed due to conflicts:

text conflict in ubuntuone_installer/gui/qt/tests/test_gui.py

65. By Roberto Alsina

resolve further

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'data/qt/local_folders.ui'
--- data/qt/local_folders.ui 2011-08-08 21:48:11 +0000
+++ data/qt/local_folders.ui 2011-08-23 13:43:25 +0000
@@ -97,7 +97,7 @@
97 </spacer>97 </spacer>
98 </item>98 </item>
99 <item>99 <item>
100 <widget class="AddFolderButton" name="add_folder_button">100 <widget class="QPushButton" name="add_folder_button">
101 <property name="text">101 <property name="text">
102 <string>Add a folder from this computer</string>102 <string>Add a folder from this computer</string>
103 </property>103 </property>
@@ -188,13 +188,6 @@
188 </item>188 </item>
189 </layout>189 </layout>
190 </widget>190 </widget>
191 <customwidgets>
192 <customwidget>
193 <class>AddFolderButton</class>
194 <extends>QPushButton</extends>
195 <header>ubuntuone.controlpanel.gui.qt.addfolder</header>
196 </customwidget>
197 </customwidgets>
198 <resources/>191 <resources/>
199 <connections/>192 <connections/>
200</ui>193</ui>
201194
=== modified file 'ubuntuone_installer/gui/qt/local_folders.py'
--- ubuntuone_installer/gui/qt/local_folders.py 2011-08-15 21:41:54 +0000
+++ ubuntuone_installer/gui/qt/local_folders.py 2011-08-23 13:43:25 +0000
@@ -24,9 +24,14 @@
24import threading24import threading
25import Queue25import Queue
2626
27from twisted.internet.defer import inlineCallbacks27from twisted.internet.defer import inlineCallbacks, returnValue
28from PyQt4 import QtCore, QtGui28from PyQt4 import QtCore, QtGui
29from ubuntuone.controlpanel.gui import humanize, sign_url29from ubuntuone.controlpanel import backend
30from ubuntuone.controlpanel.gui import (
31 humanize,
32 sign_url,
33 FOLDER_INVALID_PATH,
34)
30from ubuntuone.platform.credentials import CredentialsManagementTool35from ubuntuone.platform.credentials import CredentialsManagementTool
31from ubuntu_sso.qt.gui import SSOWizardPage36from ubuntu_sso.qt.gui import SSOWizardPage
3237
@@ -49,12 +54,19 @@
4954
50class FolderItem(QtGui.QTreeWidgetItem):55class FolderItem(QtGui.QTreeWidgetItem):
51 """Class representing a folder in the folder list UI."""56 """Class representing a folder in the folder list UI."""
52 def __init__(self, strings, path=None, queue=None):57 def __init__(self, strings, path=None, queue=None,
58 calculate=True, volume_id=None):
53 super(FolderItem, self).__init__(strings)59 super(FolderItem, self).__init__(strings)
54 self.thread = CalculateSize(path, queue)60 self.thread = CalculateSize(path, queue)
55 self.thread.start()61 # Don't calculate sizes of UDFs
56 self.size = None62 if calculate and not volume_id:
63 self.thread.start()
64 self.size = None
65 else:
66 self.size = 0
57 self.path = path67 self.path = path
68 self.setCheckState(0, QtCore.Qt.Unchecked)
69 self.volume_id = volume_id
5870
5971
60class CalculateSize(threading.Thread):72class CalculateSize(threading.Thread):
@@ -83,22 +95,17 @@
83 local_folders_ui.Ui_Form(), None, parent)95 local_folders_ui.Ui_Form(), None, parent)
84 self.setTitle(LOCAL_FOLDERS_TITLE)96 self.setTitle(LOCAL_FOLDERS_TITLE)
85 self.setSubTitle()97 self.setSubTitle()
86
87 header_view = self.ui.folder_list.header()98 header_view = self.ui.folder_list.header()
88 header_view.setResizeMode(0, header_view.Stretch)99 header_view.setResizeMode(0, header_view.Stretch)
89
90 self.queue = Queue.Queue()100 self.queue = Queue.Queue()
91 self.timer = QtCore.QTimer()101 self.timer = QtCore.QTimer()
92 self.items = {}102 self.items = {}
93 for folder_name in self.default_folders():103 self.folders_info = None
94 self.add_folder(folder_name)104 self.account_info = None
95 self.update_sizes()105 self.cp_backend = backend.ControlBackend()
96 self.timer.start(2000)
97 self.timer.timeout.connect(self.update_sizes)
98106
99 # initializePage is inherited107 # initializePage is inherited
100 # pylint: disable=C0103108 # pylint: disable=C0103
101 @inlineCallbacks
102 def initializePage(self):109 def initializePage(self):
103 """UI details."""110 """UI details."""
104 self.wizard().setButtonLayout([111 self.wizard().setButtonLayout([
@@ -107,15 +114,75 @@
107 self.wizard()._next_id = self.wizard().SYNC_NOW_OR_LATER_PAGE114 self.wizard()._next_id = self.wizard().SYNC_NOW_OR_LATER_PAGE
108 # Start with this invisible115 # Start with this invisible
109 self.ui.offer_frame.setVisible(False)116 self.ui.offer_frame.setVisible(False)
110 if not self.ui.folder_list.topLevelItemCount():117 # Show overlay until we have server data
118 self.wizard().overlay.show()
119 self.get_info()
120
121 @inlineCallbacks
122 def get_info(self):
123 """Get information from CP backend and fill folder list."""
124 # pylint: disable=W0702
125 try:
126 volumes_info = yield self.cp_backend.volumes_info()
127 self.account_info = yield self.cp_backend.account_info()
128 self.folders_info = []
129 for _, _, volumes in volumes_info:
130 for volume in volumes:
131 if volume[u'type'] == u"UDF":
132 self.folders_info.append(volume)
133 self.ui.folder_list.clear()
134 for folder in self.folders_info:
135 item = yield self.add_folder(
136 os.path.expanduser(folder['path']),
137 validate=False,
138 volume_id=folder['volume_id'],
139 calculate=False,
140 )
141 if item:
142 if folder['subscribed']:
143 item.setCheckState(0, QtCore.Qt.Checked)
144 item.size = 0
111 for folder_name in self.default_folders():145 for folder_name in self.default_folders():
112 self.add_folder(folder_name)146 item = yield self.add_folder(folder_name, validate=True)
113 yield self.update_sizes()147 self.timer.start(2000)
148 self.timer.timeout.connect(self.update_sizes)
149 self.wizard().overlay.hide()
150 self.wizard().currentIdChanged.connect(self.changed_page)
151 except:
152 logger.exception("Error getting backend info:")
153
154 @QtCore.pyqtSlot("int")
155 @inlineCallbacks
156 def changed_page(self, page_id):
157 """When moving to next page, create/[un]subscribe UDFs."""
158 self.timer.stop()
159 try:
160 self.wizard().currentIdChanged.disconnect(self.changed_page)
161 except KeyError:
162 pass
163 if page_id == self.wizard().SYNC_NOW_OR_LATER_PAGE:
164 # The page following this one
165 self.wizard().overlay.show()
166 for path, item in self.items.items():
167 if item.checkState(0) == QtCore.Qt.Checked:
168 if item.volume_id:
169 yield self.cp_backend.change_volume_settings(
170 item.volume_id,
171 dict(subscribed=True))
172 else:
173 yield self.cp_backend.create_folder(path)
174 else:
175 if item.volume_id:
176 yield self.cp_backend.change_volume_settings(
177 item.volume_id,
178 dict(subscribed=False))
179 self.wizard().overlay.hide()
114180
115 def default_folders(self):181 def default_folders(self):
116 """Return a list of the folders to add by default."""182 """Return a list of the folders to add by default."""
117 if sys.platform == 'win32':183 if sys.platform == 'win32':
118 # Special Folder "My Documents"184 # XXXX to be replaced by calls to xdg_base_directory's
185 # special_folders
119 dll = ctypes.windll.shell32186 dll = ctypes.windll.shell32
120 buf = ctypes.create_string_buffer(300)187 buf = ctypes.create_string_buffer(300)
121 dll.SHGetSpecialFolderPathA(None, buf, 5, False)188 dll.SHGetSpecialFolderPathA(None, buf, 5, False)
@@ -129,56 +196,60 @@
129 result = ['To be implemented']196 result = ['To be implemented']
130 return result197 return result
131198
132 def add_folder(self, path):199 @inlineCallbacks
200 def add_folder(self, path, validate=True, calculate=True, volume_id=None):
133 """Add a folder to the list."""201 """Add a folder to the list."""
134 if path in self.items:202 if path in self.items:
135 return None203 returnValue(None)
136 # FIXME: the path should actually be sent to u1cp to verify as valid204 if validate:
137 item = FolderItem([path, "", "remove"], path=path, queue=self.queue)205 is_valid = yield self.cp_backend.validate_path_for_folder(path)
138 self.ui.folder_list.addTopLevelItem(item)206 else:
139 self.items[path] = item207 is_valid = True
140 return item208 if is_valid:
209 item = FolderItem([path, ""],
210 path=path,
211 queue=self.queue,
212 volume_id=volume_id,
213 calculate=calculate)
214 self.ui.folder_list.addTopLevelItem(item)
215 self.items[path] = item
216 returnValue(item)
217 returnValue(None)
141218
142 @inlineCallbacks
143 def update_sizes(self):219 def update_sizes(self):
144 """Poll the queue were the threads put the size info."""220 """Poll the queue were the threads put the size info."""
145 try:221 while True:
146 path, size = self.queue.get(False)222 try:
147 item = self.items.get(path)223 path, size = self.queue.get(False)
148 if item:224 except Queue.Empty:
149 item.size = size225 break
150 item.setText(1, humanize(size))226 else:
151 except Queue.Empty:227 item = self.items.get(path)
152 pass228 if item:
153 total = 0229 item.size = size
230 try:
231 item.setText(1, humanize(size))
232 except RuntimeError:
233 del self.items[path]
234 total = long(self.account_info['quota_used'])
154 for path, item in self.items.items():235 for path, item in self.items.items():
155 if item.size is None:236 if item.size is None:
156 total = LOCAL_FOLDERS_CALCULATING237 total = LOCAL_FOLDERS_CALCULATING
157 break238 break
158 total += item.size239 if not item.volume_id and item.checkState(0) == QtCore.Qt.Checked:
159240 # Existing UDFs are already accounted for, count if marked.
241 total += item.size
160 if isinstance(total, long):242 if isinstance(total, long):
161 yield self.show_hide_offer(total)243 self.show_hide_offer(total)
162 total = humanize(total)244 total = humanize(total)
163 else:245 else:
164 yield self.show_hide_offer(0)246 self.show_hide_offer(0)
165 self.ui.folder_list.headerItem().setText(247 self.ui.folder_list.headerItem().setText(
166 1, LOCAL_FOLDERS_SPACE_HEADER % total)248 1, LOCAL_FOLDERS_SPACE_HEADER % total)
167249
168 @inlineCallbacks
169 def show_hide_offer(self, cur_size):250 def show_hide_offer(self, cur_size):
170 """Show or hide the offer to buy space according to the total size.251 """Show or hide the offer to buy space according to the total size."""
171252 quota = self.account_info['quota_total']
172 Returns a deferred that is triggered when the update is finished.
173
174 """
175 # pylint: disable=W0702
176 try:
177 user_info = yield self.ui.add_folder_button.backend.account_info()
178 except:
179 logger.exception('Error while trying to get account info:')
180
181 quota = user_info['quota_total']
182 if cur_size > quota:253 if cur_size > quota:
183 self.ui.offer_frame.setVisible(True)254 self.ui.offer_frame.setVisible(True)
184 else:255 else:
@@ -193,31 +264,44 @@
193264
194 # itemClicked is a Qt signal name.265 # itemClicked is a Qt signal name.
195 # pylint: disable=C0103266 # pylint: disable=C0103
196 def on_folder_list_itemClicked(self, item, column):267 def on_folder_list_itemChanged(self, item, column):
197 """Delete folder from the list."""268 """Delete folder from the list."""
198 if column == 2:269 if column == 0:
199 del(self.items[item.path])270 self.update_sizes()
200 item.thread._stop = True
201 self.ui.folder_list.takeTopLevelItem(
202 self.ui.folder_list.indexOfTopLevelItem(item))
203 self.update_sizes()
204271
205 @inlineCallbacks272 @inlineCallbacks
206 @QtCore.pyqtSlot()273 @QtCore.pyqtSlot()
207 def on_add_storage_button_clicked(self):274 def on_add_storage_button_clicked(self):
208 """user clicked on the "Add more storage" button."""275 """user clicked on the "Add more storage" button."""
209276 # Really want to catch everything
210 # pylint: disable=W0702277 # pylint: disable=W0702
278 url = "https://one.ubuntu.com/services/#storage_panel"
211 try:279 try:
212 credtool = CredentialsManagementTool()280 credtool = CredentialsManagementTool()
213 creds = yield credtool.find_credentials()281 creds = yield credtool.find_credentials()
214 except:282 except:
215 logger.exception('Error while trying to update que quota:')283 logger.exception('Error while trying to get credentials:')
216 creds = {}284 creds = {}
217
218 if creds:285 if creds:
219 signed_url = sign_url(286 signed_url = sign_url(url, creds)
220 "https://one.ubuntu.com/services/#storage_panel", creds)
221 else:287 else:
222 signed_url = "https://one.ubuntu.com/services/#storage_panel"288 signed_url = url
223 QtGui.QDesktopServices.openUrl(QtCore.QUrl(signed_url))289 QtGui.QDesktopServices.openUrl(QtCore.QUrl(signed_url))
290
291 @inlineCallbacks
292 @QtCore.pyqtSlot()
293 def on_add_folder_button_clicked(self):
294 """user clicked on the "Add Folder" button."""
295 folder = QtGui.QFileDialog.getExistingDirectory(parent=self)
296 folder = unicode(folder)
297 if folder == '':
298 return
299
300 is_valid = yield self.cp_backend.validate_path_for_folder(folder)
301 if not is_valid:
302 user_home = os.path.expanduser('~')
303 text = FOLDER_INVALID_PATH % {'folder_path': folder,
304 'home_folder': user_home}
305 QtGui.QMessageBox.warning(self, '', text, QtGui.QMessageBox.Close)
306 return
307 yield self.add_folder(folder, validate=False, volume_id=False)
224308
=== modified file 'ubuntuone_installer/gui/qt/tests/test_gui.py'
--- ubuntuone_installer/gui/qt/tests/test_gui.py 2011-08-22 14:05:02 +0000
+++ ubuntuone_installer/gui/qt/tests/test_gui.py 2011-08-23 13:43:25 +0000
@@ -20,9 +20,6 @@
20"""Tests for the Qt UI."""20"""Tests for the Qt UI."""
2121
22import os22import os
23import Queue
24import shutil
25import tempfile
2623
27from twisted.internet import defer24from twisted.internet import defer
28from PyQt4 import QtGui, QtCore25from PyQt4 import QtGui, QtCore
@@ -32,7 +29,6 @@
32from ubuntuone_installer.gui.qt import gui29from ubuntuone_installer.gui.qt import gui
33from ubuntuone_installer.gui.qt.tests import BaseTestCase30from ubuntuone_installer.gui.qt.tests import BaseTestCase
34from ubuntuone_installer.gui.qt import forgotten31from ubuntuone_installer.gui.qt import forgotten
35from ubuntuone_installer.gui.qt import local_folders
36from ubuntuone_installer.gui.qt import setup_account32from ubuntuone_installer.gui.qt import setup_account
37from ubuntuone_installer.gui.qt.ui import (33from ubuntuone_installer.gui.qt.ui import (
38 setup_account_ui,34 setup_account_ui,
@@ -217,6 +213,7 @@
217 self.ui.restart()213 self.ui.restart()
218 self.ui.show()214 self.ui.show()
219215
216 # pylint: disable=W0212
220 preferences_page._button_clicked(QtGui.QWizard.CustomButton2)217 preferences_page._button_clicked(QtGui.QWizard.CustomButton2)
221 self.assertEqual(218 self.assertEqual(
222 preferences_page.preferences_widget.restore_button_clicked, True)219 preferences_page.preferences_widget.restore_button_clicked, True)
@@ -245,6 +242,7 @@
245 self.assertEqual(tree_folders.isHeaderHidden(), True)242 self.assertEqual(tree_folders.isHeaderHidden(), True)
246 tree_folders = folders_page.folders_widget.ui.folders243 tree_folders = folders_page.folders_widget.ui.folders
247 self.assertEqual(tree_folders.isColumnHidden(2), True)244 self.assertEqual(tree_folders.isColumnHidden(2), True)
245 # pylint: disable=W0212
248 self.assertEqual(self.ui._next_id, self.ui.CONGRATULATIONS_PAGE)246 self.assertEqual(self.ui._next_id, self.ui.CONGRATULATIONS_PAGE)
249247
250 def test_folders_page_next_id(self):248 def test_folders_page_next_id(self):
@@ -571,120 +569,19 @@
571 self.shown = True569 self.shown = True
572570
573571
574class FakeCPBackend(object):572class FakeWizard(object):
575 """Fake Control Panel backend."""573 """Replace wizard() function on wizard pages."""
576574
577 def account_info(self, *args):575 params = None
578 """Fake account info."""576
579 return defer.succeed({"quota_total": 1000})577 # Invalid name "setButtonLayout"
580578 # pylint: disable=C0103
581579
582class FakeAddFolderButton(object):580 def setButtonLayout(self, *args, **kwargs):
583 """Fake Control Panel "Add Folder" button."""581 """Fake the functionality of setButtonLayout on QWizard class."""
584 backend = FakeCPBackend()582 FakeWizard.params = (args, kwargs)
585583
586584 # pylint: enable=C0103
587class LocalFoldersTestCase(BaseTestCase):
588 """Test the LocalFoldersPage code."""
589
590 class_ui = local_folders.LocalFoldersPage
591
592 def setUp(self):
593 """Initialize this test instance."""
594 # Create a test folder with a known size
595 self.tmpdir = tempfile.mkdtemp()
596 f = open(os.path.join(self.tmpdir, 'test_file'), 'wb')
597 f.write(" " * 600)
598 f.close()
599 os.mkdir(os.path.join(self.tmpdir, 'test_dir'))
600 f = open(os.path.join(self.tmpdir, 'test_dir', 'test_file_2'), 'wb')
601 f.write(" " * 737)
602 f.close()
603 super(LocalFoldersTestCase, self).setUp()
604
605 def tearDown(self):
606 """Remove the temporary tree."""
607 shutil.rmtree(self.tmpdir)
608 BaseTestCase.tearDown(self)
609
610 def test_size_calculation(self):
611 """Test the recursive folder size calculation."""
612 queue = Queue.Queue()
613 csize = local_folders.CalculateSize(self.tmpdir, queue)
614 csize.run()
615 path, size = queue.get()
616 self.assertEqual(path, self.tmpdir)
617 self.assertEqual(size, 1337)
618
619 def test_item_addition(self):
620 """Add an item (plus the default one), then remove them."""
621 self.ui.add_folder(self.tmpdir)
622 self.assertEqual(4, self.ui.ui.folder_list.topLevelItemCount())
623
624 def test_total_size(self):
625 """Test that the header reflects the change in item sizes."""
626 while self.ui.ui.folder_list.topLevelItemCount():
627 self.ui.on_folder_list_itemClicked(
628 self.ui.ui.folder_list.topLevelItem(0), 2)
629 self.assertEqual(0, self.ui.ui.folder_list.topLevelItemCount())
630 item = self.ui.add_folder(self.tmpdir)
631 item.size = 1337
632 item.thread.run()
633 item.thread.join()
634 self.ui.update_sizes()
635 self.assertEqual(unicode(self.ui.ui.folder_list.headerItem().text(1)),
636 u"Space (1337)")
637
638 def test_add_twice(self):
639 """Behaviour for adding the same folder twice.
640
641 * It's added only once.
642 """
643 while self.ui.ui.folder_list.topLevelItemCount():
644 self.ui.on_folder_list_itemClicked(
645 self.ui.ui.folder_list.topLevelItem(0), 2)
646 self.assertEqual(0, self.ui.ui.folder_list.topLevelItemCount())
647 self.ui.add_folder(self.tmpdir)
648 self.ui.add_folder(self.tmpdir)
649 self.assertEqual(1, self.ui.ui.folder_list.topLevelItemCount())
650
651 def test_add_missing_folder(self):
652 """Behaviour for adding a folder that doesn't exist.
653
654 * It's added.
655 * Has size 0.
656 """
657
658 while self.ui.ui.folder_list.topLevelItemCount():
659 self.ui.on_folder_list_itemClicked(
660 self.ui.ui.folder_list.topLevelItem(0), 2)
661 self.assertEqual(0, self.ui.ui.folder_list.topLevelItemCount())
662 item = self.ui.add_folder(os.path.join("xyzzy", "xyzzy", "xyzzy"))
663 self.assertEqual(1, self.ui.ui.folder_list.topLevelItemCount())
664 item.thread.run()
665 item.thread.join()
666 self.ui.update_sizes()
667 self.assertEqual(0, item.size)
668 # world did not explode
669
670 def test_over_quota(self):
671 """After removing all folders, offer_frame should be hidden.
672
673 Push the user over quota, it should be visible.
674 """
675 self.patch(self.ui.ui.offer_frame, "setVisible", self._set_called)
676 while self.ui.ui.folder_list.topLevelItemCount():
677 self.ui.on_folder_list_itemClicked(
678 self.ui.ui.folder_list.topLevelItem(0), 2)
679 self.assertEqual(0, self.ui.ui.folder_list.topLevelItemCount())
680 self.ui.update_sizes()
681 self.assertEqual(self._called, ((False,), {}))
682 self.ui.show_hide_offer(self.ui.quota() + 1)
683 self.assertEqual(self._called, ((True,), {}))
684
685
686LocalFoldersTestCase.skip = 'We need to re-write this test case entirely,' \
687 'see bug #824675 for reference.'
688585
689586
690class FakeWizard(object):587class FakeWizard(object):
691588
=== added file 'ubuntuone_installer/gui/qt/tests/test_local_folders.py'
--- ubuntuone_installer/gui/qt/tests/test_local_folders.py 1970-01-01 00:00:00 +0000
+++ ubuntuone_installer/gui/qt/tests/test_local_folders.py 2011-08-23 13:43:25 +0000
@@ -0,0 +1,585 @@
1# -*- coding: utf-8 -*-
2
3# Authors: Roberto Alsina <roberto.alsina@canonical.com>
4#
5# Copyright 2011 Canonical Ltd.
6#
7# This program is free software: you can redistribute it and/or modify it
8# under the terms of the GNU General Public License version 3, as published
9# by the Free Software Foundation.
10#
11# This program is distributed in the hope that it will be useful, but
12# WITHOUT ANY WARRANTY; without even the implied warranties of
13# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
14# PURPOSE. See the GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License along
17# with this program. If not, see <http://www.gnu.org/licenses/>.
18
19"""Tests for the local folders page."""
20
21import os
22import Queue
23import shutil
24import tempfile
25
26from twisted.internet import defer
27from PyQt4 import QtGui, QtCore
28
29from ubuntuone_installer.gui.qt import local_folders
30from ubuntuone_installer.gui.qt.tests import BaseTestCase, TestCase
31from ubuntuone_installer.gui.qt.tests.test_gui import FakeOverlay
32
33
34class FakeFileDialog(object):
35
36 """A fake QFileDialog class."""
37
38 # pylint: disable=C0103
39 def getExistingDirectory(self, *args, **kwargs):
40 """Fake existing folder name."""
41 return u"whatever"
42
43
44class FakeMessageBox(object):
45
46 """A fake QMessageBox class."""
47
48 Close = 2
49 _warning = None
50
51 def warning(self, *args, **kwargs):
52 """Fake warning."""
53 self._warning = (args, kwargs)
54
55
56class FakeCPBackend(object):
57 """Fake Control Panel backend."""
58
59 def __init__(self):
60 """Initialize."""
61 self._is_valid = True
62 self.volume_setings_changes = []
63 self.folders_created = []
64
65 def account_info(self, *args):
66 """Fake account info."""
67 return defer.succeed({
68 "quota_total": 1000,
69 "quota_used": 200,
70 })
71
72 def volumes_info(self, *args):
73 """Fake volumes info."""
74 return defer.succeed(((None, None,
75 [
76 {
77 'type': u'UDF',
78 'path': os.path.expanduser(u'~/xyzzy'),
79 'volume_id': 'asdfgh',
80 'subscribed': True,
81 },
82 {
83 'type': u'UDF',
84 'path': os.path.expanduser(u'~/zxyzzy'),
85 'volume_id': 'qwerty',
86 'subscribed': False,
87 },
88 {
89 'type': u'SHARE',
90 'path': os.path.expanduser(u'~/foobar'),
91 'volume_id': 'shared',
92 'subscribed': False,
93 },
94 ],
95 ),))
96
97 def validate_path_for_folder(self, path):
98 """Fake folder validation."""
99 return self._is_valid
100
101 def change_volume_settings(self, *args):
102 """Fake change volume settings."""
103 self.volume_setings_changes.append(args)
104
105 def create_folder(self, *args):
106 """Fake folder creation."""
107 self.folders_created.append(args)
108
109
110class FakeFailingCPBackend(object):
111 """Fake Control Panel backend that fails."""
112
113 def account_info(self, *args):
114 """Fake account info."""
115 return defer.fail(Exception())
116
117 def volumes_info(self, *args):
118 """Fake account info."""
119 return defer.fail(Exception())
120
121
122class FakeSignal(object):
123
124 """A fake PyQt signal."""
125
126 def __init__(self, *args, **kwargs):
127 """Initialize."""
128 self.target = None
129
130 def connect(self, target):
131 """Fake connect."""
132 self.target = target
133
134 def disconnect(self, *args):
135 """Fake disconnect."""
136 self.target = None
137
138 def emit(self, *args):
139 """Fake emit."""
140 if self.target:
141 self.target(*args)
142
143
144class FakeMainWindow(object):
145
146 """A fake MainWindow."""
147
148 loginSuccess = FakeSignal()
149 registrationSuccess = FakeSignal()
150 userCancellation = FakeSignal()
151 shown = False
152 _buttonlayout = None
153 SYNC_NOW_OR_LATER_PAGE = 4
154 overlay = FakeOverlay()
155 currentIdChanged = FakeSignal()
156
157 def show(self):
158 """Fake method."""
159 self.shown = True
160
161 # pylint: disable=C0103
162 def setButtonLayout(self, *args):
163 """Save ButtonLayout."""
164 self._buttonlayout = args
165
166
167class FakeCalculateSize(object):
168
169 """A fake CalculateSize."""
170
171 def __init__(self, *args, **kwargs):
172 self.started = False
173
174 def start(self):
175 """Fake start."""
176 self.started = True
177
178
179class FolderItemTestCase(TestCase):
180
181 """Tests for the FolderItem class."""
182
183 def test_folder_item_new_udf(self):
184 """Test when created like a new UDF."""
185 self.patch(local_folders, "CalculateSize", FakeCalculateSize)
186 item = local_folders.FolderItem(["s1", "s2"], "path")
187 self.assertTrue(item.size is None)
188 self.assertEqual(item.path, "path")
189 # pylint: disable=E1101
190 self.assertTrue(item.thread.started)
191 self.assertEqual(item.checkState(0), QtCore.Qt.Unchecked)
192 self.assertTrue(item.volume_id is None)
193
194 def test_folder_item_existing_udf(self):
195 """Test when created like an existing UDF."""
196 self.patch(local_folders, "CalculateSize", FakeCalculateSize)
197 item = local_folders.FolderItem(["s1", "s2"], "path",
198 calculate=False, volume_id="xyzzy")
199 self.assertEqual(item.size, 0)
200 self.assertEqual(item.path, "path")
201 # pylint: disable=E1101
202 self.assertFalse(item.thread.started)
203 self.assertEqual(item.checkState(0), QtCore.Qt.Unchecked)
204 self.assertEqual(item.volume_id, "xyzzy")
205
206
207class LocalFoldersTestCase(BaseTestCase):
208 """Test the LocalFoldersPage code."""
209
210 class_ui = local_folders.LocalFoldersPage
211
212 def setUp(self):
213 """Initialize this test instance."""
214 # Create a test folder with a known size
215 self.tmpdir = tempfile.mkdtemp()
216 f = open(os.path.join(self.tmpdir, 'test_file'), 'wb')
217 f.write(" " * 600)
218 f.close()
219 os.mkdir(os.path.join(self.tmpdir, 'test_dir'))
220 f = open(os.path.join(self.tmpdir, 'test_dir', 'test_file_2'), 'wb')
221 f.write(" " * 737)
222 f.close()
223 self.addCleanup(shutil.rmtree, self.tmpdir)
224 self.fake_wizard = FakeMainWindow()
225 self.patch(local_folders.backend, "ControlBackend", FakeCPBackend)
226 super(LocalFoldersTestCase, self).setUp()
227 self.patch(self.ui, "wizard", lambda: self.fake_wizard)
228
229 def test_status_before_initialize(self):
230 """Test status of the page components before initializePage()."""
231 self.assertIsInstance(self.ui.timer, QtCore.QTimer)
232 self.assertFalse(self.ui.timer.isActive())
233 self.assertTrue(self.ui.folders_info is None)
234 self.assertTrue(self.ui.account_info is None)
235 self.assertEqual(self.ui.items, {})
236 self.assertEqual(self.ui.ui.folder_list.topLevelItemCount(), 0)
237
238 def test_status_after_initialize(self):
239 """Test status of page components after initializePage()."""
240 self.patch(self.ui, "get_info", self._set_called)
241 self.ui.initializePage()
242 self.assertTrue(self.ui.wizard().overlay.show_counter, 1)
243 self.assertTrue(self.ui.wizard().overlay.hide_counter, 0)
244 self.assertFalse(self.ui.ui.offer_frame.isVisible())
245 self.assertEqual(self._called, ((), {}))
246
247 @defer.inlineCallbacks
248 def test_status_after_get_info(self):
249 """Test status of page components after get_info()."""
250 yield self.ui.get_info()
251 self.assertTrue(self.ui.wizard().overlay.hide_counter, 1)
252 self.assertEqual(self.ui.ui.folder_list.topLevelItemCount(), 5)
253 self.assertEqual(self.ui.wizard().currentIdChanged.target,
254 self.ui.changed_page)
255 self.assertEqual(
256 self.ui.items[os.path.expanduser('~/xyzzy')].checkState(0),
257 QtCore.Qt.Checked)
258 self.assertEqual(
259 self.ui.items[os.path.expanduser('~/zxyzzy')].checkState(0),
260 QtCore.Qt.Unchecked)
261 check_states = [self.ui.items[item].checkState(0)
262 for item in self.ui.items]
263 self.assertEqual(len([
264 x for x in check_states if x != QtCore.Qt.Unchecked]), 1)
265 self.assertNotIn('shared', self.ui.items)
266
267 @defer.inlineCallbacks
268 def test_subscribed_udf_checked(self):
269 """Check that subscribed UDF items are created correctly."""
270 yield self.ui.get_info()
271 item = self.ui.items[os.path.expanduser(u'~/xyzzy')]
272 self.assertEqual(item.checkState(0), QtCore.Qt.Checked)
273 self.assertEqual(item.size, 0)
274 self.assertEqual(item.volume_id, 'asdfgh')
275
276 @defer.inlineCallbacks
277 def test_unsubscribed_udf_checked(self):
278 """Check that unsubscribed UDF items are created correctly."""
279 yield self.ui.get_info()
280 item = self.ui.items[os.path.expanduser(u'~/zxyzzy')]
281 self.assertEqual(item.checkState(0), QtCore.Qt.Unchecked)
282 self.assertEqual(item.size, 0)
283 self.assertEqual(item.volume_id, 'qwerty')
284
285 def test_size_calculation(self):
286 """Test the recursive folder size calculation."""
287 queue = Queue.Queue()
288 csize = local_folders.CalculateSize(self.tmpdir, queue)
289 csize.run()
290 path, size = queue.get()
291 self.assertEqual(path, self.tmpdir)
292 self.assertEqual(size, 1337)
293
294 @defer.inlineCallbacks
295 def test_item_addition(self):
296 """Add a folder."""
297 self.ui.ui.folder_list.clear()
298 yield self.ui.add_folder(self.tmpdir)
299 self.assertEqual(1, self.ui.ui.folder_list.topLevelItemCount())
300
301 @defer.inlineCallbacks
302 def test_invalid_item_addition(self):
303 """Try to add an invalid folder."""
304 self.ui.ui.folder_list.clear()
305 self.ui.cp_backend._is_valid = False
306 yield self.ui.add_folder(self.tmpdir)
307 self.assertEqual(0, self.ui.ui.folder_list.topLevelItemCount())
308
309 @defer.inlineCallbacks
310 def test_not_validating_invalid_item_addition(self):
311 """Try to add an invalid folder, and not validate it."""
312 self.ui.ui.folder_list.clear()
313 self.ui.cp_backend._is_valid = False
314 yield self.ui.add_folder(self.tmpdir, validate=False)
315 self.assertEqual(1, self.ui.ui.folder_list.topLevelItemCount())
316
317 @defer.inlineCallbacks
318 def test_not_calculating_item_addition(self):
319 """Try to add a folder, and not calculate it."""
320 self.ui.ui.folder_list.clear()
321 yield self.ui.add_folder(self.tmpdir, calculate=False)
322 self.assertEqual(self.ui.items[self.tmpdir].size, 0)
323 self.assertEqual(1, self.ui.ui.folder_list.topLevelItemCount())
324
325 @defer.inlineCallbacks
326 def test_volume_id_item_addition(self):
327 """Try to add a folder with a volume_id."""
328 self.ui.ui.folder_list.clear()
329 yield self.ui.add_folder(self.tmpdir, volume_id="vol")
330 self.assertEqual(self.ui.items[self.tmpdir].volume_id, "vol")
331 self.assertEqual(1, self.ui.ui.folder_list.topLevelItemCount())
332
333 @defer.inlineCallbacks
334 def test_total_size(self):
335 """Test that the header reflects the change in item sizes."""
336 self.patch(self.ui, "show_hide_offer", self._set_called)
337 yield self.ui.get_info()
338 self.ui.ui.folder_list.clear()
339 self.ui.items = {}
340 item = yield self.ui.add_folder(self.tmpdir)
341 item.thread.run()
342 item.thread.join()
343 item.size = 1337
344 item.setCheckState(0, QtCore.Qt.Checked)
345 self.ui.update_sizes()
346 self.assertEqual(unicode(self.ui.ui.folder_list.headerItem().text(1)),
347 u"Space (1.5 KiB)")
348 self.assertEqual(self._called, ((1537L,), {}))
349
350 @defer.inlineCallbacks
351 def test_total_size_unchecked(self):
352 """Unchecked items use no space beyond quota_used."""
353 self.patch(self.ui, "show_hide_offer", self._set_called)
354 yield self.ui.get_info()
355 self.ui.ui.folder_list.clear()
356 self.ui.items = {}
357 item = yield self.ui.add_folder(self.tmpdir)
358 item.thread.run()
359 item.thread.join()
360 item.size = 1337
361 item.setCheckState(0, QtCore.Qt.Unchecked)
362 self.ui.update_sizes()
363 self.assertEqual(unicode(self.ui.ui.folder_list.headerItem().text(1)),
364 u"Space (200 bytes)")
365 self.assertEqual(self._called, ((200L,), {}))
366
367 @defer.inlineCallbacks
368 def test_total_size_unchecked_no_udfs(self):
369 """No checked items and no quota used should amount to 0 bytes."""
370 self.patch(self.ui, "show_hide_offer", self._set_called)
371 yield self.ui.get_info()
372 self.ui.account_info['quota_used'] = 0
373 self.ui.ui.folder_list.clear()
374 self.ui.items = {}
375 item = yield self.ui.add_folder(self.tmpdir)
376 item.thread.run()
377 item.thread.join()
378 item.size = 1337
379 item.setCheckState(0, QtCore.Qt.Unchecked)
380 self.ui.update_sizes()
381 self.assertEqual(unicode(self.ui.ui.folder_list.headerItem().text(1)),
382 u"Space (0 bytes)")
383 self.assertEqual(self._called, ((0,), {}))
384
385 @defer.inlineCallbacks
386 def test_total_size_udf(self):
387 """UDFs use no space beyond quota_used."""
388 self.patch(self.ui, "show_hide_offer", self._set_called)
389 yield self.ui.get_info()
390 for _, item in self.ui.items.items():
391 if item.thread.is_alive():
392 item.thread.join()
393 if item.volume_id:
394 item.setCheckState(0, QtCore.Qt.Checked)
395 else:
396 item.setCheckState(0, QtCore.Qt.Unchecked)
397 self.ui.update_sizes()
398 self.assertEqual(unicode(self.ui.ui.folder_list.headerItem().text(1)),
399 u"Space (200 bytes)")
400 self.assertEqual(self._called, ((200L,), {}))
401
402 def test_add_twice(self):
403 """Behaviour for adding the same folder twice:
404
405 * It's added only once.
406 """
407 self.ui.ui.folder_list.clear()
408 self.assertEqual(0, self.ui.ui.folder_list.topLevelItemCount())
409 self.ui.add_folder(self.tmpdir)
410 self.ui.add_folder(self.tmpdir)
411 self.assertEqual(1, self.ui.ui.folder_list.topLevelItemCount())
412
413 @defer.inlineCallbacks
414 def test_add_missing_folder(self):
415 """Behaviour for adding a folder that doesn't exist:
416
417 * It's added.
418 * Has size 0.
419 """
420
421 yield self.ui.get_info()
422 self.ui.ui.folder_list.clear()
423 self.assertEqual(0, self.ui.ui.folder_list.topLevelItemCount())
424 item = yield self.ui.add_folder(os.path.join(
425 "xyzzy", "xyzzy", "xyzzy"))
426 self.assertEqual(1, self.ui.ui.folder_list.topLevelItemCount())
427 item.thread.run()
428 item.thread.join()
429 self.ui.update_sizes()
430 self.assertEqual(0, item.size)
431 # world did not explode
432
433 def test_over_quota(self):
434 """After removing all folders, offer_frame should be hidden.
435
436 Push the user over quota, it should be visible.
437 """
438 self.patch(self.ui.ui.offer_frame, "setVisible", self._set_called)
439 yield self.ui.get_info()
440 self.ui.ui.folder_list.clear()
441 self.ui.update_sizes()
442 self.assertEqual(self._called, ((False,), {}))
443 self.ui.show_hide_offer(self.ui.quota() + 1)
444 self.assertEqual(self._called, ((True,), {}))
445
446 def test_add_folder_clicked_valid(self):
447 """Test behaviour when adding a valid folder via the button."""
448 self.patch(QtGui, "QFileDialog", FakeFileDialog())
449 self.patch(self.ui, "add_folder", self._set_called)
450 self.ui.ui.add_folder_button.click()
451 self.assertEqual(self._called,
452 ((u'whatever',), {'validate': False, 'volume_id': False}))
453
454 def test_add_folder_clicked_invalid(self):
455 """Test behaviour when adding an invalid folder via the button."""
456 self.patch(QtGui, "QFileDialog", FakeFileDialog())
457 message_box = FakeMessageBox()
458 self.patch(QtGui, "QMessageBox", message_box)
459 self.patch(self.ui, "add_folder", self._set_called)
460 self.ui.cp_backend._is_valid = False
461 self.ui.ui.add_folder_button.click()
462 self.assertEqual(self._called, False)
463 user_home = os.path.expanduser('~')
464 text = local_folders.FOLDER_INVALID_PATH % {
465 'folder_path': "whatever",
466 'home_folder': user_home,
467 }
468 # pylint: disable=W0212
469 self.assertEqual(message_box._warning,
470 ((self.ui,
471 '', text, 2), {}))
472
473 @defer.inlineCallbacks
474 def test_changed_page_existing_udf_behaviour(self):
475 """If a UDF is checked, subscribe it, if not, unsubscribe it."""
476 yield self.ui.get_info()
477 show_counter = self.ui.wizard().overlay.show_counter
478 hide_counter = self.ui.wizard().overlay.hide_counter
479 self.ui.changed_page(self.ui.wizard().SYNC_NOW_OR_LATER_PAGE)
480 self.assertEqual(self.ui.cp_backend.volume_setings_changes,
481 [('asdfgh', {'subscribed': True}),
482 ('qwerty', {'subscribed': False})])
483 self.assertEqual(self.ui.wizard().overlay.show_counter,
484 show_counter + 1)
485 self.assertEqual(self.ui.wizard().overlay.hide_counter,
486 hide_counter + 1)
487
488 @defer.inlineCallbacks
489 def test_changed_page_new_udf_behaviour(self):
490 """Create UDFs for non-existing, checked UDFs."""
491 yield self.ui.get_info()
492 show_counter = self.ui.wizard().overlay.show_counter
493 hide_counter = self.ui.wizard().overlay.hide_counter
494 self.ui.ui.folder_list.clear()
495 self.ui.items = {}
496 item = yield self.ui.add_folder("whatever")
497 item.setCheckState(0, QtCore.Qt.Checked)
498 item = yield self.ui.add_folder("whatever2")
499 item.setCheckState(0, QtCore.Qt.Unchecked)
500 self.ui.changed_page(self.ui.wizard().SYNC_NOW_OR_LATER_PAGE)
501 self.assertEqual(self.ui.cp_backend.folders_created,
502 [('whatever',)])
503 self.assertEqual(self.ui.wizard().overlay.show_counter,
504 show_counter + 1)
505 self.assertEqual(self.ui.wizard().overlay.hide_counter,
506 hide_counter + 1)
507
508 def test_exception_on_account_info(self):
509 """When account_info fails, nothing should happen."""
510 self.patch(self.ui,
511 "cp_backend", FakeFailingCPBackend())
512 self.ui.get_info()
513 # Still here
514
515 def test_timer_is_started(self):
516 """When displaying the page, the timer should start."""
517 self.ui.initializePage()
518 self.assertTrue(self.ui.timer.isActive())
519
520 def test_timer_is_stopped(self):
521 """When leaving the page, the timer should stop."""
522 self.ui.changed_page(-1)
523 self.assertFalse(self.ui.timer.isActive())
524
525 def test_special_folders(self):
526 """Test that default_folders does the right thing."""
527 folders = self.ui.default_folders()
528 self.assertEqual(len(folders), 3)
529 self.assertEqual(folders, [os.path.abspath(f) for f in folders])
530 # pylint: disable=W0404
531 import ctypes
532 dll = ctypes.windll.shell32
533 buf = ctypes.create_string_buffer(300)
534 dll.SHGetSpecialFolderPathA(None, buf, 5, False)
535 docs = buf.value
536 dll.SHGetSpecialFolderPathA(None, buf, 13, False)
537 music = buf.value
538 dll.SHGetSpecialFolderPathA(None, buf, 39, False)
539 pictures = buf.value
540 expected = [docs, music, pictures]
541 self.assertEqual(sorted(folders), sorted(expected))
542
543 def test_on_folder_list_item_changed_col_0(self):
544 """Test that it calls the right thing."""
545 self.patch(self.ui, 'update_sizes', self._set_called)
546 self.ui.on_folder_list_itemChanged("item", 0)
547 self.assertEqual(self._called, ((), {}))
548
549 def test_on_folder_list_item_changed_col_1(self):
550 """Test that it calls nothing."""
551 self.patch(self.ui, 'update_sizes', self._set_called)
552 self.ui.on_folder_list_itemChanged("item", 1)
553 self.assertFalse(self._called)
554
555 @defer.inlineCallbacks
556 def test_runtime_error(self):
557 """Test that runtime error is handled correctly."""
558
559 def raise_runtime(*args):
560 """Dummy function that raises RuntimeError."""
561 raise RuntimeError
562
563 yield self.ui.get_info()
564 item = yield self.ui.add_folder(
565 "dummy", validate=False, calculate=False)
566 self.patch(item, 'setText', raise_runtime)
567 self.ui.queue.put(["dummy", 1000])
568 self.assertIn("dummy", self.ui.items)
569 self.ui.update_sizes()
570 self.assertNotIn("dummy", self.ui.items)
571
572 @defer.inlineCallbacks
573 def test_queue_is_emptied(self):
574 """Test that update_sizes empties the queue."""
575 self.ui.account_info = {
576 "quota_total": 1000,
577 "quota_used": 200,
578 }
579 yield self.ui.add_folder("dummy1", validate=False, calculate=False)
580 yield self.ui.add_folder("dummy2", validate=False, calculate=False)
581 self.ui.queue.put(["dummy1", 1000])
582 self.ui.queue.put(["dummy2", 1000])
583 self.assertFalse(self.ui.queue.empty())
584 self.ui.update_sizes()
585 self.assertTrue(self.ui.queue.empty())

Subscribers

People subscribed via source and target branches