Merge lp:~mandel/ubuntuone-control-panel/auto-update-looping-call into lp:ubuntuone-control-panel

Proposed by Manuel de la Peña on 2011-11-02
Status: Rejected
Rejected by: Manuel de la Peña on 2012-04-20
Proposed branch: lp:~mandel/ubuntuone-control-panel/auto-update-looping-call
Merge into: lp:ubuntuone-control-panel
Prerequisite: lp:~mandel/ubuntuone-control-panel/auto-update-functions
Diff against target: 834 lines (+488/-115)
15 files modified
setup.py (+1/-0)
ubuntuone/controlpanel/gui/__init__.py (+2/-0)
ubuntuone/controlpanel/gui/qt/gui.py (+13/-0)
ubuntuone/controlpanel/gui/qt/main/__init__.py (+1/-1)
ubuntuone/controlpanel/gui/qt/systray.py (+17/-0)
ubuntuone/controlpanel/gui/qt/task/__init__.py (+30/-0)
ubuntuone/controlpanel/gui/qt/task/linux.py (+54/-0)
ubuntuone/controlpanel/gui/qt/task/tests/__init__.py (+17/-0)
ubuntuone/controlpanel/gui/qt/task/tests/test_linux.py (+82/-0)
ubuntuone/controlpanel/gui/qt/tests/test_gui.py (+132/-0)
ubuntuone/controlpanel/gui/qt/tests/test_start.py (+0/-108)
ubuntuone/controlpanel/gui/qt/tests/test_systray.py (+77/-5)
ubuntuone/controlpanel/utils/__init__.py (+4/-0)
ubuntuone/controlpanel/utils/tests/test_windows.py (+46/-1)
ubuntuone/controlpanel/utils/windows.py (+12/-0)
To merge this branch: bzr merge lp:~mandel/ubuntuone-control-panel/auto-update-looping-call
Reviewer Review Type Date Requested Status
Natalia Bidart 2011-11-02 Needs Fixing on 2012-01-23
Alejandro J. Cura (community) Needs Fixing on 2012-01-23
Review via email: mp+81015@code.launchpad.net

Commit message

Provides the code so that there is a looping call in the control panel that will check for update of the application.

Description of the change

Provides the code so that there is a looping call in the control panel that will check for update of the application.

To post a comment you must log in.
Alejandro J. Cura (alecu) wrote :

This style of coding tests is very ugly and error prone:

534 + def _clean_fake_window_class(self):
535 + """Clean the fake window class."""
536 + FakeMainWindow.called = []
537 + FakeMainWindow.close_callback = None
538 +
539 + def _clean_fake_icon_class(self):
540 + """Clean the fake icon class."""
541 + FakeTrayIcon.window = None
542 + FakeTrayIcon.auto_update = None
543 +
544 + def _clean_fake_looping_call_class(self):
545 + """Clean the fake looping call class."""
546 + FakeLoopingCall.function = None
547 + FakeLoopingCall.args = None
548 + FakeLoopingCall.kwargs = None
549 + FakeLoopingCall.called = []

Instead of patching with a class that gets its attributes overwritten, please patch with an instance that gets thrown away, so there's no need to reset the class attributes like in the code above.

For instance:
"self.patch(gui, 'LoopingCall', FakeLoopingCall)" ->
"self.patch(gui, 'LoopingCall', FakeLoopingCallClass())"

This is present a few times in this branch, so make sure all occurrences end up fixed.

---

Also, to test code that uses LoopingCalls (and reactor.callLaters, too) remember that you can use twisted.internet.task.Clock, by patching the LoopingCall.clock attribute. Then you will be able to fake the reactor moving forward in time by calling clock.advance.

This gives you a lot of flexibility to test this kind of code, and the tests end up a lot faster and reliable than if they were using a small value for the LoopingCall or callLater time.

review: Needs Fixing
Manuel de la Peña (mandel) wrote :

> This style of coding tests is very ugly and error prone:
>
> 534 + def _clean_fake_window_class(self):
> 535 + """Clean the fake window class."""
> 536 + FakeMainWindow.called = []
> 537 + FakeMainWindow.close_callback = None
> 538 +
> 539 + def _clean_fake_icon_class(self):
> 540 + """Clean the fake icon class."""
> 541 + FakeTrayIcon.window = None
> 542 + FakeTrayIcon.auto_update = None
> 543 +
> 544 + def _clean_fake_looping_call_class(self):
> 545 + """Clean the fake looping call class."""
> 546 + FakeLoopingCall.function = None
> 547 + FakeLoopingCall.args = None
> 548 + FakeLoopingCall.kwargs = None
> 549 + FakeLoopingCall.called = []
>
> Instead of patching with a class that gets its attributes overwritten, please
> patch with an instance that gets thrown away, so there's no need to reset the
> class attributes like in the code above.
>
> For instance:
> "self.patch(gui, 'LoopingCall', FakeLoopingCall)" ->
> "self.patch(gui, 'LoopingCall', FakeLoopingCallClass())"
>
> This is present a few times in this branch, so make sure all occurrences end
> up fixed.
>

Fixed them on ubuntuone/controlpanel/gui/qt/tests/test_gui.py since that are the only tests where the class level and did indeed look ugly.

> ---
>
> Also, to test code that uses LoopingCalls (and reactor.callLaters, too)
> remember that you can use twisted.internet.task.Clock, by patching the
> LoopingCall.clock attribute. Then you will be able to fake the reactor moving
> forward in time by calling clock.advance.
>
> This gives you a lot of flexibility to test this kind of code, and the tests
> end up a lot faster and reliable than if they were using a small value for the
> LoopingCall or callLater time.

In my tests I'm asserting that the Looping call was indeed instantiated and started and I trust that the twisted implementation will be correct. I don't think I really need to move the clock to assert that the function was indeed called since the branch this branch depends on already tests that function.

I prefer not to force looping calls to be called since I find those types of tests to be integration tests. I'll leave the tests as they are so we can have a conversation later.

251. By Manuel de la Peña on 2011-11-14

Merged with parent.

252. By Manuel de la Peña on 2011-11-14

Fixed tests.

253. By Manuel de la Peña on 2011-11-14

Ensure __call__ returns self.

254. By Manuel de la Peña on 2011-11-16

Merged with parent and fixed conflicts.

255. By Manuel de la Peña on 2011-11-16

Resolve conflicts.

256. By Manuel de la Peña on 2011-11-16

Fixed lint issues.

257. By Manuel de la Peña on 2011-11-16

Removed all unecesary tests.

Natalia Bidart (nataliabidart) wrote :

This import code:

from ubuntuone.controlpanel.utils import (autoupdate,
    AUTOUPDATE_INTERVAL,
)

should be:

from ubuntuone.controlpanel.utils import (
    autoupdate,
    AUTOUPDATE_INTERVAL,
)

Also, in ubuntuone/controlpanel/gui/qt/gui.py, seems like this change is unrelated and may not be the desired one?

47 - if with_icon or minimized:
48 + if with_icon:

review: Needs Fixing
Manuel de la Peña (mandel) wrote :

Looking at the code you will see that we had:

46 if not minimized:
47 - if with_icon or minimized:

so if minimized = False we go through the if at line 46 we have that an x or False == x, right?

258. By Manuel de la Peña on 2011-12-01

Merged with trunk.

259. By Manuel de la Peña on 2011-12-01

Fix the order of the import.

Alejandro J. Cura (alecu) wrote :

The TrayIcon constructor takes a LoopingCall as a parameter, but it's never used in production code.
It should be either removed or used.

---

Both FakeMainWindow and FakeTrayIcon are modifying the class attributes.
For instance:
   FakeMainWindow.close_callback = close_callback

The tests should be operating on object instances instead of classes, since modifying classes is bad form and causes maintenance issues.

review: Needs Fixing
260. By Manuel de la Peña on 2011-12-01

Create an assign in the same statement.

261. By Manuel de la Peña on 2011-12-14

Reviews changes:Improved the tests not to use the class attrs.
Removed unused __init__ parameter.

262. By Manuel de la Peña on 2011-12-14

Fixed bug qhen calling the lc.

263. By Manuel de la Peña on 2011-12-14

Fixed qt tray tests.

264. By Manuel de la Peña on 2011-12-14

Fixed broken test after changes in FakeWindow.

Alejandro J. Cura (alecu) wrote :

Code looks good, all tests pass.
Approving.

review: Approve
265. By Manuel de la Peña on 2012-01-23

Merged with trunk and fixed conflicts.

266. By Manuel de la Peña on 2012-01-23

Fixed tests so that they work when using the FakeSdTool.

267. By Manuel de la Peña on 2012-01-23

Fixed some small lint issues.

Alejandro J. Cura (alecu) wrote :

I've just been warned by nessita that if we land this branch it will mean that the qt control panel on linux will need the qt4reactor, and we are trying to move away from it.
Nessita will add more details to this bug.

review: Needs Fixing
Natalia Bidart (nataliabidart) wrote :

* The file ubuntuone/controlpanel/gui/qt/tests/test_systray.py submitted in this branch is changing the EOLs to windows style, and we should have unix EOLs in all the files.

* Also, like alecu mentioned, we are working on having the QT control panel running only a Qt main loop in Linux, since we need to drop the dependency on the qt4reactor in that OS because we can't file a MIR for it (at least for now). So, since the autoupdate code is only relevant to windows, we'd need to move all that (included the LoopingCall, which brings in the need of a twisted reactor) to a windows-specific location, for example, main/windows.py.

I'd suggest to move the 'start' method to the 'main/' python package, and have all the autoupdate code moved to main/windows.py.

Sorry if I was not explicit about this before, but the goal of dropping the qt4reactor in Linux is a rather new objective.

Let me know if you need further assistance!
Thanks.

review: Needs Fixing
Manuel de la Peña (mandel) wrote :

> * The file ubuntuone/controlpanel/gui/qt/tests/test_systray.py submitted in
> this branch is changing the EOLs to windows style, and we should have unix
> EOLs in all the files.
>

Hm.. strange I used vim on linux to edit that.. let me double check and make sure its ok.

> * Also, like alecu mentioned, we are working on having the QT control panel
> running only a Qt main loop in Linux, since we need to drop the dependency on
> the qt4reactor in that OS because we can't file a MIR for it (at least for
> now). So, since the autoupdate code is only relevant to windows, we'd need to
> move all that (included the LoopingCall, which brings in the need of a twisted
> reactor) to a windows-specific location, for example, main/windows.py.
>
> I'd suggest to move the 'start' method to the 'main/' python package, and have
> all the autoupdate code moved to main/windows.py.
>
> Sorry if I was not explicit about this before, but the goal of dropping the
> qt4reactor in Linux is a rather new objective.
>
> Let me know if you need further assistance!
> Thanks.

Sure, no problem, its a rather old branch so moving qt to linux is a new thing. I'll get it fix for today.

268. By Manuel de la Peña on 2012-01-24

Removed the need to import the reactor from the systray.

269. By Manuel de la Peña on 2012-01-24

Fixed the start tests that failed due to changes in the API.

270. By Manuel de la Peña on 2012-01-24

Added a looping call for the linux implementation based on QTimer.

271. By Manuel de la Peña on 2012-01-24

Face palm.

272. By Manuel de la Peña on 2012-01-24

Added tests for the linux LoopingCall.

273. By Manuel de la Peña on 2012-01-24

Face palm.

274. By Manuel de la Peña on 2012-01-24

_execute required to have self passed.

275. By Manuel de la Peña on 2012-01-24

Fixed tests.

276. By Manuel de la Peña on 2012-01-24

Got all tests working correctly.

Manuel de la Peña (mandel) wrote :

I have applied changes to the code to simplify the move away from the qt4reactor, please let me know if they are goo enough:

* Removed the import of stop in the systray (present in trunk). Because we pass the stop function to main lets just foward that to systray.
* Added a LoopingCall object that uses QTimer for linux since we won't be using the reactor. Added tests to ensure if works as a LoopingCall (including same assert exceptions.)
* dos2unix used in the files with windows EOL.

277. By Manuel de la Peña on 2012-01-24

Moved start to a diff location so that we do not drag the qt reactor with us.

278. By Manuel de la Peña on 2012-01-24

Merged with parent.

279. By Manuel de la Peña on 2012-01-24

Added tests for start.

280. By Manuel de la Peña on 2012-01-24

Fixed tests on windows.

281. By Manuel de la Peña on 2012-01-25

Always perform the auto update looping call.

282. By Manuel de la Peña on 2012-01-25

Fixed lint issues.

283. By Manuel de la Peña on 2012-01-25

Updated copyrighs.

284. By Manuel de la Peña on 2012-01-25

Removed the pep8 issue.

285. By Manuel de la Peña on 2012-03-29

Merged with trunk and fixed all the conflicts.

286. By Manuel de la Peña on 2012-03-30

Moved looping call to a task package.

287. By Manuel de la Peña on 2012-03-30

Merged and resolve conflicts with the unsintall branch.

288. By Manuel de la Peña on 2012-03-30

Added the tests to the correct location. Updated setup.py

289. By Manuel de la Peña on 2012-03-30

Set the value of the looping call.

Unmerged revisions

289. By Manuel de la Peña on 2012-03-30

Set the value of the looping call.

288. By Manuel de la Peña on 2012-03-30

Added the tests to the correct location. Updated setup.py

287. By Manuel de la Peña on 2012-03-30

Merged and resolve conflicts with the unsintall branch.

286. By Manuel de la Peña on 2012-03-30

Moved looping call to a task package.

285. By Manuel de la Peña on 2012-03-29

Merged with trunk and fixed all the conflicts.

284. By Manuel de la Peña on 2012-01-25

Removed the pep8 issue.

283. By Manuel de la Peña on 2012-01-25

Updated copyrighs.

282. By Manuel de la Peña on 2012-01-25

Fixed lint issues.

281. By Manuel de la Peña on 2012-01-25

Always perform the auto update looping call.

280. By Manuel de la Peña on 2012-01-24

Fixed tests on windows.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'setup.py'
2--- setup.py 2012-03-07 14:24:40 +0000
3+++ setup.py 2012-03-30 18:41:34 +0000
4@@ -207,6 +207,7 @@
5 'ubuntuone.controlpanel.gui',
6 'ubuntuone.controlpanel.gui.qt',
7 'ubuntuone.controlpanel.gui.qt.main',
8+ 'ubuntuone.controlpanel.gui.qt.task',
9 'ubuntuone.controlpanel.gui.qt.ui',
10 'ubuntuone.controlpanel.gui.qt.uniqueapp',
11 'ubuntuone.controlpanel.sd_client',
12
13=== modified file 'ubuntuone/controlpanel/gui/__init__.py'
14--- ubuntuone/controlpanel/gui/__init__.py 2012-03-20 15:11:56 +0000
15+++ ubuntuone/controlpanel/gui/__init__.py 2012-03-30 18:41:34 +0000
16@@ -246,6 +246,8 @@
17 TALK_TO_US = _('Talk to us')
18 VALUE_ERROR = _('Value could not be retrieved.')
19 UNKNOWN_ERROR = _('Unknown error')
20+UPDATES_TITLE = _('Updates')
21+UPDATES_MESSAGE = _('There is a new update available')
22 USAGE_LABEL = _('%(used)s of %(total)s')
23 QUIT_LABEL = _('Quit Ubuntu One')
24
25
26=== modified file 'ubuntuone/controlpanel/gui/qt/gui.py'
27--- ubuntuone/controlpanel/gui/qt/gui.py 2012-03-29 19:05:02 +0000
28+++ ubuntuone/controlpanel/gui/qt/gui.py 2012-03-30 18:41:34 +0000
29@@ -18,8 +18,15 @@
30
31 from PyQt4 import QtGui, QtCore
32
33+from ubuntuone.controlpanel.utils import (
34+ autoupdate,
35+ AUTOUPDATE_INTERVAL,
36+)
37+from ubuntuone.controlpanel.gui import UPDATES_TITLE, UPDATES_MESSAGE
38 from ubuntuone.controlpanel.gui.qt.systray import TrayIcon
39+from ubuntuone.controlpanel.gui.qt.task import LoopingCall
40 from ubuntuone.controlpanel.gui.qt.ui import mainwindow_ui
41+from ubuntuone.controlpanel.logger import setup_logging
42 from ubuntuone.controlpanel.utils import add_to_autostart
43
44 # pylint: disable=E0611
45@@ -32,6 +39,8 @@
46
47 U1_DOTDESKTOP = "ubuntuone-installer.desktop"
48
49+logger = setup_logging('qt.gui')
50+
51
52 class MainWindow(QtGui.QMainWindow):
53 """The Main Window of the Control Panel."""
54@@ -108,6 +117,10 @@
55 if with_icon or minimized:
56 QtGui.QApplication.instance().setQuitOnLastWindowClosed(False)
57 icon = TrayIcon(window=window, close_callback=close_callback)
58+ if AUTOUPDATE_INTERVAL:
59+ icon.auto_update_lc = LoopingCall(autoupdate, UPDATES_TITLE,
60+ UPDATES_MESSAGE, icon, logger)
61+ icon.auto_update_lc.start(AUTOUPDATE_INTERVAL, now=True)
62 else:
63 icon = None
64 return icon, window
65
66=== modified file 'ubuntuone/controlpanel/gui/qt/main/__init__.py'
67--- ubuntuone/controlpanel/gui/qt/main/__init__.py 2012-03-29 17:20:14 +0000
68+++ ubuntuone/controlpanel/gui/qt/main/__init__.py 2012-03-30 18:41:34 +0000
69@@ -1,6 +1,6 @@
70 # -*- coding: utf-8 -*-
71 #
72-# Copyright 2011 Canonical Ltd.
73+# Copyright 2011-12 Canonical Ltd.
74 #
75 # This program is free software: you can redistribute it and/or modify it
76 # under the terms of the GNU General Public License version 3, as published
77
78=== modified file 'ubuntuone/controlpanel/gui/qt/systray.py'
79--- ubuntuone/controlpanel/gui/qt/systray.py 2012-03-02 13:53:24 +0000
80+++ ubuntuone/controlpanel/gui/qt/systray.py 2012-03-30 18:41:34 +0000
81@@ -34,6 +34,9 @@
82 self.setIcon(QtGui.QIcon(":/icon.png"))
83 self.setVisible(True)
84 self.window = window
85+ self.auto_update_lc = None
86+ self._message_cb = lambda: None
87+ self.messageClicked.connect(self.on_message_clicked)
88 self.activated.connect(self.on_activated)
89 self.context_menu = QtGui.QMenu()
90 self.restore = QtGui.QAction(RESTORE_LABEL, self,
91@@ -52,6 +55,10 @@
92 if reason == self.Trigger: # Left-click
93 self.restore_window()
94
95+ def on_message_clicked(self):
96+ """Execute the callback that was set."""
97+ self._message_cb()
98+
99 def restore_window(self):
100 """Show the main window."""
101 if self.window is None:
102@@ -79,4 +86,14 @@
103 except:
104 # Maybe it was not running?
105 pass
106+ if self.auto_update_lc is not None and\
107+ self.auto_update_lc.running:
108+ self.auto_update_lc.stop()
109+
110 self.close_callback()
111+
112+
113+ def show_message(self, title, message, clicked_cb=lambda: None):
114+ """Show a message and set the new callback to execute."""
115+ self._message_cb = clicked_cb
116+ self.showMessage(title, message)
117
118=== added directory 'ubuntuone/controlpanel/gui/qt/task'
119=== added file 'ubuntuone/controlpanel/gui/qt/task/__init__.py'
120--- ubuntuone/controlpanel/gui/qt/task/__init__.py 1970-01-01 00:00:00 +0000
121+++ ubuntuone/controlpanel/gui/qt/task/__init__.py 2012-03-30 18:41:34 +0000
122@@ -0,0 +1,30 @@
123+# -*- coding: utf-8 -*-
124+#
125+# Copyright 2012 Canonical Ltd.
126+#
127+# This program is free software: you can redistribute it and/or modify it
128+# under the terms of the GNU General Public License version 3, as published
129+# by the Free Software Foundation.
130+#
131+# This program is distributed in the hope that it will be useful, but
132+# WITHOUT ANY WARRANTY; without even the implied warranties of
133+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
134+# PURPOSE. See the GNU General Public License for more details.
135+#
136+# You should have received a copy of the GNU General Public License along
137+# with this program. If not, see <http://www.gnu.org/licenses/>.
138+
139+"""Allow to perform tasks in the main loop."""
140+
141+import sys
142+
143+
144+#pylint: disable=C0103
145+if sys.platform == 'win32':
146+ from twisted.internet import task
147+ source = task
148+else:
149+ from ubuntuone.controlpanel.gui.qt.task import linux
150+ source = linux
151+
152+LoopingCall = source.LoopingCall
153
154=== added file 'ubuntuone/controlpanel/gui/qt/task/linux.py'
155--- ubuntuone/controlpanel/gui/qt/task/linux.py 1970-01-01 00:00:00 +0000
156+++ ubuntuone/controlpanel/gui/qt/task/linux.py 2012-03-30 18:41:34 +0000
157@@ -0,0 +1,54 @@
158+# -*- coding: utf-8 -*-
159+#
160+# Copyright 2011-12 Canonical Ltd.
161+#
162+# This program is free software: you can redistribute it and/or modify it
163+# under the terms of the GNU General Public License version 3, as published
164+# by the Free Software Foundation.
165+#
166+# This program is distributed in the hope that it will be useful, but
167+# WITHOUT ANY WARRANTY; without even the implied warranties of
168+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
169+# PURPOSE. See the GNU General Public License for more details.
170+#
171+# You should have received a copy of the GNU General Public License along
172+# with this program. If not, see <http://www.gnu.org/licenses/>.
173+
174+"""Main method to be used on linux."""
175+
176+from PyQt4 import QtCore
177+
178+
179+class LoopingCall(QtCore.QObject):
180+ """A looping call executed every x seconds."""
181+
182+ def __init__(self, callback, *args, **kwargs):
183+ """Create a new function to call."""
184+ QtCore.QObject.__init__(self)
185+ self.cb = callback
186+ self.args = args
187+ self.kwargs = kwargs
188+ self.timer = QtCore.QTimer(self)
189+ self.timer.timeout.connect(self._execute)
190+ self.running = False
191+
192+ def _execute(self):
193+ """Execute the function passed to the looping call."""
194+ self.cb(*self.args, **self.kwargs)
195+
196+ def start(self, interval, now=True):
197+ """Start the looping call."""
198+ assert not self.running, ("Tried to start an already running "
199+ "LoopingCall.")
200+
201+ self.running = True
202+ if now:
203+ self._execute()
204+ self.timer.start(interval)
205+
206+ def stop(self):
207+ """Stop the looping call."""
208+ assert self.running, ("Tried to stop a LoopingCall that was "
209+ "not running.")
210+ self.timer.stop()
211+ self.running = False
212
213=== added directory 'ubuntuone/controlpanel/gui/qt/task/tests'
214=== added file 'ubuntuone/controlpanel/gui/qt/task/tests/__init__.py'
215--- ubuntuone/controlpanel/gui/qt/task/tests/__init__.py 1970-01-01 00:00:00 +0000
216+++ ubuntuone/controlpanel/gui/qt/task/tests/__init__.py 2012-03-30 18:41:34 +0000
217@@ -0,0 +1,17 @@
218+# -*- coding: utf-8 -*-
219+#
220+# Copyright 2011-2012 Canonical Ltd.
221+#
222+# This program is free software: you can redistribute it and/or modify it
223+# under the terms of the GNU General Public License version 3, as published
224+# by the Free Software Foundation.
225+#
226+# This program is distributed in the hope that it will be useful, but
227+# WITHOUT ANY WARRANTY; without even the implied warranties of
228+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
229+# PURPOSE. See the GNU General Public License for more details.
230+#
231+# You should have received a copy of the GNU General Public License along
232+# with this program. If not, see <http://www.gnu.org/licenses/>.
233+
234+"""The test suite for the Qt task module."""
235
236=== added file 'ubuntuone/controlpanel/gui/qt/task/tests/test_linux.py'
237--- ubuntuone/controlpanel/gui/qt/task/tests/test_linux.py 1970-01-01 00:00:00 +0000
238+++ ubuntuone/controlpanel/gui/qt/task/tests/test_linux.py 2012-03-30 18:41:34 +0000
239@@ -0,0 +1,82 @@
240+# -*- coding: utf-8 -*-
241+#
242+# Copyright 2011 Canonical Ltd.
243+#
244+# This program is free software: you can redistribute it and/or modify it
245+# under the terms of the GNU General Public License version 3, as published
246+# by the Free Software Foundation.
247+#
248+# This program is distributed in the hope that it will be useful, but
249+# WITHOUT ANY WARRANTY; without even the implied warranties of
250+# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
251+# PURPOSE. See the GNU General Public License for more details.
252+#
253+# You should have received a copy of the GNU General Public License along
254+# with this program. If not, see <http://www.gnu.org/licenses/>.
255+
256+"""Tests for the main module on linux."""
257+
258+from twisted.internet import defer
259+
260+from ubuntuone.controlpanel.gui.qt.task import linux
261+from ubuntuone.controlpanel.tests import TestCase
262+
263+
264+class FakeCallback(object):
265+ """Fake callback."""
266+
267+ def __init__(self):
268+ """Create a new instance."""
269+ self.args = None
270+ self.kwargs = None
271+ self.deferred = defer.Deferred()
272+
273+ def __call__(self, *args, **kwargs):
274+ """Call the cb."""
275+ self.args = args
276+ self.kwargs = kwargs
277+ if not self.deferred.called:
278+ self.deferred.callback(True)
279+
280+
281+class TestLoopingCall(TestCase):
282+ """Test the looping call implementation."""
283+
284+ @defer.inlineCallbacks
285+ def setUp(self):
286+ """Set up tests."""
287+ yield super(TestLoopingCall, self).setUp()
288+ self.cb = FakeCallback()
289+ self.interval = 100
290+
291+ @defer.inlineCallbacks
292+ def test_start(self):
293+ """Test that the call is indeed done."""
294+ lc = linux.LoopingCall(self.cb, 'test', other='test')
295+ lc.start(self.interval)
296+ self.addCleanup(lc.stop)
297+ yield self.cb.deferred
298+ self.assertTrue(lc.running)
299+ self.assertEqual(('test',), self.cb.args)
300+ self.assertEqual(dict(other='test'), self.cb.kwargs)
301+
302+ def test_start_twice(self):
303+ """Test that we can't call start more than once."""
304+ lc = linux.LoopingCall(self.cb, 'test', other='test')
305+ lc.start(self.interval)
306+ self.addCleanup(lc.stop)
307+ self.assertRaises(AssertionError, lc.start, self.interval)
308+
309+ def test_stop(self):
310+ """Test that we stop."""
311+ lc = linux.LoopingCall(self.cb, 'test', other='test')
312+ lc.start(self.interval)
313+ lc.stop()
314+ self.assertFalse(lc.running)
315+
316+ def test_stop_twice(self):
317+ """Test that we can't call stop more than once."""
318+ lc = linux.LoopingCall(self.cb, 'test', other='test')
319+ lc.start(self.interval)
320+ lc.stop()
321+ self.assertRaises(AssertionError, lc.stop)
322
323=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_gui.py'
324--- ubuntuone/controlpanel/gui/qt/tests/test_gui.py 2012-03-26 13:30:07 +0000
325+++ ubuntuone/controlpanel/gui/qt/tests/test_gui.py 2012-03-30 18:41:34 +0000
326@@ -35,6 +35,70 @@
327 self.called = (args, kwargs)
328
329
330+class FakeMainWindow(object):
331+ """Fake window."""
332+
333+ def __init__(self, close_callback=None):
334+ """Create a new instance."""
335+ self.called = []
336+ self.close_callback = None
337+
338+ def __call__(self, *args, **kwargs):
339+ """Make the object callable to fake a constructor."""
340+ return self
341+
342+ def show(self):
343+ """Fake show the window."""
344+ self.called.append(('show',))
345+
346+
347+class FakeTrayIcon(object):
348+ """Fake tray icon."""
349+
350+ def __init__(self, window=None):
351+ """Create a new instance."""
352+ self.auto_update = None
353+ self.window = None
354+
355+ def __call__(self, window, *args, **kwargs):
356+ """Make the object callable to fake a cosntructor."""
357+ self.window = window
358+ return self
359+
360+ def get_auto_update_lc(self):
361+ """Get the auto_update_lc."""
362+ return self.auto_update
363+
364+ def set_auto_update_lc(self, value):
365+ """Set the auto_update_lc."""
366+ self.auto_update = value
367+
368+ auto_update_lc = property(get_auto_update_lc,
369+ set_auto_update_lc)
370+
371+
372+class FakeLoopingCall(object):
373+ """Fake a looping call."""
374+
375+ def __init__(self):
376+ """Create a new instance."""
377+ self.function = None
378+ self.args = None
379+ self.kwargs = None
380+ self.called = []
381+
382+ def __call__(self, f, *args, **kwargs):
383+ """Call looping call."""
384+ self.function = f
385+ self.args = args
386+ self.kwargs = kwargs
387+ return self
388+
389+ def start(self, interval, now=False):
390+ """Fake the start."""
391+ self.called.append(('start', interval, now))
392+
393+
394 class MainWindowTestCase(BaseTestCase):
395 """Test the qt main window."""
396
397@@ -131,3 +195,71 @@
398 def test_add_to_autostart(self):
399 """Test that the add_to_autostart function is called when CP opens."""
400 self.assertTrue(self._called)
401+
402+
403+class StartTestCase(BaseTestCase):
404+ """Test the start method."""
405+
406+ @defer.inlineCallbacks
407+ def setUp(self):
408+ """Set the tests."""
409+ yield super(StartTestCase, self).setUp()
410+ self.called = []
411+ self.auto_update_interval = 0.5
412+ self.looping_call = FakeLoopingCall()
413+
414+ def stop():
415+ """Fake stop function."""
416+ self.called.append(('stop',))
417+
418+ def auto_update(title, message, icon, logger):
419+ """Fake auto_update_cb."""
420+ self.called.append(('auto_update_cb', title, message,
421+ icon, logger))
422+
423+ self.stop_cb = stop
424+ self.auto_update_cb = auto_update
425+ self.main_window = FakeMainWindow()
426+ self.tray_icon = FakeTrayIcon()
427+ old_time = gui.AUTOUPDATE_INTERVAL
428+ self.addCleanup(self._set_time, old_time)
429+ gui.AUTOUPDATE_INTERVAL = 1
430+
431+ def _set_time(self, time):
432+ """Set the correct value back."""
433+ gui.AUTOUPDATE_INTERVAL = time
434+
435+ def test_start_not_minimized_no_icon(self):
436+ """Test the not minimized mode with no icon."""
437+ icon, window = gui.start(self.stop_cb, minimized=False,
438+ with_icon=False)
439+ self.assertEqual(1, len(self.main_window.called))
440+ self.assertTrue('show' in self.main_window.called[0])
441+ self.assertEqual(None, self.looping_call.function)
442+ self.assertNotEqual(None, window)
443+ self.assertEqual(None, icon)
444+
445+ def _assert_minimized_or_icon(self, icon, window):
446+ """Assert the start of a minimized of icon mode."""
447+ self.assertIsInstance(self.tray_icon.auto_update, FakeLoopingCall)
448+ self.assertNotEqual(None, self.looping_call.function)
449+ self.assertNotEqual(None, icon)
450+
451+ def test_start_with_icon(self):
452+ """Test the with icon start."""
453+ icon, window = gui.start(self.stop_cb, minimized=False,
454+ with_icon=True)
455+ self._assert_minimized_or_icon(icon, window)
456+ self.assertEqual(1, len(self.main_window.called))
457+ self.assertTrue('show' in self.main_window.called[0])
458+ self.assertNotEqual(None, window)
459+ self.assertNotEqual(None, self.tray_icon.window)
460+
461+ def test_start_minimized(self):
462+ """Test the minimized start."""
463+ icon, window = gui.start(self.stop_cb, minimized=True,
464+ with_icon=False)
465+ self._assert_minimized_or_icon(icon, window)
466+ self.assertEqual(0, len(self.main_window.called))
467+ self.assertEqual(None, window)
468+ self.assertEqual(None, self.tray_icon.window)
469
470=== removed file 'ubuntuone/controlpanel/gui/qt/tests/test_start.py'
471--- ubuntuone/controlpanel/gui/qt/tests/test_start.py 2012-03-20 19:13:28 +0000
472+++ ubuntuone/controlpanel/gui/qt/tests/test_start.py 1970-01-01 00:00:00 +0000
473@@ -1,108 +0,0 @@
474-# -*- coding: utf-8 -*-
475-
476-# Author: Roberto Alsina <roberto.alsina@canonical.com>
477-#
478-# Copyright 2011 Canonical Ltd.
479-#
480-# This program is free software: you can redistribute it and/or modify it
481-# under the terms of the GNU General Public License version 3, as published
482-# by the Free Software Foundation.
483-#
484-# This program is distributed in the hope that it will be useful, but
485-# WITHOUT ANY WARRANTY; without even the implied warranties of
486-# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
487-# PURPOSE. See the GNU General Public License for more details.
488-#
489-# You should have received a copy of the GNU General Public License along
490-# with this program. If not, see <http://www.gnu.org/licenses/>.
491-
492-"""Tests for the start function."""
493-
494-from twisted.internet import defer
495-
496-from ubuntuone.controlpanel.gui.qt import gui
497-from ubuntuone.controlpanel.tests import TestCase
498-
499-
500-class FakeThing(object):
501-
502- """A fake thing."""
503-
504- shown = False
505- size = lambda *a: gui.QtCore.QSize(123456, 654321)
506- style = None
507-
508- def __init__(self):
509- self.args = []
510-
511- def __call__(self, *args, **kwargs):
512- self.args.append((args, kwargs))
513- return self
514-
515- def show(self):
516- """Show."""
517- self.shown = True
518-
519- # Invalid name "setGeometry"
520- # pylint: disable=C0103
521-
522- def setGeometry(self, style):
523- """Save the new geometry."""
524- self.style = style
525-
526-
527-class StartTestCase(TestCase):
528- """Test the qt control panel."""
529-
530- @defer.inlineCallbacks
531- def setUp(self):
532- yield super(StartTestCase, self).setUp()
533- self.main_window = FakeThing()
534- self.tray_icon = FakeThing()
535- self.patch(gui, "MainWindow", self.main_window)
536- self.patch(gui, "TrayIcon", self.tray_icon)
537-
538- def close_cb(self):
539- """A dummy close callback."""
540-
541- def test_minimized(self):
542- """Test behaviour with minimized=True."""
543- gui.start(close_callback=self.close_cb,
544- minimized=True, with_icon=True)
545- kwargs = {'close_callback': self.close_cb, 'window': None}
546- self.assertEqual(self.tray_icon.args, [((), kwargs)])
547- self.assertEqual(self.main_window.args, [])
548-
549- def test_with_icon(self):
550- """Test behaviour with with_icon=True."""
551- gui.start(close_callback=self.close_cb,
552- with_icon=True, minimized=False)
553- kwargs = {'close_callback': self.close_cb, 'window': self.main_window}
554- self.assertEqual(self.main_window.args, [((), {'installer': False})])
555- self.assertEqual(self.tray_icon.args, [((), kwargs)])
556-
557- def test_both_false(self):
558- """Test behaviour when with_icon and minimized are False."""
559- gui.start(close_callback=self.close_cb,
560- with_icon=False, minimized=False)
561- # Should be called
562- self.assertNotEqual(self.main_window.args, [])
563- # Should not be called
564- self.assertEqual(self.tray_icon.args, [])
565-
566- def test_both_true(self):
567- """Test behaviour when with_icon and minimized are True."""
568- gui.start(close_callback=self.close_cb,
569- with_icon=True, minimized=True)
570- kwargs = {'close_callback': self.close_cb, 'window': None}
571- self.assertEqual(self.tray_icon.args, [((), kwargs)])
572- self.assertEqual(self.main_window.args, [])
573-
574- def test_center_window(self):
575- """The main window should be centered."""
576- gui.start(close_callback=self.close_cb)
577- app = gui.QtGui.QApplication.instance()
578- expected = gui.QtGui.QStyle.alignedRect(gui.QtCore.Qt.LeftToRight,
579- gui.QtCore.Qt.AlignCenter, self.main_window.size(),
580- app.desktop().availableGeometry())
581- self.assertEqual(self.main_window.style, expected)
582
583=== modified file 'ubuntuone/controlpanel/gui/qt/tests/test_systray.py'
584--- ubuntuone/controlpanel/gui/qt/tests/test_systray.py 2012-02-06 21:10:10 +0000
585+++ ubuntuone/controlpanel/gui/qt/tests/test_systray.py 2012-03-30 18:41:34 +0000
586@@ -19,7 +19,7 @@
587 """Tests for the notification area icon."""
588
589 from PyQt4 import QtGui
590-from twisted.internet.defer import inlineCallbacks
591+from twisted.internet import defer
592
593 from ubuntuone.controlpanel.gui.qt import systray
594 from ubuntuone.controlpanel.tests import TestCase
595@@ -46,12 +46,37 @@
596 super(FakeMainWindow, self).__init__()
597
598
599+class FakeLoopingCall(object):
600+ """Fake a twisted looping call."""
601+
602+ def __init__(self, is_running=False):
603+ """Create a new instance."""
604+ self.called = []
605+ self.running = is_running
606+
607+ def start(self, interval, now=False):
608+ """Fake starting the call."""
609+ self.called.append(('start', interval, now))
610+ self.running = True
611+
612+ def stop(self):
613+ """Fake stopping the call."""
614+ self.called.append(('stop',))
615+ self.running = False
616+
617+
618 class SystrayTestCase(TestCase):
619
620 """Test the notification area icon."""
621
622- def test_quit(self):
623- """Test the quit option in the menu."""
624+ @defer.inlineCallbacks
625+ def setUp(self):
626+ """Set the different tests."""
627+ yield super(SystrayTestCase, self).setUp()
628+ self.looping_call = FakeLoopingCall()
629+
630+ def test_quit_no_lc(self):
631+ """Test the quit option with no looping call."""
632 # Not done on setup, because if I patch stop
633 # after instantiation, it doesn't get called.
634 self.patch(systray.TrayIcon, "stop", self._set_called)
635@@ -59,12 +84,36 @@
636 tray.quit.trigger()
637 self.assertEqual(self._called, ((False,), {}))
638
639- @inlineCallbacks
640+ @defer.inlineCallbacks
641+ def test_quit_lc_not_running(self):
642+ """Test the quit option with a stopped looping call."""
643+ st = FakeSDTool()
644+ self.patch(systray, "SyncDaemonTool", lambda: st)
645+ tray = systray.TrayIcon(close_callback=self._set_called)
646+ tray.auto_update_lc = self.looping_call
647+ yield tray.stop()
648+ self.assertEqual(self._called, ((), {}))
649+ self.assertEqual(0, len(self.looping_call.called))
650+
651+ @defer.inlineCallbacks
652+ def test_quit_lc_running(self):
653+ """Test the quit option with a running looping call."""
654+ st = FakeSDTool()
655+ self.patch(systray, "SyncDaemonTool", lambda: st)
656+ self.looping_call.running = True
657+ tray = systray.TrayIcon(close_callback=self._set_called)
658+ tray.auto_update_lc = self.looping_call
659+ yield tray.stop()
660+ self.assertEqual(self._called, ((), {}))
661+ self.assertEqual(1, len(self.looping_call.called))
662+ self.assertTrue('stop' in self.looping_call.called[0])
663+
664+ @defer.inlineCallbacks
665 def test_stop_sd(self):
666 """Quit should call SyncDaemonTool.quit()."""
667 st = FakeSDTool()
668 self.patch(systray, "SyncDaemonTool", lambda: st)
669- tray = systray.TrayIcon()
670+ tray = systray.TrayIcon(close_callback=lambda: None)
671 yield tray.stop()
672 self.assertTrue(st.called)
673
674@@ -122,3 +171,26 @@
675 self.assertEqual(tray.window, None)
676 self.assertIsInstance(tray.context_menu, QtGui.QMenu)
677 self.assertFalse(tray.icon() == None)
678+
679+ def test_show_message(self):
680+ """Test the method that shows a message."""
681+ called = []
682+ title = 'updates'
683+ message = 'you want to update!'
684+
685+ def fake_qt_show_message(instance, title, message):
686+ """A fake qt show message."""
687+ called.extend([instance, title, message])
688+
689+ def fake_clicked_message_cb():
690+ """A fake clicked cb."""
691+
692+ self.patch(systray.TrayIcon, 'showMessage', fake_qt_show_message)
693+ tray = systray.TrayIcon()
694+ tray.show_message(title, message, fake_clicked_message_cb)
695+ self.assertTrue(tray in called)
696+ self.assertTrue(title in called)
697+ self.assertTrue(message in called)
698+ # pylint: disable=W0212
699+ self.assertEqual(fake_clicked_message_cb, tray._message_cb)
700+ # pylint: enable=W0212
701
702=== modified file 'ubuntuone/controlpanel/utils/__init__.py'
703--- ubuntuone/controlpanel/utils/__init__.py 2012-03-29 17:58:28 +0000
704+++ ubuntuone/controlpanel/utils/__init__.py 2012-03-30 18:41:34 +0000
705@@ -39,16 +39,20 @@
706 from ubuntuone.controlpanel.utils import windows
707 add_to_autostart = windows.add_to_autostart
708 are_updates_present = windows.are_updates_present
709+ autoupdate = windows.autoupdate
710 default_folders = windows.default_folders
711 perform_update = windows.perform_update
712 uninstall_application = windows.uninstall_application
713+ AUTOUPDATE_INTERVAL = windows.AUTOUPDATE_INTERVAL
714 else:
715 from ubuntuone.controlpanel.utils import linux
716 add_to_autostart = no_op
717 are_updates_present = no_op
718+ autoupdate = no_op
719 default_folders = linux.default_folders
720 perform_update = no_op
721 uninstall_application = no_op
722+ AUTOUPDATE_INTERVAL = None
723
724 # pylint: enable=C0103
725
726
727=== modified file 'ubuntuone/controlpanel/utils/tests/test_windows.py'
728--- ubuntuone/controlpanel/utils/tests/test_windows.py 2012-03-30 17:34:33 +0000
729+++ ubuntuone/controlpanel/utils/tests/test_windows.py 2012-03-30 18:41:34 +0000
730@@ -21,7 +21,7 @@
731
732 from twisted.internet import defer
733
734-from ubuntuone.controlpanel import utils
735+from ubuntuone.controlpanel import gui, utils
736 from ubuntuone.controlpanel.tests import TestCase
737
738 # let me use protected methods
739@@ -56,6 +56,19 @@
740 self.addCleanup(setattr, utils.windows.sys, attr_name, value)
741
742
743+class FakeSysTray(object):
744+ """A Fake systray object."""
745+
746+ def __init__(self):
747+ """Create a new instance."""
748+ self.called = []
749+
750+ def show_message(self, title, message, callback):
751+ """Fake show message."""
752+ self.called.append(('show_message', title, message,
753+ callback))
754+
755+
756 class AutoupdaterTestCase(TestCase):
757 """Test the code that is used for the auto update process."""
758
759@@ -69,6 +82,7 @@
760 self.return_from_call = 0
761 self.command = None
762 self.args = []
763+ self.systray = FakeSysTray()
764
765 def fake_execute_process(command, args=None, path=None):
766 """Fake async process execution."""
767@@ -114,6 +128,37 @@
768 '--unattendedmodeui none', '', 0)
769 self.assertEqual(self._called, (args, {}))
770
771+ @defer.inlineCallbacks
772+ def test_autoupdate_no_updates(self):
773+ """Test the autoupdate method with no updates."""
774+
775+ def fake_are_updates_present():
776+ """Fake are_updates_present."""
777+ return defer.succeed(False)
778+
779+ self.patch(utils.windows, 'are_updates_present',
780+ fake_are_updates_present)
781+ yield utils.autoupdate(gui.UPDATES_TITLE, gui.UPDATES_MESSAGE,
782+ self.systray)
783+ self.assertEqual(0, len(self.systray.called))
784+
785+ @defer.inlineCallbacks
786+ def test_autoupdate_systray(self):
787+ """Test the autoupdate method with the systray integration."""
788+
789+ def fake_are_updates_present():
790+ """Fake are_updates_present."""
791+ return defer.succeed(True)
792+
793+ self.patch(utils.windows, 'are_updates_present',
794+ fake_are_updates_present)
795+ yield utils.autoupdate(gui.UPDATES_TITLE, gui.UPDATES_MESSAGE,
796+ self.systray)
797+ self.assertEqual(1, len(self.systray.called))
798+ self.assertTrue(gui.UPDATES_TITLE in self.systray.called[0])
799+ self.assertTrue(gui.UPDATES_MESSAGE in self.systray.called[0])
800+ self.assertTrue(utils.windows.perform_update in self.systray.called[0])
801+
802
803 class FakeOpenKey(object):
804
805
806=== modified file 'ubuntuone/controlpanel/utils/windows.py'
807--- ubuntuone/controlpanel/utils/windows.py 2012-03-30 17:34:33 +0000
808+++ ubuntuone/controlpanel/utils/windows.py 2012-03-30 18:41:34 +0000
809@@ -33,6 +33,7 @@
810
811 logger = setup_logging('utils.windows')
812 AUTOUPDATE_EXE_NAME = 'autoupdate-windows.exe'
813+AUTOUPDATE_INTERVAL = 60 * 24 # 60s * 24 hours
814 AUTORUN_KEY = r"Software\Microsoft\Windows\CurrentVersion\Run"
815 UNINSTALL_EXE_NAME = 'uninstall.exe'
816
817@@ -87,6 +88,17 @@
818 defer.returnValue(result)
819
820
821+@defer.inlineCallbacks
822+def autoupdate(title, message, tray_icon):
823+ """Perform the full process of autoupdating the app."""
824+ # Check fo updates and pop a message to the user when
825+ # and update is present
826+ are_present = yield are_updates_present()
827+ if are_present:
828+ tray_icon.show_message(title, message,
829+ perform_update)
830+
831+
832 def default_folders(user_home=None):
833 """Return a list of the folders to add by default."""
834 # as per http://msdn.microsoft.com/en-us/library/windows/desktop/bb762181,

Subscribers

People subscribed via source and target branches

to all changes: