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
1=== modified file 'data/qt/local_folders.ui'
2--- data/qt/local_folders.ui 2011-08-08 21:48:11 +0000
3+++ data/qt/local_folders.ui 2011-08-23 13:43:25 +0000
4@@ -97,7 +97,7 @@
5 </spacer>
6 </item>
7 <item>
8- <widget class="AddFolderButton" name="add_folder_button">
9+ <widget class="QPushButton" name="add_folder_button">
10 <property name="text">
11 <string>Add a folder from this computer</string>
12 </property>
13@@ -188,13 +188,6 @@
14 </item>
15 </layout>
16 </widget>
17- <customwidgets>
18- <customwidget>
19- <class>AddFolderButton</class>
20- <extends>QPushButton</extends>
21- <header>ubuntuone.controlpanel.gui.qt.addfolder</header>
22- </customwidget>
23- </customwidgets>
24 <resources/>
25 <connections/>
26 </ui>
27
28=== modified file 'ubuntuone_installer/gui/qt/local_folders.py'
29--- ubuntuone_installer/gui/qt/local_folders.py 2011-08-15 21:41:54 +0000
30+++ ubuntuone_installer/gui/qt/local_folders.py 2011-08-23 13:43:25 +0000
31@@ -24,9 +24,14 @@
32 import threading
33 import Queue
34
35-from twisted.internet.defer import inlineCallbacks
36+from twisted.internet.defer import inlineCallbacks, returnValue
37 from PyQt4 import QtCore, QtGui
38-from ubuntuone.controlpanel.gui import humanize, sign_url
39+from ubuntuone.controlpanel import backend
40+from ubuntuone.controlpanel.gui import (
41+ humanize,
42+ sign_url,
43+ FOLDER_INVALID_PATH,
44+)
45 from ubuntuone.platform.credentials import CredentialsManagementTool
46 from ubuntu_sso.qt.gui import SSOWizardPage
47
48@@ -49,12 +54,19 @@
49
50 class FolderItem(QtGui.QTreeWidgetItem):
51 """Class representing a folder in the folder list UI."""
52- def __init__(self, strings, path=None, queue=None):
53+ def __init__(self, strings, path=None, queue=None,
54+ calculate=True, volume_id=None):
55 super(FolderItem, self).__init__(strings)
56 self.thread = CalculateSize(path, queue)
57- self.thread.start()
58- self.size = None
59+ # Don't calculate sizes of UDFs
60+ if calculate and not volume_id:
61+ self.thread.start()
62+ self.size = None
63+ else:
64+ self.size = 0
65 self.path = path
66+ self.setCheckState(0, QtCore.Qt.Unchecked)
67+ self.volume_id = volume_id
68
69
70 class CalculateSize(threading.Thread):
71@@ -83,22 +95,17 @@
72 local_folders_ui.Ui_Form(), None, parent)
73 self.setTitle(LOCAL_FOLDERS_TITLE)
74 self.setSubTitle()
75-
76 header_view = self.ui.folder_list.header()
77 header_view.setResizeMode(0, header_view.Stretch)
78-
79 self.queue = Queue.Queue()
80 self.timer = QtCore.QTimer()
81 self.items = {}
82- for folder_name in self.default_folders():
83- self.add_folder(folder_name)
84- self.update_sizes()
85- self.timer.start(2000)
86- self.timer.timeout.connect(self.update_sizes)
87+ self.folders_info = None
88+ self.account_info = None
89+ self.cp_backend = backend.ControlBackend()
90
91 # initializePage is inherited
92 # pylint: disable=C0103
93- @inlineCallbacks
94 def initializePage(self):
95 """UI details."""
96 self.wizard().setButtonLayout([
97@@ -107,15 +114,75 @@
98 self.wizard()._next_id = self.wizard().SYNC_NOW_OR_LATER_PAGE
99 # Start with this invisible
100 self.ui.offer_frame.setVisible(False)
101- if not self.ui.folder_list.topLevelItemCount():
102+ # Show overlay until we have server data
103+ self.wizard().overlay.show()
104+ self.get_info()
105+
106+ @inlineCallbacks
107+ def get_info(self):
108+ """Get information from CP backend and fill folder list."""
109+ # pylint: disable=W0702
110+ try:
111+ volumes_info = yield self.cp_backend.volumes_info()
112+ self.account_info = yield self.cp_backend.account_info()
113+ self.folders_info = []
114+ for _, _, volumes in volumes_info:
115+ for volume in volumes:
116+ if volume[u'type'] == u"UDF":
117+ self.folders_info.append(volume)
118+ self.ui.folder_list.clear()
119+ for folder in self.folders_info:
120+ item = yield self.add_folder(
121+ os.path.expanduser(folder['path']),
122+ validate=False,
123+ volume_id=folder['volume_id'],
124+ calculate=False,
125+ )
126+ if item:
127+ if folder['subscribed']:
128+ item.setCheckState(0, QtCore.Qt.Checked)
129+ item.size = 0
130 for folder_name in self.default_folders():
131- self.add_folder(folder_name)
132- yield self.update_sizes()
133+ item = yield self.add_folder(folder_name, validate=True)
134+ self.timer.start(2000)
135+ self.timer.timeout.connect(self.update_sizes)
136+ self.wizard().overlay.hide()
137+ self.wizard().currentIdChanged.connect(self.changed_page)
138+ except:
139+ logger.exception("Error getting backend info:")
140+
141+ @QtCore.pyqtSlot("int")
142+ @inlineCallbacks
143+ def changed_page(self, page_id):
144+ """When moving to next page, create/[un]subscribe UDFs."""
145+ self.timer.stop()
146+ try:
147+ self.wizard().currentIdChanged.disconnect(self.changed_page)
148+ except KeyError:
149+ pass
150+ if page_id == self.wizard().SYNC_NOW_OR_LATER_PAGE:
151+ # The page following this one
152+ self.wizard().overlay.show()
153+ for path, item in self.items.items():
154+ if item.checkState(0) == QtCore.Qt.Checked:
155+ if item.volume_id:
156+ yield self.cp_backend.change_volume_settings(
157+ item.volume_id,
158+ dict(subscribed=True))
159+ else:
160+ yield self.cp_backend.create_folder(path)
161+ else:
162+ if item.volume_id:
163+ yield self.cp_backend.change_volume_settings(
164+ item.volume_id,
165+ dict(subscribed=False))
166+ self.wizard().overlay.hide()
167
168 def default_folders(self):
169 """Return a list of the folders to add by default."""
170 if sys.platform == 'win32':
171- # Special Folder "My Documents"
172+ # XXXX to be replaced by calls to xdg_base_directory's
173+ # special_folders
174 dll = ctypes.windll.shell32
175 buf = ctypes.create_string_buffer(300)
176 dll.SHGetSpecialFolderPathA(None, buf, 5, False)
177@@ -129,56 +196,60 @@
178 result = ['To be implemented']
179 return result
180
181- def add_folder(self, path):
182+ @inlineCallbacks
183+ def add_folder(self, path, validate=True, calculate=True, volume_id=None):
184 """Add a folder to the list."""
185 if path in self.items:
186- return None
187- # FIXME: the path should actually be sent to u1cp to verify as valid
188- item = FolderItem([path, "", "remove"], path=path, queue=self.queue)
189- self.ui.folder_list.addTopLevelItem(item)
190- self.items[path] = item
191- return item
192+ returnValue(None)
193+ if validate:
194+ is_valid = yield self.cp_backend.validate_path_for_folder(path)
195+ else:
196+ is_valid = True
197+ if is_valid:
198+ item = FolderItem([path, ""],
199+ path=path,
200+ queue=self.queue,
201+ volume_id=volume_id,
202+ calculate=calculate)
203+ self.ui.folder_list.addTopLevelItem(item)
204+ self.items[path] = item
205+ returnValue(item)
206+ returnValue(None)
207
208- @inlineCallbacks
209 def update_sizes(self):
210 """Poll the queue were the threads put the size info."""
211- try:
212- path, size = self.queue.get(False)
213- item = self.items.get(path)
214- if item:
215- item.size = size
216- item.setText(1, humanize(size))
217- except Queue.Empty:
218- pass
219- total = 0
220+ while True:
221+ try:
222+ path, size = self.queue.get(False)
223+ except Queue.Empty:
224+ break
225+ else:
226+ item = self.items.get(path)
227+ if item:
228+ item.size = size
229+ try:
230+ item.setText(1, humanize(size))
231+ except RuntimeError:
232+ del self.items[path]
233+ total = long(self.account_info['quota_used'])
234 for path, item in self.items.items():
235 if item.size is None:
236 total = LOCAL_FOLDERS_CALCULATING
237 break
238- total += item.size
239-
240+ if not item.volume_id and item.checkState(0) == QtCore.Qt.Checked:
241+ # Existing UDFs are already accounted for, count if marked.
242+ total += item.size
243 if isinstance(total, long):
244- yield self.show_hide_offer(total)
245+ self.show_hide_offer(total)
246 total = humanize(total)
247 else:
248- yield self.show_hide_offer(0)
249+ self.show_hide_offer(0)
250 self.ui.folder_list.headerItem().setText(
251 1, LOCAL_FOLDERS_SPACE_HEADER % total)
252
253- @inlineCallbacks
254 def show_hide_offer(self, cur_size):
255- """Show or hide the offer to buy space according to the total size.
256-
257- Returns a deferred that is triggered when the update is finished.
258-
259- """
260- # pylint: disable=W0702
261- try:
262- user_info = yield self.ui.add_folder_button.backend.account_info()
263- except:
264- logger.exception('Error while trying to get account info:')
265-
266- quota = user_info['quota_total']
267+ """Show or hide the offer to buy space according to the total size."""
268+ quota = self.account_info['quota_total']
269 if cur_size > quota:
270 self.ui.offer_frame.setVisible(True)
271 else:
272@@ -193,31 +264,44 @@
273
274 # itemClicked is a Qt signal name.
275 # pylint: disable=C0103
276- def on_folder_list_itemClicked(self, item, column):
277+ def on_folder_list_itemChanged(self, item, column):
278 """Delete folder from the list."""
279- if column == 2:
280- del(self.items[item.path])
281- item.thread._stop = True
282- self.ui.folder_list.takeTopLevelItem(
283- self.ui.folder_list.indexOfTopLevelItem(item))
284- self.update_sizes()
285+ if column == 0:
286+ self.update_sizes()
287
288 @inlineCallbacks
289 @QtCore.pyqtSlot()
290 def on_add_storage_button_clicked(self):
291 """user clicked on the "Add more storage" button."""
292-
293+ # Really want to catch everything
294 # pylint: disable=W0702
295+ url = "https://one.ubuntu.com/services/#storage_panel"
296 try:
297 credtool = CredentialsManagementTool()
298 creds = yield credtool.find_credentials()
299 except:
300- logger.exception('Error while trying to update que quota:')
301+ logger.exception('Error while trying to get credentials:')
302 creds = {}
303-
304 if creds:
305- signed_url = sign_url(
306- "https://one.ubuntu.com/services/#storage_panel", creds)
307+ signed_url = sign_url(url, creds)
308 else:
309- signed_url = "https://one.ubuntu.com/services/#storage_panel"
310+ signed_url = url
311 QtGui.QDesktopServices.openUrl(QtCore.QUrl(signed_url))
312+
313+ @inlineCallbacks
314+ @QtCore.pyqtSlot()
315+ def on_add_folder_button_clicked(self):
316+ """user clicked on the "Add Folder" button."""
317+ folder = QtGui.QFileDialog.getExistingDirectory(parent=self)
318+ folder = unicode(folder)
319+ if folder == '':
320+ return
321+
322+ is_valid = yield self.cp_backend.validate_path_for_folder(folder)
323+ if not is_valid:
324+ user_home = os.path.expanduser('~')
325+ text = FOLDER_INVALID_PATH % {'folder_path': folder,
326+ 'home_folder': user_home}
327+ QtGui.QMessageBox.warning(self, '', text, QtGui.QMessageBox.Close)
328+ return
329+ yield self.add_folder(folder, validate=False, volume_id=False)
330
331=== modified file 'ubuntuone_installer/gui/qt/tests/test_gui.py'
332--- ubuntuone_installer/gui/qt/tests/test_gui.py 2011-08-22 14:05:02 +0000
333+++ ubuntuone_installer/gui/qt/tests/test_gui.py 2011-08-23 13:43:25 +0000
334@@ -20,9 +20,6 @@
335 """Tests for the Qt UI."""
336
337 import os
338-import Queue
339-import shutil
340-import tempfile
341
342 from twisted.internet import defer
343 from PyQt4 import QtGui, QtCore
344@@ -32,7 +29,6 @@
345 from ubuntuone_installer.gui.qt import gui
346 from ubuntuone_installer.gui.qt.tests import BaseTestCase
347 from ubuntuone_installer.gui.qt import forgotten
348-from ubuntuone_installer.gui.qt import local_folders
349 from ubuntuone_installer.gui.qt import setup_account
350 from ubuntuone_installer.gui.qt.ui import (
351 setup_account_ui,
352@@ -217,6 +213,7 @@
353 self.ui.restart()
354 self.ui.show()
355
356+ # pylint: disable=W0212
357 preferences_page._button_clicked(QtGui.QWizard.CustomButton2)
358 self.assertEqual(
359 preferences_page.preferences_widget.restore_button_clicked, True)
360@@ -245,6 +242,7 @@
361 self.assertEqual(tree_folders.isHeaderHidden(), True)
362 tree_folders = folders_page.folders_widget.ui.folders
363 self.assertEqual(tree_folders.isColumnHidden(2), True)
364+ # pylint: disable=W0212
365 self.assertEqual(self.ui._next_id, self.ui.CONGRATULATIONS_PAGE)
366
367 def test_folders_page_next_id(self):
368@@ -571,120 +569,19 @@
369 self.shown = True
370
371
372-class FakeCPBackend(object):
373- """Fake Control Panel backend."""
374-
375- def account_info(self, *args):
376- """Fake account info."""
377- return defer.succeed({"quota_total": 1000})
378-
379-
380-class FakeAddFolderButton(object):
381- """Fake Control Panel "Add Folder" button."""
382- backend = FakeCPBackend()
383-
384-
385-class LocalFoldersTestCase(BaseTestCase):
386- """Test the LocalFoldersPage code."""
387-
388- class_ui = local_folders.LocalFoldersPage
389-
390- def setUp(self):
391- """Initialize this test instance."""
392- # Create a test folder with a known size
393- self.tmpdir = tempfile.mkdtemp()
394- f = open(os.path.join(self.tmpdir, 'test_file'), 'wb')
395- f.write(" " * 600)
396- f.close()
397- os.mkdir(os.path.join(self.tmpdir, 'test_dir'))
398- f = open(os.path.join(self.tmpdir, 'test_dir', 'test_file_2'), 'wb')
399- f.write(" " * 737)
400- f.close()
401- super(LocalFoldersTestCase, self).setUp()
402-
403- def tearDown(self):
404- """Remove the temporary tree."""
405- shutil.rmtree(self.tmpdir)
406- BaseTestCase.tearDown(self)
407-
408- def test_size_calculation(self):
409- """Test the recursive folder size calculation."""
410- queue = Queue.Queue()
411- csize = local_folders.CalculateSize(self.tmpdir, queue)
412- csize.run()
413- path, size = queue.get()
414- self.assertEqual(path, self.tmpdir)
415- self.assertEqual(size, 1337)
416-
417- def test_item_addition(self):
418- """Add an item (plus the default one), then remove them."""
419- self.ui.add_folder(self.tmpdir)
420- self.assertEqual(4, self.ui.ui.folder_list.topLevelItemCount())
421-
422- def test_total_size(self):
423- """Test that the header reflects the change in item sizes."""
424- while self.ui.ui.folder_list.topLevelItemCount():
425- self.ui.on_folder_list_itemClicked(
426- self.ui.ui.folder_list.topLevelItem(0), 2)
427- self.assertEqual(0, self.ui.ui.folder_list.topLevelItemCount())
428- item = self.ui.add_folder(self.tmpdir)
429- item.size = 1337
430- item.thread.run()
431- item.thread.join()
432- self.ui.update_sizes()
433- self.assertEqual(unicode(self.ui.ui.folder_list.headerItem().text(1)),
434- u"Space (1337)")
435-
436- def test_add_twice(self):
437- """Behaviour for adding the same folder twice.
438-
439- * It's added only once.
440- """
441- while self.ui.ui.folder_list.topLevelItemCount():
442- self.ui.on_folder_list_itemClicked(
443- self.ui.ui.folder_list.topLevelItem(0), 2)
444- self.assertEqual(0, self.ui.ui.folder_list.topLevelItemCount())
445- self.ui.add_folder(self.tmpdir)
446- self.ui.add_folder(self.tmpdir)
447- self.assertEqual(1, self.ui.ui.folder_list.topLevelItemCount())
448-
449- def test_add_missing_folder(self):
450- """Behaviour for adding a folder that doesn't exist.
451-
452- * It's added.
453- * Has size 0.
454- """
455-
456- while self.ui.ui.folder_list.topLevelItemCount():
457- self.ui.on_folder_list_itemClicked(
458- self.ui.ui.folder_list.topLevelItem(0), 2)
459- self.assertEqual(0, self.ui.ui.folder_list.topLevelItemCount())
460- item = self.ui.add_folder(os.path.join("xyzzy", "xyzzy", "xyzzy"))
461- self.assertEqual(1, self.ui.ui.folder_list.topLevelItemCount())
462- item.thread.run()
463- item.thread.join()
464- self.ui.update_sizes()
465- self.assertEqual(0, item.size)
466- # world did not explode
467-
468- def test_over_quota(self):
469- """After removing all folders, offer_frame should be hidden.
470-
471- Push the user over quota, it should be visible.
472- """
473- self.patch(self.ui.ui.offer_frame, "setVisible", self._set_called)
474- while self.ui.ui.folder_list.topLevelItemCount():
475- self.ui.on_folder_list_itemClicked(
476- self.ui.ui.folder_list.topLevelItem(0), 2)
477- self.assertEqual(0, self.ui.ui.folder_list.topLevelItemCount())
478- self.ui.update_sizes()
479- self.assertEqual(self._called, ((False,), {}))
480- self.ui.show_hide_offer(self.ui.quota() + 1)
481- self.assertEqual(self._called, ((True,), {}))
482-
483-
484-LocalFoldersTestCase.skip = 'We need to re-write this test case entirely,' \
485- 'see bug #824675 for reference.'
486+class FakeWizard(object):
487+ """Replace wizard() function on wizard pages."""
488+
489+ params = None
490+
491+ # Invalid name "setButtonLayout"
492+ # pylint: disable=C0103
493+
494+ def setButtonLayout(self, *args, **kwargs):
495+ """Fake the functionality of setButtonLayout on QWizard class."""
496+ FakeWizard.params = (args, kwargs)
497+
498+ # pylint: enable=C0103
499
500
501 class FakeWizard(object):
502
503=== added file 'ubuntuone_installer/gui/qt/tests/test_local_folders.py'
504--- ubuntuone_installer/gui/qt/tests/test_local_folders.py 1970-01-01 00:00:00 +0000
505+++ ubuntuone_installer/gui/qt/tests/test_local_folders.py 2011-08-23 13:43:25 +0000
506@@ -0,0 +1,585 @@
507+# -*- coding: utf-8 -*-
508+
509+# Authors: Roberto Alsina <roberto.alsina@canonical.com>
510+#
511+# Copyright 2011 Canonical Ltd.
512+#
513+# This program is free software: you can redistribute it and/or modify it
514+# under the terms of the GNU General Public License version 3, as published
515+# by the Free Software Foundation.
516+#
517+# This program is distributed in the hope that it will be useful, but
518+# WITHOUT ANY WARRANTY; without even the implied warranties of
519+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
520+# PURPOSE. See the GNU General Public License for more details.
521+#
522+# You should have received a copy of the GNU General Public License along
523+# with this program. If not, see <http://www.gnu.org/licenses/>.
524+
525+"""Tests for the local folders page."""
526+
527+import os
528+import Queue
529+import shutil
530+import tempfile
531+
532+from twisted.internet import defer
533+from PyQt4 import QtGui, QtCore
534+
535+from ubuntuone_installer.gui.qt import local_folders
536+from ubuntuone_installer.gui.qt.tests import BaseTestCase, TestCase
537+from ubuntuone_installer.gui.qt.tests.test_gui import FakeOverlay
538+
539+
540+class FakeFileDialog(object):
541+
542+ """A fake QFileDialog class."""
543+
544+ # pylint: disable=C0103
545+ def getExistingDirectory(self, *args, **kwargs):
546+ """Fake existing folder name."""
547+ return u"whatever"
548+
549+
550+class FakeMessageBox(object):
551+
552+ """A fake QMessageBox class."""
553+
554+ Close = 2
555+ _warning = None
556+
557+ def warning(self, *args, **kwargs):
558+ """Fake warning."""
559+ self._warning = (args, kwargs)
560+
561+
562+class FakeCPBackend(object):
563+ """Fake Control Panel backend."""
564+
565+ def __init__(self):
566+ """Initialize."""
567+ self._is_valid = True
568+ self.volume_setings_changes = []
569+ self.folders_created = []
570+
571+ def account_info(self, *args):
572+ """Fake account info."""
573+ return defer.succeed({
574+ "quota_total": 1000,
575+ "quota_used": 200,
576+ })
577+
578+ def volumes_info(self, *args):
579+ """Fake volumes info."""
580+ return defer.succeed(((None, None,
581+ [
582+ {
583+ 'type': u'UDF',
584+ 'path': os.path.expanduser(u'~/xyzzy'),
585+ 'volume_id': 'asdfgh',
586+ 'subscribed': True,
587+ },
588+ {
589+ 'type': u'UDF',
590+ 'path': os.path.expanduser(u'~/zxyzzy'),
591+ 'volume_id': 'qwerty',
592+ 'subscribed': False,
593+ },
594+ {
595+ 'type': u'SHARE',
596+ 'path': os.path.expanduser(u'~/foobar'),
597+ 'volume_id': 'shared',
598+ 'subscribed': False,
599+ },
600+ ],
601+ ),))
602+
603+ def validate_path_for_folder(self, path):
604+ """Fake folder validation."""
605+ return self._is_valid
606+
607+ def change_volume_settings(self, *args):
608+ """Fake change volume settings."""
609+ self.volume_setings_changes.append(args)
610+
611+ def create_folder(self, *args):
612+ """Fake folder creation."""
613+ self.folders_created.append(args)
614+
615+
616+class FakeFailingCPBackend(object):
617+ """Fake Control Panel backend that fails."""
618+
619+ def account_info(self, *args):
620+ """Fake account info."""
621+ return defer.fail(Exception())
622+
623+ def volumes_info(self, *args):
624+ """Fake account info."""
625+ return defer.fail(Exception())
626+
627+
628+class FakeSignal(object):
629+
630+ """A fake PyQt signal."""
631+
632+ def __init__(self, *args, **kwargs):
633+ """Initialize."""
634+ self.target = None
635+
636+ def connect(self, target):
637+ """Fake connect."""
638+ self.target = target
639+
640+ def disconnect(self, *args):
641+ """Fake disconnect."""
642+ self.target = None
643+
644+ def emit(self, *args):
645+ """Fake emit."""
646+ if self.target:
647+ self.target(*args)
648+
649+
650+class FakeMainWindow(object):
651+
652+ """A fake MainWindow."""
653+
654+ loginSuccess = FakeSignal()
655+ registrationSuccess = FakeSignal()
656+ userCancellation = FakeSignal()
657+ shown = False
658+ _buttonlayout = None
659+ SYNC_NOW_OR_LATER_PAGE = 4
660+ overlay = FakeOverlay()
661+ currentIdChanged = FakeSignal()
662+
663+ def show(self):
664+ """Fake method."""
665+ self.shown = True
666+
667+ # pylint: disable=C0103
668+ def setButtonLayout(self, *args):
669+ """Save ButtonLayout."""
670+ self._buttonlayout = args
671+
672+
673+class FakeCalculateSize(object):
674+
675+ """A fake CalculateSize."""
676+
677+ def __init__(self, *args, **kwargs):
678+ self.started = False
679+
680+ def start(self):
681+ """Fake start."""
682+ self.started = True
683+
684+
685+class FolderItemTestCase(TestCase):
686+
687+ """Tests for the FolderItem class."""
688+
689+ def test_folder_item_new_udf(self):
690+ """Test when created like a new UDF."""
691+ self.patch(local_folders, "CalculateSize", FakeCalculateSize)
692+ item = local_folders.FolderItem(["s1", "s2"], "path")
693+ self.assertTrue(item.size is None)
694+ self.assertEqual(item.path, "path")
695+ # pylint: disable=E1101
696+ self.assertTrue(item.thread.started)
697+ self.assertEqual(item.checkState(0), QtCore.Qt.Unchecked)
698+ self.assertTrue(item.volume_id is None)
699+
700+ def test_folder_item_existing_udf(self):
701+ """Test when created like an existing UDF."""
702+ self.patch(local_folders, "CalculateSize", FakeCalculateSize)
703+ item = local_folders.FolderItem(["s1", "s2"], "path",
704+ calculate=False, volume_id="xyzzy")
705+ self.assertEqual(item.size, 0)
706+ self.assertEqual(item.path, "path")
707+ # pylint: disable=E1101
708+ self.assertFalse(item.thread.started)
709+ self.assertEqual(item.checkState(0), QtCore.Qt.Unchecked)
710+ self.assertEqual(item.volume_id, "xyzzy")
711+
712+
713+class LocalFoldersTestCase(BaseTestCase):
714+ """Test the LocalFoldersPage code."""
715+
716+ class_ui = local_folders.LocalFoldersPage
717+
718+ def setUp(self):
719+ """Initialize this test instance."""
720+ # Create a test folder with a known size
721+ self.tmpdir = tempfile.mkdtemp()
722+ f = open(os.path.join(self.tmpdir, 'test_file'), 'wb')
723+ f.write(" " * 600)
724+ f.close()
725+ os.mkdir(os.path.join(self.tmpdir, 'test_dir'))
726+ f = open(os.path.join(self.tmpdir, 'test_dir', 'test_file_2'), 'wb')
727+ f.write(" " * 737)
728+ f.close()
729+ self.addCleanup(shutil.rmtree, self.tmpdir)
730+ self.fake_wizard = FakeMainWindow()
731+ self.patch(local_folders.backend, "ControlBackend", FakeCPBackend)
732+ super(LocalFoldersTestCase, self).setUp()
733+ self.patch(self.ui, "wizard", lambda: self.fake_wizard)
734+
735+ def test_status_before_initialize(self):
736+ """Test status of the page components before initializePage()."""
737+ self.assertIsInstance(self.ui.timer, QtCore.QTimer)
738+ self.assertFalse(self.ui.timer.isActive())
739+ self.assertTrue(self.ui.folders_info is None)
740+ self.assertTrue(self.ui.account_info is None)
741+ self.assertEqual(self.ui.items, {})
742+ self.assertEqual(self.ui.ui.folder_list.topLevelItemCount(), 0)
743+
744+ def test_status_after_initialize(self):
745+ """Test status of page components after initializePage()."""
746+ self.patch(self.ui, "get_info", self._set_called)
747+ self.ui.initializePage()
748+ self.assertTrue(self.ui.wizard().overlay.show_counter, 1)
749+ self.assertTrue(self.ui.wizard().overlay.hide_counter, 0)
750+ self.assertFalse(self.ui.ui.offer_frame.isVisible())
751+ self.assertEqual(self._called, ((), {}))
752+
753+ @defer.inlineCallbacks
754+ def test_status_after_get_info(self):
755+ """Test status of page components after get_info()."""
756+ yield self.ui.get_info()
757+ self.assertTrue(self.ui.wizard().overlay.hide_counter, 1)
758+ self.assertEqual(self.ui.ui.folder_list.topLevelItemCount(), 5)
759+ self.assertEqual(self.ui.wizard().currentIdChanged.target,
760+ self.ui.changed_page)
761+ self.assertEqual(
762+ self.ui.items[os.path.expanduser('~/xyzzy')].checkState(0),
763+ QtCore.Qt.Checked)
764+ self.assertEqual(
765+ self.ui.items[os.path.expanduser('~/zxyzzy')].checkState(0),
766+ QtCore.Qt.Unchecked)
767+ check_states = [self.ui.items[item].checkState(0)
768+ for item in self.ui.items]
769+ self.assertEqual(len([
770+ x for x in check_states if x != QtCore.Qt.Unchecked]), 1)
771+ self.assertNotIn('shared', self.ui.items)
772+
773+ @defer.inlineCallbacks
774+ def test_subscribed_udf_checked(self):
775+ """Check that subscribed UDF items are created correctly."""
776+ yield self.ui.get_info()
777+ item = self.ui.items[os.path.expanduser(u'~/xyzzy')]
778+ self.assertEqual(item.checkState(0), QtCore.Qt.Checked)
779+ self.assertEqual(item.size, 0)
780+ self.assertEqual(item.volume_id, 'asdfgh')
781+
782+ @defer.inlineCallbacks
783+ def test_unsubscribed_udf_checked(self):
784+ """Check that unsubscribed UDF items are created correctly."""
785+ yield self.ui.get_info()
786+ item = self.ui.items[os.path.expanduser(u'~/zxyzzy')]
787+ self.assertEqual(item.checkState(0), QtCore.Qt.Unchecked)
788+ self.assertEqual(item.size, 0)
789+ self.assertEqual(item.volume_id, 'qwerty')
790+
791+ def test_size_calculation(self):
792+ """Test the recursive folder size calculation."""
793+ queue = Queue.Queue()
794+ csize = local_folders.CalculateSize(self.tmpdir, queue)
795+ csize.run()
796+ path, size = queue.get()
797+ self.assertEqual(path, self.tmpdir)
798+ self.assertEqual(size, 1337)
799+
800+ @defer.inlineCallbacks
801+ def test_item_addition(self):
802+ """Add a folder."""
803+ self.ui.ui.folder_list.clear()
804+ yield self.ui.add_folder(self.tmpdir)
805+ self.assertEqual(1, self.ui.ui.folder_list.topLevelItemCount())
806+
807+ @defer.inlineCallbacks
808+ def test_invalid_item_addition(self):
809+ """Try to add an invalid folder."""
810+ self.ui.ui.folder_list.clear()
811+ self.ui.cp_backend._is_valid = False
812+ yield self.ui.add_folder(self.tmpdir)
813+ self.assertEqual(0, self.ui.ui.folder_list.topLevelItemCount())
814+
815+ @defer.inlineCallbacks
816+ def test_not_validating_invalid_item_addition(self):
817+ """Try to add an invalid folder, and not validate it."""
818+ self.ui.ui.folder_list.clear()
819+ self.ui.cp_backend._is_valid = False
820+ yield self.ui.add_folder(self.tmpdir, validate=False)
821+ self.assertEqual(1, self.ui.ui.folder_list.topLevelItemCount())
822+
823+ @defer.inlineCallbacks
824+ def test_not_calculating_item_addition(self):
825+ """Try to add a folder, and not calculate it."""
826+ self.ui.ui.folder_list.clear()
827+ yield self.ui.add_folder(self.tmpdir, calculate=False)
828+ self.assertEqual(self.ui.items[self.tmpdir].size, 0)
829+ self.assertEqual(1, self.ui.ui.folder_list.topLevelItemCount())
830+
831+ @defer.inlineCallbacks
832+ def test_volume_id_item_addition(self):
833+ """Try to add a folder with a volume_id."""
834+ self.ui.ui.folder_list.clear()
835+ yield self.ui.add_folder(self.tmpdir, volume_id="vol")
836+ self.assertEqual(self.ui.items[self.tmpdir].volume_id, "vol")
837+ self.assertEqual(1, self.ui.ui.folder_list.topLevelItemCount())
838+
839+ @defer.inlineCallbacks
840+ def test_total_size(self):
841+ """Test that the header reflects the change in item sizes."""
842+ self.patch(self.ui, "show_hide_offer", self._set_called)
843+ yield self.ui.get_info()
844+ self.ui.ui.folder_list.clear()
845+ self.ui.items = {}
846+ item = yield self.ui.add_folder(self.tmpdir)
847+ item.thread.run()
848+ item.thread.join()
849+ item.size = 1337
850+ item.setCheckState(0, QtCore.Qt.Checked)
851+ self.ui.update_sizes()
852+ self.assertEqual(unicode(self.ui.ui.folder_list.headerItem().text(1)),
853+ u"Space (1.5 KiB)")
854+ self.assertEqual(self._called, ((1537L,), {}))
855+
856+ @defer.inlineCallbacks
857+ def test_total_size_unchecked(self):
858+ """Unchecked items use no space beyond quota_used."""
859+ self.patch(self.ui, "show_hide_offer", self._set_called)
860+ yield self.ui.get_info()
861+ self.ui.ui.folder_list.clear()
862+ self.ui.items = {}
863+ item = yield self.ui.add_folder(self.tmpdir)
864+ item.thread.run()
865+ item.thread.join()
866+ item.size = 1337
867+ item.setCheckState(0, QtCore.Qt.Unchecked)
868+ self.ui.update_sizes()
869+ self.assertEqual(unicode(self.ui.ui.folder_list.headerItem().text(1)),
870+ u"Space (200 bytes)")
871+ self.assertEqual(self._called, ((200L,), {}))
872+
873+ @defer.inlineCallbacks
874+ def test_total_size_unchecked_no_udfs(self):
875+ """No checked items and no quota used should amount to 0 bytes."""
876+ self.patch(self.ui, "show_hide_offer", self._set_called)
877+ yield self.ui.get_info()
878+ self.ui.account_info['quota_used'] = 0
879+ self.ui.ui.folder_list.clear()
880+ self.ui.items = {}
881+ item = yield self.ui.add_folder(self.tmpdir)
882+ item.thread.run()
883+ item.thread.join()
884+ item.size = 1337
885+ item.setCheckState(0, QtCore.Qt.Unchecked)
886+ self.ui.update_sizes()
887+ self.assertEqual(unicode(self.ui.ui.folder_list.headerItem().text(1)),
888+ u"Space (0 bytes)")
889+ self.assertEqual(self._called, ((0,), {}))
890+
891+ @defer.inlineCallbacks
892+ def test_total_size_udf(self):
893+ """UDFs use no space beyond quota_used."""
894+ self.patch(self.ui, "show_hide_offer", self._set_called)
895+ yield self.ui.get_info()
896+ for _, item in self.ui.items.items():
897+ if item.thread.is_alive():
898+ item.thread.join()
899+ if item.volume_id:
900+ item.setCheckState(0, QtCore.Qt.Checked)
901+ else:
902+ item.setCheckState(0, QtCore.Qt.Unchecked)
903+ self.ui.update_sizes()
904+ self.assertEqual(unicode(self.ui.ui.folder_list.headerItem().text(1)),
905+ u"Space (200 bytes)")
906+ self.assertEqual(self._called, ((200L,), {}))
907+
908+ def test_add_twice(self):
909+ """Behaviour for adding the same folder twice:
910+
911+ * It's added only once.
912+ """
913+ self.ui.ui.folder_list.clear()
914+ self.assertEqual(0, self.ui.ui.folder_list.topLevelItemCount())
915+ self.ui.add_folder(self.tmpdir)
916+ self.ui.add_folder(self.tmpdir)
917+ self.assertEqual(1, self.ui.ui.folder_list.topLevelItemCount())
918+
919+ @defer.inlineCallbacks
920+ def test_add_missing_folder(self):
921+ """Behaviour for adding a folder that doesn't exist:
922+
923+ * It's added.
924+ * Has size 0.
925+ """
926+
927+ yield self.ui.get_info()
928+ self.ui.ui.folder_list.clear()
929+ self.assertEqual(0, self.ui.ui.folder_list.topLevelItemCount())
930+ item = yield self.ui.add_folder(os.path.join(
931+ "xyzzy", "xyzzy", "xyzzy"))
932+ self.assertEqual(1, self.ui.ui.folder_list.topLevelItemCount())
933+ item.thread.run()
934+ item.thread.join()
935+ self.ui.update_sizes()
936+ self.assertEqual(0, item.size)
937+ # world did not explode
938+
939+ def test_over_quota(self):
940+ """After removing all folders, offer_frame should be hidden.
941+
942+ Push the user over quota, it should be visible.
943+ """
944+ self.patch(self.ui.ui.offer_frame, "setVisible", self._set_called)
945+ yield self.ui.get_info()
946+ self.ui.ui.folder_list.clear()
947+ self.ui.update_sizes()
948+ self.assertEqual(self._called, ((False,), {}))
949+ self.ui.show_hide_offer(self.ui.quota() + 1)
950+ self.assertEqual(self._called, ((True,), {}))
951+
952+ def test_add_folder_clicked_valid(self):
953+ """Test behaviour when adding a valid folder via the button."""
954+ self.patch(QtGui, "QFileDialog", FakeFileDialog())
955+ self.patch(self.ui, "add_folder", self._set_called)
956+ self.ui.ui.add_folder_button.click()
957+ self.assertEqual(self._called,
958+ ((u'whatever',), {'validate': False, 'volume_id': False}))
959+
960+ def test_add_folder_clicked_invalid(self):
961+ """Test behaviour when adding an invalid folder via the button."""
962+ self.patch(QtGui, "QFileDialog", FakeFileDialog())
963+ message_box = FakeMessageBox()
964+ self.patch(QtGui, "QMessageBox", message_box)
965+ self.patch(self.ui, "add_folder", self._set_called)
966+ self.ui.cp_backend._is_valid = False
967+ self.ui.ui.add_folder_button.click()
968+ self.assertEqual(self._called, False)
969+ user_home = os.path.expanduser('~')
970+ text = local_folders.FOLDER_INVALID_PATH % {
971+ 'folder_path': "whatever",
972+ 'home_folder': user_home,
973+ }
974+ # pylint: disable=W0212
975+ self.assertEqual(message_box._warning,
976+ ((self.ui,
977+ '', text, 2), {}))
978+
979+ @defer.inlineCallbacks
980+ def test_changed_page_existing_udf_behaviour(self):
981+ """If a UDF is checked, subscribe it, if not, unsubscribe it."""
982+ yield self.ui.get_info()
983+ show_counter = self.ui.wizard().overlay.show_counter
984+ hide_counter = self.ui.wizard().overlay.hide_counter
985+ self.ui.changed_page(self.ui.wizard().SYNC_NOW_OR_LATER_PAGE)
986+ self.assertEqual(self.ui.cp_backend.volume_setings_changes,
987+ [('asdfgh', {'subscribed': True}),
988+ ('qwerty', {'subscribed': False})])
989+ self.assertEqual(self.ui.wizard().overlay.show_counter,
990+ show_counter + 1)
991+ self.assertEqual(self.ui.wizard().overlay.hide_counter,
992+ hide_counter + 1)
993+
994+ @defer.inlineCallbacks
995+ def test_changed_page_new_udf_behaviour(self):
996+ """Create UDFs for non-existing, checked UDFs."""
997+ yield self.ui.get_info()
998+ show_counter = self.ui.wizard().overlay.show_counter
999+ hide_counter = self.ui.wizard().overlay.hide_counter
1000+ self.ui.ui.folder_list.clear()
1001+ self.ui.items = {}
1002+ item = yield self.ui.add_folder("whatever")
1003+ item.setCheckState(0, QtCore.Qt.Checked)
1004+ item = yield self.ui.add_folder("whatever2")
1005+ item.setCheckState(0, QtCore.Qt.Unchecked)
1006+ self.ui.changed_page(self.ui.wizard().SYNC_NOW_OR_LATER_PAGE)
1007+ self.assertEqual(self.ui.cp_backend.folders_created,
1008+ [('whatever',)])
1009+ self.assertEqual(self.ui.wizard().overlay.show_counter,
1010+ show_counter + 1)
1011+ self.assertEqual(self.ui.wizard().overlay.hide_counter,
1012+ hide_counter + 1)
1013+
1014+ def test_exception_on_account_info(self):
1015+ """When account_info fails, nothing should happen."""
1016+ self.patch(self.ui,
1017+ "cp_backend", FakeFailingCPBackend())
1018+ self.ui.get_info()
1019+ # Still here
1020+
1021+ def test_timer_is_started(self):
1022+ """When displaying the page, the timer should start."""
1023+ self.ui.initializePage()
1024+ self.assertTrue(self.ui.timer.isActive())
1025+
1026+ def test_timer_is_stopped(self):
1027+ """When leaving the page, the timer should stop."""
1028+ self.ui.changed_page(-1)
1029+ self.assertFalse(self.ui.timer.isActive())
1030+
1031+ def test_special_folders(self):
1032+ """Test that default_folders does the right thing."""
1033+ folders = self.ui.default_folders()
1034+ self.assertEqual(len(folders), 3)
1035+ self.assertEqual(folders, [os.path.abspath(f) for f in folders])
1036+ # pylint: disable=W0404
1037+ import ctypes
1038+ dll = ctypes.windll.shell32
1039+ buf = ctypes.create_string_buffer(300)
1040+ dll.SHGetSpecialFolderPathA(None, buf, 5, False)
1041+ docs = buf.value
1042+ dll.SHGetSpecialFolderPathA(None, buf, 13, False)
1043+ music = buf.value
1044+ dll.SHGetSpecialFolderPathA(None, buf, 39, False)
1045+ pictures = buf.value
1046+ expected = [docs, music, pictures]
1047+ self.assertEqual(sorted(folders), sorted(expected))
1048+
1049+ def test_on_folder_list_item_changed_col_0(self):
1050+ """Test that it calls the right thing."""
1051+ self.patch(self.ui, 'update_sizes', self._set_called)
1052+ self.ui.on_folder_list_itemChanged("item", 0)
1053+ self.assertEqual(self._called, ((), {}))
1054+
1055+ def test_on_folder_list_item_changed_col_1(self):
1056+ """Test that it calls nothing."""
1057+ self.patch(self.ui, 'update_sizes', self._set_called)
1058+ self.ui.on_folder_list_itemChanged("item", 1)
1059+ self.assertFalse(self._called)
1060+
1061+ @defer.inlineCallbacks
1062+ def test_runtime_error(self):
1063+ """Test that runtime error is handled correctly."""
1064+
1065+ def raise_runtime(*args):
1066+ """Dummy function that raises RuntimeError."""
1067+ raise RuntimeError
1068+
1069+ yield self.ui.get_info()
1070+ item = yield self.ui.add_folder(
1071+ "dummy", validate=False, calculate=False)
1072+ self.patch(item, 'setText', raise_runtime)
1073+ self.ui.queue.put(["dummy", 1000])
1074+ self.assertIn("dummy", self.ui.items)
1075+ self.ui.update_sizes()
1076+ self.assertNotIn("dummy", self.ui.items)
1077+
1078+ @defer.inlineCallbacks
1079+ def test_queue_is_emptied(self):
1080+ """Test that update_sizes empties the queue."""
1081+ self.ui.account_info = {
1082+ "quota_total": 1000,
1083+ "quota_used": 200,
1084+ }
1085+ yield self.ui.add_folder("dummy1", validate=False, calculate=False)
1086+ yield self.ui.add_folder("dummy2", validate=False, calculate=False)
1087+ self.ui.queue.put(["dummy1", 1000])
1088+ self.ui.queue.put(["dummy2", 1000])
1089+ self.assertFalse(self.ui.queue.empty())
1090+ self.ui.update_sizes()
1091+ self.assertTrue(self.ui.queue.empty())

Subscribers

People subscribed via source and target branches