Merge lp:~nataliabidart/software-center/fix-986563 into lp:software-center/5.2
- fix-986563
- Merge into 5.2
Status: | Merged |
---|---|
Merged at revision: | 3032 |
Proposed branch: | lp:~nataliabidart/software-center/fix-986563 |
Merge into: | lp:software-center/5.2 |
Prerequisite: | lp:~nataliabidart/software-center/fix-977931 |
Diff against target: |
862 lines (+323/-159) 9 files modified
softwarecenter/backend/channel.py (+4/-4) softwarecenter/db/application.py (+5/-4) softwarecenter/db/database.py (+20/-5) softwarecenter/testutils.py (+56/-0) softwarecenter/ui/gtk3/models/appstore2.py (+11/-5) softwarecenter/ui/gtk3/views/catview_gtk.py (+28/-16) test/gtk3/test_app.py (+2/-36) test/gtk3/test_catview.py (+190/-88) test/test_database.py (+7/-1) |
To merge this branch: | bzr merge lp:~nataliabidart/software-center/fix-986563 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Vogt | Approve | ||
Review via email: mp+106710@code.launchpad.net |
Commit message
- Filtered out those exhibits that do not their packages available in the db.
Description of the change
Some of the changes in this diff were needed to avoid some nasty exception while running the test suite (for example the changes of how get_distro is imported).
- 3027. By Natalia Bidart
-
Attaching bug #986563.
Natalia Bidart (nataliabidart) wrote : | # |
> This is again a beauty to read and I much appreciate the effort that goes
> beyond the immediate fix and that
> helps making the code and tests better. The ExhibitsTestCase code is a
> excellent example on a good and
> clean test, very nice.
:-)
> Some questions/comments:
> - what do you think about moving "_pkg_available()" out into a more generic
> place like db/utils.py ?
(09:42:04 AM) nessita: mvo: is there any better/simpler way of knowing if a given pkg is available, without creating an Application? Like, can the StoreDatabase know that?
(otherwise we need to pass the db to the helper and that sounds, at least to me, a bit dirty)
> - _filter_
> if result list is empty and that empty list is set via set_exhibits() as the
> exhibit banner will ignore empty lists
Added.
> Why is
> 281 + if self.top_rated is None:
> 282 + return
> needed? Is that for the tests when the spawn helper stuff is disabled?
When running the catview tests, I was having tracebacks about this, apparently
_update_
> This looks like its not really needed for the pkg:
> 735 + pkg.banner_url = ''
> 736 + pkg.title_
> or am I misreading this?
This is needed since the _HtmlRenderer in exhibits.py is calling the following fragment:
144 html = EXHIBIT_HTML % {
145 'banner_url': self.exhibit.
146 'title': self.exhibit.
147 'subtitle': "",
148 }
and since I'm passing a Mock, I need to mimic those attributes.
> One thing that is maybe nice to test is a list with a existing and a non-
> existing pkgname.
Adding a test, sounds great.
> While looking at the test_catview.py I noticed quite a bit of older code that
> wasn't ideal so I took the
> liberty to do some cleanup in lp:~mvo/software-center/test-catview-cleanup
> that is based on your branch.
> I will propose this for merging.
Sounds even better!
- 3028. By Natalia Bidart
-
Merged 5.2 in.
- 3029. By Natalia Bidart
-
Adding tweaks from the review.
Michael Vogt (mvo) wrote : | # |
Thanks for this update and for addressing the review points so quickly!
On Tue, May 22, 2012 at 12:57:20PM -0000, Natalia Bidart wrote:
> > - what do you think about moving "_pkg_available()" out into a more generic
> > place like db/utils.py ?
>
> (09:42:04 AM) nessita: mvo: is there any better/simpler way of knowing if a given pkg is available, without creating an Application? Like, can the StoreDatabase know that?
>
> (otherwise we need to pass the db to the helper and that sounds, at least to me, a bit dirty)
I think putting it into the DB is the ideal way forward, its fine to
land it as it is IMO, we can create a seperate branch that moves it
into the DB.
> > Why is
> > 281 + if self.top_rated is None:
> > 282 + return
> > needed? Is that for the tests when the spawn helper stuff is disabled?
>
> When running the catview tests, I was having tracebacks about this, apparently
> _update_
Looking at the code I think this happens because "self.build(
is called in LobbyViewGtk after the db reopen signal is connected and
the refresh-
"self.build(
please check if that fixes the crash?
> > This looks like its not really needed for the pkg:
> > 735 + pkg.banner_url = ''
> > 736 + pkg.title_
> > or am I misreading this?
>
> This is needed since the _HtmlRenderer in exhibits.py is calling the following fragment:
>
> 144 html = EXHIBIT_HTML % {
> 145 'banner_url': self.exhibit.
> 146 'title': self.exhibit.
> 147 'subtitle': "",
> 148 }
>
> and since I'm passing a Mock, I need to mimic those attributes.
Right, what I'm puzzled about is that those are parts of the "pkg"
Mock in line 735 not the "exhibit" Mock.
Thanks!
Michael
- 3030. By Natalia Bidart
-
Moving the package availability calculation to StoreDatabase.
- 3031. By Natalia Bidart
-
More cleanup after review comments.
Natalia Bidart (nataliabidart) wrote : | # |
> > > Why is
> > > 281 + if self.top_rated is None:
> > > 282 + return
> > > needed? Is that for the tests when the spawn helper stuff is disabled?
> >
> > When running the catview tests, I was having tracebacks about this,
> apparently
> > _update_
> was set, so I added that workaround.
>
> Looking at the code I think this happens because "self.build(
> is called in LobbyViewGtk after the db reopen signal is connected and
> the refresh-
> "self.build(
> please check if that fixes the crash?
It does! Pushing that.
> > > This looks like its not really needed for the pkg:
> > > 735 + pkg.banner_url = ''
> > > 736 + pkg.title_
> > > or am I misreading this?
> >
> > This is needed since the _HtmlRenderer in exhibits.py is calling the
> following fragment:
> >
> > 144 html = EXHIBIT_HTML % {
> > 145 'banner_url': self.exhibit.
> > 146 'title': self.exhibit.
> > 147 'subtitle': "",
> > 148 }
> >
> > and since I'm passing a Mock, I need to mimic those attributes.
>
> Right, what I'm puzzled about is that those are parts of the "pkg"
> Mock in line 735 not the "exhibit" Mock.
Ah, good point. I debugged a bit more and found out, like you predicted, that setting that to the pkd was indeed unnecessary (the output confused me before due to the exceptions being printed out-of-order, perhaps there are threads involved?). I removed it.
- 3032. By Natalia Bidart
-
PyFlakes fixes.
Michael Vogt (mvo) wrote : | # |
Thanks for this update and for moving the code into the DB, even nicer now. I will merge this now and it will be part of the next SRU.
Michael Vogt (mvo) wrote : | # |
One thought for later that I just had was that it would be nice to have the code react to the db-reopen signal and refilter the exhibits again to update once there are new apps from e.g. the software-
Michael Vogt (mvo) wrote : | # |
I just noticed one regression that is a bit of a client/server interaction issue.
For the Ubuntu books exhibit the server sends:
note the \r\n at the end, so we need to either fix the server or do a strip() in the packagename.
Michael Vogt (mvo) wrote : | # |
I filed bug #1004417 about this.
Preview Diff
1 | === modified file 'softwarecenter/backend/channel.py' |
2 | --- softwarecenter/backend/channel.py 2012-03-19 14:20:55 +0000 |
3 | +++ softwarecenter/backend/channel.py 2012-05-22 14:21:21 +0000 |
4 | @@ -22,7 +22,7 @@ |
5 | |
6 | from gettext import gettext as _ |
7 | |
8 | -from softwarecenter.distro import get_distro |
9 | +import softwarecenter.distro |
10 | |
11 | from softwarecenter.enums import (SortMethods, |
12 | Icons, |
13 | @@ -34,7 +34,7 @@ |
14 | |
15 | class ChannelsManager(object): |
16 | def __init__(self, db, **kwargs): |
17 | - self.distro = get_distro() |
18 | + self.distro = softwarecenter.distro.get_distro() |
19 | self.db = db |
20 | |
21 | # public |
22 | @@ -154,7 +154,7 @@ |
23 | self.installed_only = installed_only |
24 | self._channel_sort_mode = channel_sort_mode |
25 | # distro specific stuff |
26 | - self.distro = get_distro() |
27 | + self.distro = softwarecenter.distro.get_distro() |
28 | # configure the channel |
29 | self._channel_display_name = self._get_display_name_for_channel( |
30 | channel_name, channel_origin, channel_component) |
31 | @@ -351,7 +351,7 @@ |
32 | return AptChannelsManager.channel_available(channelname) |
33 | |
34 | if __name__ == "__main__": |
35 | - distro = get_distro() |
36 | + distro = softwarecenter.distro.get_distro() |
37 | channel = SoftwareChannel(distro.get_distro_channel_name(), |
38 | None, None) |
39 | print(channel) |
40 | |
41 | === modified file 'softwarecenter/db/application.py' |
42 | --- softwarecenter/db/application.py 2012-05-11 13:48:04 +0000 |
43 | +++ softwarecenter/db/application.py 2012-05-22 14:21:21 +0000 |
44 | @@ -24,9 +24,10 @@ |
45 | import os |
46 | import re |
47 | |
48 | +import softwarecenter.distro |
49 | + |
50 | from gettext import gettext as _ |
51 | from softwarecenter.backend.channel import is_channel_available |
52 | -from softwarecenter.distro import get_distro |
53 | from softwarecenter.enums import PkgStates, XapianValues, Icons |
54 | |
55 | from softwarecenter.paths import (APP_INSTALL_CHANNELS_PATH, |
56 | @@ -177,7 +178,7 @@ |
57 | self._db = db |
58 | self._db.connect("reopen", self._on_db_reopen) |
59 | self._cache = self._db._aptcache |
60 | - self._distro = get_distro() |
61 | + self._distro = softwarecenter.distro.get_distro() |
62 | self._history = None |
63 | # import here (intead of global) to avoid dbus dependency |
64 | # in update-software-center (that imports application, but |
65 | @@ -298,7 +299,7 @@ |
66 | # try apt first |
67 | if self._pkg: |
68 | for origin in self._pkg.candidate.origins: |
69 | - if (origin.origin == get_distro().get_distro_channel_name() and |
70 | + if (origin.origin == self._distro.get_distro_channel_name() and |
71 | origin.trusted and origin.component): |
72 | return origin.component |
73 | # then xapian |
74 | @@ -639,7 +640,7 @@ |
75 | self.emit("screenshots-available", self._screenshot_list) |
76 | return |
77 | # download it |
78 | - distro = get_distro() |
79 | + distro = self._distro |
80 | url = distro.SCREENSHOT_JSON_URL % self._app.pkgname |
81 | try: |
82 | f = Gio.File.new_for_uri(url) |
83 | |
84 | === modified file 'softwarecenter/db/database.py' |
85 | --- softwarecenter/db/database.py 2012-05-03 20:27:00 +0000 |
86 | +++ softwarecenter/db/database.py 2012-05-22 14:21:21 +0000 |
87 | @@ -493,9 +493,10 @@ |
88 | return popcon |
89 | |
90 | def get_xapian_document(self, appname, pkgname): |
91 | - """ Get the machting xapian document for appname, pkgname |
92 | - |
93 | - If no document is found, raise a IndexError |
94 | + """Get the machting xapian document for appname, pkgname. |
95 | + |
96 | + If no document is found, raise a IndexError. |
97 | + |
98 | """ |
99 | #LOG.debug("get_xapian_document app='%s' pkg='%s'" % (appname, |
100 | # pkgname)) |
101 | @@ -517,9 +518,23 @@ |
102 | raise IndexError("No app '%s' for '%s' in database" % (appname, |
103 | pkgname)) |
104 | |
105 | + def is_pkgname_known(self, pkgname): |
106 | + """Check if 'pkgname' is known to this database. |
107 | + |
108 | + Note that even if this function returns True, it may mean that the |
109 | + package needs to be purchased first or is available in a |
110 | + not-yet-enabled source. |
111 | + |
112 | + """ |
113 | + # check cache first, then our own database |
114 | + return (pkgname in self._aptcache or |
115 | + any(self.xapiandb.postlist("AP" + pkgname))) |
116 | + |
117 | def is_appname_duplicated(self, appname): |
118 | - """Check if the given appname is stored multiple times in the db |
119 | - This can happen for generic names like "Terminal" |
120 | + """Check if the given appname is stored multiple times in the db. |
121 | + |
122 | + This can happen for generic names like "Terminal". |
123 | + |
124 | """ |
125 | for (i, m) in enumerate(self.xapiandb.postlist("AA" + appname)): |
126 | if i > 0: |
127 | |
128 | === modified file 'softwarecenter/testutils.py' |
129 | --- softwarecenter/testutils.py 2012-04-16 16:41:29 +0000 |
130 | +++ softwarecenter/testutils.py 2012-05-22 14:21:21 +0000 |
131 | @@ -22,6 +22,8 @@ |
132 | import tempfile |
133 | import time |
134 | |
135 | +from collections import defaultdict |
136 | + |
137 | from mock import Mock |
138 | |
139 | m_dbus = m_polkit = m_aptd = None |
140 | @@ -314,3 +316,57 @@ |
141 | {u'rating': 1.5, u'package_name': u'tucan'}], |
142 | u'app': u'pitivi'} |
143 | return recommend_app_data |
144 | + |
145 | + |
146 | +class ObjectWithSignals(object): |
147 | + """A faked object that you can connect to and emit signals.""" |
148 | + |
149 | + def __init__(self, *a, **kw): |
150 | + super(ObjectWithSignals, self).__init__() |
151 | + self._callbacks = defaultdict(list) |
152 | + |
153 | + def connect(self, signal, callback): |
154 | + """Connect a signal with a callback.""" |
155 | + self._callbacks[signal].append(callback) |
156 | + |
157 | + def disconnect(self, signal, callback): |
158 | + """Connect a signal with a callback.""" |
159 | + self._callbacks[signal].remove(callback) |
160 | + if len(self._callbacks[signal]) == 0: |
161 | + self._callbacks.pop(signal) |
162 | + |
163 | + def disconnect_by_func(self, callback): |
164 | + """Disconnect 'callback' from every signal.""" |
165 | + # do not use iteritems since we may change the dict inside the for |
166 | + for signal, callbacks in self._callbacks.items(): |
167 | + if callback in callbacks: |
168 | + self.disconnect(signal, callback) |
169 | + |
170 | + def emit(self, signal, *args, **kwargs): |
171 | + """Emit 'signal' passing *args, **kwargs to every callback.""" |
172 | + for callback in self._callbacks[signal]: |
173 | + callback(*args, **kwargs) |
174 | + |
175 | + |
176 | +class FakedCache(ObjectWithSignals, dict): |
177 | + """A faked cache.""" |
178 | + |
179 | + def __init__(self, *a, **kw): |
180 | + super(FakedCache, self).__init__() |
181 | + self.ready = False |
182 | + |
183 | + def open(self): |
184 | + """Open this cache.""" |
185 | + self.ready = True |
186 | + |
187 | + def component_available(self, distro_codename, component): |
188 | + """Return whether 'component' is available in 'distro_codename'.""" |
189 | + |
190 | + def get_addons(self, pkgname): |
191 | + """Return (recommended, suggested) addons for 'pkgname'.""" |
192 | + return ([], []) |
193 | + |
194 | + def get_total_size_on_install(self, pkgname, addons_to_install, |
195 | + addons_to_remove, archive_suite): |
196 | + """Return a fake (total_download_size, total_install_size) result.""" |
197 | + return (0, 0) |
198 | |
199 | === modified file 'softwarecenter/ui/gtk3/models/appstore2.py' |
200 | --- softwarecenter/ui/gtk3/models/appstore2.py 2012-05-22 12:19:51 +0000 |
201 | +++ softwarecenter/ui/gtk3/models/appstore2.py 2012-05-22 14:21:21 +0000 |
202 | @@ -115,9 +115,7 @@ |
203 | self.icons = icons |
204 | self.icon_size = icon_size |
205 | |
206 | - # cache the 'missing icon' used in the treeview for apps without an |
207 | - # icon |
208 | - self._missing_icon = icons.load_icon(Icons.MISSING_APP, icon_size, 0) |
209 | + self._missing_icon = None # delay this until actually needed |
210 | if global_icon_cache: |
211 | self.icon_cache = _app_icon_cache |
212 | else: |
213 | @@ -146,6 +144,14 @@ |
214 | on_image_download_complete, pkgname) |
215 | image_downloader.download_file(url, icon_file_path) |
216 | |
217 | + @property |
218 | + def missing_icon(self): |
219 | + # cache the 'missing icon' used in treeviews for apps without an icon |
220 | + if self._missing_icon is None: |
221 | + self._missing_icon = self.icons.load_icon(Icons.MISSING_APP, |
222 | + self.icon_size, 0) |
223 | + return self._missing_icon |
224 | + |
225 | def update_availability(self, doc): |
226 | doc.available = None |
227 | doc.installed = None |
228 | @@ -228,10 +234,10 @@ |
229 | self.get_pkgname(doc), |
230 | full_icon_file_name) |
231 | # display the missing icon while the real one downloads |
232 | - self.icon_cache[icon_name] = self._missing_icon |
233 | + self.icon_cache[icon_name] = self.missing_icon |
234 | except GObject.GError as e: |
235 | LOG.debug("get_icon returned '%s'" % e) |
236 | - return self._missing_icon |
237 | + return self.missing_icon |
238 | |
239 | def get_review_stats(self, doc): |
240 | return self.review_loader.get_review_stats(self.get_application(doc)) |
241 | |
242 | === modified file 'softwarecenter/ui/gtk3/views/catview_gtk.py' |
243 | --- softwarecenter/ui/gtk3/views/catview_gtk.py 2012-03-16 16:52:54 +0000 |
244 | +++ softwarecenter/ui/gtk3/views/catview_gtk.py 2012-05-22 14:21:21 +0000 |
245 | @@ -28,9 +28,11 @@ |
246 | |
247 | import softwarecenter.paths |
248 | from softwarecenter.db.application import Application |
249 | -from softwarecenter.enums import (NonAppVisibility, |
250 | - SortMethods, |
251 | - TOP_RATED_CAROUSEL_LIMIT) |
252 | +from softwarecenter.enums import ( |
253 | + NonAppVisibility, |
254 | + SortMethods, |
255 | + TOP_RATED_CAROUSEL_LIMIT, |
256 | +) |
257 | from softwarecenter.utils import wait_for_apt_cache_ready |
258 | from softwarecenter.ui.gtk3.models.appstore2 import AppPropertiesHelper |
259 | from softwarecenter.ui.gtk3.widgets.viewport import Viewport |
260 | @@ -224,11 +226,15 @@ |
261 | apps_filter, apps_limit=0): |
262 | CategoriesViewGtk.__init__(self, datadir, desktopdir, cache, db, icons, |
263 | apps_filter, apps_limit=0) |
264 | + self.top_rated = None |
265 | + self.exhibit_banner = None |
266 | |
267 | # sections |
268 | self.departments = None |
269 | self.appcount = None |
270 | |
271 | + self.build(desktopdir) |
272 | + |
273 | # ensure that on db-reopen we refresh the whats-new titles |
274 | self.db.connect("reopen", self._on_db_reopen) |
275 | |
276 | @@ -237,9 +243,6 @@ |
277 | self.reviews_loader.connect( |
278 | "refresh-review-stats-finished", self._on_refresh_review_stats) |
279 | |
280 | - self.build(desktopdir) |
281 | - return |
282 | - |
283 | def _on_db_reopen(self, db): |
284 | self._update_whats_new_content() |
285 | |
286 | @@ -270,7 +273,6 @@ |
287 | |
288 | #self._append_video_clips() |
289 | #self._append_top_of_the_pops |
290 | - return |
291 | |
292 | #~ def _append_top_of_the_pops(self): |
293 | #~ self.totp_hbox = Gtk.HBox(spacing=self.SPACING) |
294 | @@ -337,24 +339,34 @@ |
295 | flags=['nonapps-visible']) |
296 | self.emit("category-selected", cat) |
297 | |
298 | + def _filter_and_set_exhibits(self, sca_client, exhibit_list): |
299 | + result = [] |
300 | + # filter out those exhibits that are not available in this run |
301 | + for exhibit in exhibit_list: |
302 | + available = all(self.db.is_pkgname_known(p) for p in |
303 | + exhibit.package_names.split(',')) |
304 | + if available: |
305 | + result.append(exhibit) |
306 | + |
307 | + # its ok if result is empty, since set_exhibits() will ignore |
308 | + # empty lists |
309 | + self.exhibit_banner.set_exhibits(result) |
310 | + |
311 | def _append_banner_ads(self): |
312 | - exhibit_banner = ExhibitBanner() |
313 | - exhibit_banner.set_exhibits([FeaturedExhibit(), |
314 | - ]) |
315 | - exhibit_banner.connect("show-exhibits-clicked", self._on_show_exhibits) |
316 | + self.exhibit_banner = ExhibitBanner() |
317 | + self.exhibit_banner.set_exhibits([FeaturedExhibit()]) |
318 | + self.exhibit_banner.connect( |
319 | + "show-exhibits-clicked", self._on_show_exhibits) |
320 | |
321 | # query using the agent |
322 | scagent = SoftwareCenterAgent() |
323 | - scagent.connect( |
324 | - "exhibits", lambda sca, l: exhibit_banner.set_exhibits(l)) |
325 | + scagent.connect("exhibits", self._filter_and_set_exhibits) |
326 | scagent.query_exhibits() |
327 | |
328 | a = Gtk.Alignment() |
329 | a.set_padding(0, StockEms.SMALL, 0, 0) |
330 | - a.add(exhibit_banner) |
331 | - |
332 | + a.add(self.exhibit_banner) |
333 | self.vbox.pack_start(a, False, False, 0) |
334 | - return |
335 | |
336 | def _append_departments(self): |
337 | # set the departments section to use the label markup we have just |
338 | |
339 | === modified file 'test/gtk3/test_app.py' |
340 | --- test/gtk3/test_app.py 2012-05-16 15:52:07 +0000 |
341 | +++ test/gtk3/test_app.py 2012-05-22 14:21:21 +0000 |
342 | @@ -8,7 +8,7 @@ |
343 | |
344 | from mock import Mock |
345 | |
346 | -from testutils import get_mock_options, setup_test_env |
347 | +from testutils import FakedCache, get_mock_options, setup_test_env |
348 | setup_test_env() |
349 | |
350 | import softwarecenter.paths |
351 | @@ -17,40 +17,6 @@ |
352 | from softwarecenter.ui.gtk3 import app |
353 | |
354 | |
355 | -class FakedCache(dict): |
356 | - """A faked cache.""" |
357 | - |
358 | - def __init__(self, *a, **kw): |
359 | - super(FakedCache, self).__init__() |
360 | - self._callbacks = defaultdict(list) |
361 | - self.ready = False |
362 | - |
363 | - def open(self): |
364 | - """Open this cache.""" |
365 | - self.ready = True |
366 | - |
367 | - def connect(self, signal, callback): |
368 | - """Connect a signal with a callback.""" |
369 | - self._callbacks[signal].append(callback) |
370 | - |
371 | - def disconnect_by_func(self, callback): |
372 | - """Disconnect 'callback' from every signal.""" |
373 | - for signal, cb in self._callbacks.iteritems(): |
374 | - if cb == callback: |
375 | - self._callbacks[signal].remove(callback) |
376 | - if len(self._callbacks[signal]) == 0: |
377 | - self._callbacks.pop(signal) |
378 | - |
379 | - def get_addons(self, pkgname): |
380 | - """Return (recommended, suggested) addons for 'pkgname'.""" |
381 | - return ([],[]) |
382 | - |
383 | - def get_total_size_on_install(self,pkgname, addons_to_install, |
384 | - addons_to_remove, archive_suite): |
385 | - """Return a fake (total_download_size, total_install_size) result.""" |
386 | - return (0, 0) |
387 | - |
388 | - |
389 | class ParsePackagesArgsTestCase(unittest.TestCase): |
390 | """Test suite for the parse_packages_args helper.""" |
391 | |
392 | @@ -109,7 +75,7 @@ |
393 | fname = __file__ |
394 | assert os.path.exists(fname) |
395 | self.assertRaises(DebFileOpenError, app.parse_packages_args, fname) |
396 | - |
397 | + |
398 | |
399 | class ParsePackagesWithAptPrefixTestCase(ParsePackagesArgsTestCase): |
400 | |
401 | |
402 | === modified file 'test/gtk3/test_catview.py' |
403 | --- test/gtk3/test_catview.py 2012-03-15 22:23:21 +0000 |
404 | +++ test/gtk3/test_catview.py 2012-05-22 14:21:21 +0000 |
405 | @@ -1,28 +1,42 @@ |
406 | -from gi.repository import Gtk |
407 | import time |
408 | import unittest |
409 | + |
410 | +from gi.repository import Gtk |
411 | from mock import patch, Mock |
412 | |
413 | from testutils import setup_test_env |
414 | setup_test_env() |
415 | |
416 | +import softwarecenter.distro |
417 | +import softwarecenter.paths |
418 | + |
419 | +from softwarecenter.db.database import StoreDatabase |
420 | from softwarecenter.enums import SortMethods |
421 | -from softwarecenter.testutils import (get_test_db, |
422 | - make_recommender_agent_recommend_me_dict) |
423 | +from softwarecenter.testutils import ( |
424 | + FakedCache, |
425 | + get_test_db, |
426 | + make_recommender_agent_recommend_me_dict, |
427 | + ObjectWithSignals, |
428 | +) |
429 | +from softwarecenter.ui.gtk3.views import catview_gtk |
430 | +from softwarecenter.ui.gtk3.views.catview_gtk import get_test_window_catview |
431 | +from softwarecenter.ui.gtk3.widgets.containers import FramedHeaderBox |
432 | +from softwarecenter.ui.gtk3.widgets.spinner import SpinnerNotebook |
433 | + |
434 | |
435 | class TestCatView(unittest.TestCase): |
436 | |
437 | def setUp(self): |
438 | + self._cat = None |
439 | self.db = get_test_db() |
440 | |
441 | def _on_category_selected(self, subcatview, category): |
442 | - #print "**************", subcatview, category |
443 | self._cat = category |
444 | - |
445 | - def test_subcatview_top_rated(self): |
446 | - from softwarecenter.ui.gtk3.views.catview_gtk import get_test_window_catview |
447 | + |
448 | + def test_top_rated(self): |
449 | # get the widgets we need |
450 | win = get_test_window_catview() |
451 | + self.addCleanup(win.destroy) |
452 | lobby = win.get_data("lobby") |
453 | |
454 | # simulate review-stats refresh |
455 | @@ -37,12 +51,11 @@ |
456 | self.assertNotEqual(self._cat, None) |
457 | self.assertEqual(self._cat.name, "Top Rated") |
458 | self.assertEqual(self._cat.sortmode, SortMethods.BY_TOP_RATED) |
459 | - win.destroy() |
460 | |
461 | - def test_subcatview_new(self): |
462 | - from softwarecenter.ui.gtk3.views.catview_gtk import get_test_window_catview |
463 | + def test_new(self): |
464 | # get the widgets we need |
465 | win = get_test_window_catview() |
466 | + self.addCleanup(win.destroy) |
467 | lobby = win.get_data("lobby") |
468 | |
469 | # test db reopen triggers whats-new update |
470 | @@ -58,9 +71,8 @@ |
471 | # encoding is utf-8 (since r2218, see category.py) |
472 | self.assertEqual(self._cat.name, 'What\xe2\x80\x99s New') |
473 | self.assertEqual(self._cat.sortmode, SortMethods.BY_CATALOGED_TIME) |
474 | - win.destroy() |
475 | |
476 | - def test_subcatview_new_no_sort_info_yet(self): |
477 | + def test_new_no_sort_info_yet(self): |
478 | # ensure that we don't show a empty "whats new" category |
479 | # see LP: #865985 |
480 | from softwarecenter.testutils import get_test_db |
481 | @@ -68,7 +80,7 @@ |
482 | cache = db._aptcache |
483 | # simulate a fresh install with no catalogedtime info |
484 | del db._axi_values["catalogedtime"] |
485 | - |
486 | + |
487 | from softwarecenter.testutils import get_test_gtk3_icon_cache |
488 | icons = get_test_gtk3_icon_cache() |
489 | |
490 | @@ -76,15 +88,15 @@ |
491 | apps_filter = AppFilter(db, cache) |
492 | |
493 | from softwarecenter.distro import get_distro |
494 | - import softwarecenter.paths |
495 | - from softwarecenter.paths import APP_INSTALL_PATH |
496 | from softwarecenter.ui.gtk3.views.catview_gtk import LobbyViewGtk |
497 | - view = LobbyViewGtk(softwarecenter.paths.datadir, APP_INSTALL_PATH, |
498 | + view = LobbyViewGtk(softwarecenter.paths.datadir, |
499 | + softwarecenter.paths.APP_INSTALL_PATH, |
500 | cache, db, icons, get_distro(), apps_filter) |
501 | view.show() |
502 | |
503 | # gui |
504 | win = Gtk.Window() |
505 | + self.addCleanup(win.destroy) |
506 | win.set_size_request(800, 400) |
507 | |
508 | scroll = Gtk.ScrolledWindow() |
509 | @@ -96,82 +108,85 @@ |
510 | self._p() |
511 | self.assertFalse(view.whats_new_frame.get_property("visible")) |
512 | self._p() |
513 | - win.destroy() |
514 | |
515 | - def test_subcatview_recommended_for_you_opt_in_display(self): |
516 | - |
517 | - # patch the recommender UUID value to insure that we are not opted-in for this test |
518 | - get_recommender_opted_in_patcher = patch('softwarecenter.backend.recagent.RecommenderAgent.is_opted_in') |
519 | - self.addCleanup(get_recommender_opted_in_patcher.stop) |
520 | - mock_get_recommender_opted_in = get_recommender_opted_in_patcher.start() |
521 | + def test_recommended_for_you_opt_in_display(self): |
522 | + # patch the recommender UUID value to ensure that we are not opted-in |
523 | + # for this test |
524 | + recommender_opted_in_patcher = patch( |
525 | + 'softwarecenter.backend.recagent.RecommenderAgent.is_opted_in') |
526 | + self.addCleanup(recommender_opted_in_patcher.stop) |
527 | + mock_get_recommender_opted_in = recommender_opted_in_patcher.start() |
528 | mock_get_recommender_opted_in.return_value = False |
529 | - |
530 | - from softwarecenter.ui.gtk3.views.catview_gtk import get_test_window_catview |
531 | + |
532 | # get the widgets we need |
533 | win = get_test_window_catview() |
534 | + self.addCleanup(win.destroy) |
535 | + |
536 | lobby = win.get_data("lobby") |
537 | rec_panel = lobby.recommended_for_you_panel |
538 | self._p() |
539 | - from softwarecenter.ui.gtk3.widgets.containers import FramedHeaderBox |
540 | - self.assertTrue(rec_panel.spinner_notebook.get_current_page() == FramedHeaderBox.CONTENT) |
541 | + self.assertEqual(rec_panel.spinner_notebook.get_current_page(), |
542 | + FramedHeaderBox.CONTENT) |
543 | self.assertTrue(rec_panel.opt_in_vbox.get_property("visible")) |
544 | - win.destroy() |
545 | - |
546 | + |
547 | # patch out the agent query method to avoid making the actual server call |
548 | @patch('softwarecenter.backend.recagent.RecommenderAgent' |
549 | '.post_submit_profile') |
550 | - def test_subcatview_recommended_for_you_spinner_display(self, mock_query): |
551 | - |
552 | - # patch the recommender UUID value to insure that we are not opted-in for this test |
553 | - get_recommender_opted_in_patcher = patch('softwarecenter.backend.recagent.RecommenderAgent.is_opted_in') |
554 | - self.addCleanup(get_recommender_opted_in_patcher.stop) |
555 | - mock_get_recommender_opted_in = get_recommender_opted_in_patcher.start() |
556 | + def test_recommended_for_you_spinner_display(self, mock_query): |
557 | + # patch the recommender UUID value to insure that we are not opted-in |
558 | + # for this test |
559 | + recommender_opted_in_patcher = patch( |
560 | + 'softwarecenter.backend.recagent.RecommenderAgent.is_opted_in') |
561 | + self.addCleanup(recommender_opted_in_patcher.stop) |
562 | + mock_get_recommender_opted_in = recommender_opted_in_patcher.start() |
563 | mock_get_recommender_opted_in.return_value = False |
564 | - |
565 | - from softwarecenter.ui.gtk3.views.catview_gtk import get_test_window_catview |
566 | + |
567 | # get the widgets we need |
568 | win = get_test_window_catview() |
569 | + self.addCleanup(win.destroy) |
570 | lobby = win.get_data("lobby") |
571 | rec_panel = lobby.recommended_for_you_panel |
572 | self._p() |
573 | - # click the opt-in button to initiate the process, this will show the spinner |
574 | + # click the opt-in button to initiate the process, |
575 | + # this will show the spinner |
576 | rec_panel.opt_in_button.emit('clicked') |
577 | self._p() |
578 | - from softwarecenter.ui.gtk3.widgets.spinner import SpinnerNotebook |
579 | - self.assertTrue(rec_panel.spinner_notebook.get_current_page() == SpinnerNotebook.SPINNER_PAGE) |
580 | + self.assertEqual(rec_panel.spinner_notebook.get_current_page(), |
581 | + SpinnerNotebook.SPINNER_PAGE) |
582 | self.assertTrue(rec_panel.opt_in_vbox.get_property("visible")) |
583 | - win.destroy() |
584 | |
585 | # patch out the agent query method to avoid making the actual server call |
586 | @patch('softwarecenter.backend.recagent.RecommenderAgent' |
587 | '.post_submit_profile') |
588 | - def test_subcatview_recommended_for_you_display_recommendations(self, mock_query): |
589 | - |
590 | - # patch the recommender UUID value to insure that we are not opted-in for this test |
591 | - get_recommender_opted_in_patcher = patch('softwarecenter.backend.recagent.RecommenderAgent.is_opted_in') |
592 | - self.addCleanup(get_recommender_opted_in_patcher.stop) |
593 | - mock_get_recommender_opted_in = get_recommender_opted_in_patcher.start() |
594 | + def test_recommended_for_you_display_recommendations(self, |
595 | + mock_query): |
596 | + # patch the recommender UUID value to insure that we are not opted-in |
597 | + # for this test |
598 | + recommender_opted_in_patcher = patch( |
599 | + 'softwarecenter.backend.recagent.RecommenderAgent.is_opted_in') |
600 | + self.addCleanup(recommender_opted_in_patcher.stop) |
601 | + mock_get_recommender_opted_in = recommender_opted_in_patcher.start() |
602 | mock_get_recommender_opted_in.return_value = False |
603 | - |
604 | - from softwarecenter.ui.gtk3.views.catview_gtk import get_test_window_catview |
605 | + |
606 | # get the widgets we need |
607 | win = get_test_window_catview() |
608 | + self.addCleanup(win.destroy) |
609 | lobby = win.get_data("lobby") |
610 | rec_panel = lobby.recommended_for_you_panel |
611 | self._p() |
612 | - # click the opt-in button to initiate the process, this will show the spinner |
613 | + # click the opt-in button to initiate the process, |
614 | + # this will show the spinner |
615 | rec_panel.opt_in_button.emit('clicked') |
616 | self._p() |
617 | rec_panel._update_recommended_for_you_content() |
618 | self._p() |
619 | # we fake the callback from the agent here |
620 | - lobby.recommended_for_you_panel.recommended_for_you_cat._recommend_me_result( |
621 | - None, |
622 | - make_recommender_agent_recommend_me_dict()) |
623 | - self.assertNotEqual( |
624 | - lobby.recommended_for_you_panel.recommended_for_you_cat.get_documents(self.db), []) |
625 | - from softwarecenter.ui.gtk3.widgets.spinner import SpinnerNotebook |
626 | - self.assertTrue(rec_panel.spinner_notebook.get_current_page() == SpinnerNotebook.CONTENT_PAGE) |
627 | + for_you = lobby.recommended_for_you_panel.recommended_for_you_cat |
628 | + for_you._recommend_me_result(None, |
629 | + make_recommender_agent_recommend_me_dict()) |
630 | + self.assertNotEqual(for_you.get_documents(self.db), []) |
631 | + self.assertEqual(rec_panel.spinner_notebook.get_current_page(), |
632 | + SpinnerNotebook.CONTENT_PAGE) |
633 | self._p() |
634 | # test clicking recommended_for_you More button |
635 | lobby.connect("category-selected", self._on_category_selected) |
636 | @@ -179,49 +194,51 @@ |
637 | self._p() |
638 | self.assertNotEqual(self._cat, None) |
639 | self.assertEqual(self._cat.name, "Recommended For You") |
640 | - win.destroy() |
641 | - |
642 | + |
643 | # patch out the agent query method to avoid making the actual server call |
644 | @patch('softwarecenter.backend.recagent.RecommenderAgent' |
645 | '.query_recommend_me') |
646 | - def test_subcatview_recommended_for_you_display_recommendations_not_opted_in(self, mock_query): |
647 | - |
648 | - # patch the recommender UUID value to insure that we are not opted-in for this test |
649 | - get_recommender_opted_in_patcher = patch('softwarecenter.backend.recagent.RecommenderAgent.is_opted_in') |
650 | - self.addCleanup(get_recommender_opted_in_patcher.stop) |
651 | - mock_get_recommender_opted_in = get_recommender_opted_in_patcher.start() |
652 | + def test_recommended_for_you_display_recommendations_not_opted_in(self, |
653 | + mock_query): |
654 | + # patch the recommender UUID value to insure that we are not opted-in |
655 | + # for this test |
656 | + recommender_opted_in_patcher = patch( |
657 | + 'softwarecenter.backend.recagent.RecommenderAgent.is_opted_in') |
658 | + self.addCleanup(recommender_opted_in_patcher.stop) |
659 | + mock_get_recommender_opted_in = recommender_opted_in_patcher.start() |
660 | mock_get_recommender_opted_in.return_value = False |
661 | - |
662 | - from softwarecenter.ui.gtk3.views.catview_gtk import get_test_window_catview |
663 | + |
664 | # get the widgets we need |
665 | win = get_test_window_catview() |
666 | + self.addCleanup(win.destroy) |
667 | # we want to work in the "subcat" view |
668 | notebook = win.get_child() |
669 | notebook.next_page() |
670 | - |
671 | + |
672 | subcat_view = win.get_data("subcat") |
673 | self._p() |
674 | - self.assertFalse(subcat_view.recommended_for_you_in_cat.get_property("visible")) |
675 | - win.destroy() |
676 | - |
677 | + self.assertFalse(subcat_view.recommended_for_you_in_cat.get_property( |
678 | + "visible")) |
679 | + |
680 | # patch out the agent query method to avoid making the actual server call |
681 | @patch('softwarecenter.backend.recagent.RecommenderAgent' |
682 | '.query_recommend_me') |
683 | - def test_subcatview_recommended_for_you_display_recommendations_opted_in(self, mock_query): |
684 | - |
685 | - # patch the recommender UUID value to insure that we are not opted-in for this test |
686 | - get_recommender_opted_in_patcher = patch('softwarecenter.backend.recagent.RecommenderAgent.is_opted_in') |
687 | - self.addCleanup(get_recommender_opted_in_patcher.stop) |
688 | - mock_get_recommender_opted_in = get_recommender_opted_in_patcher.start() |
689 | + def test_recommended_for_you_display_recommendations_opted_in(self, mock_query): |
690 | + # patch the recommender UUID value to insure that we are not opted-in |
691 | + # for this test |
692 | + recommender_opted_in_patcher = patch( |
693 | + 'softwarecenter.backend.recagent.RecommenderAgent.is_opted_in') |
694 | + self.addCleanup(recommender_opted_in_patcher.stop) |
695 | + mock_get_recommender_opted_in = recommender_opted_in_patcher.start() |
696 | mock_get_recommender_opted_in.return_value = True |
697 | - |
698 | - from softwarecenter.ui.gtk3.views.catview_gtk import get_test_window_catview |
699 | + |
700 | # get the widgets we need |
701 | win = get_test_window_catview() |
702 | + self.addCleanup(win.destroy) |
703 | # we want to work in the "subcat" view |
704 | notebook = win.get_child() |
705 | notebook.next_page() |
706 | - |
707 | + |
708 | subcat_view = win.get_data("subcat") |
709 | rec_cat_panel = subcat_view.recommended_for_you_in_cat |
710 | self._p() |
711 | @@ -231,18 +248,21 @@ |
712 | rec_cat_panel.recommended_for_you_cat._recommend_me_result( |
713 | None, |
714 | make_recommender_agent_recommend_me_dict()) |
715 | - result_docs = rec_cat_panel.recommended_for_you_cat.get_documents(self.db) |
716 | + result_docs = rec_cat_panel.recommended_for_you_cat.get_documents( |
717 | + self.db) |
718 | self.assertNotEqual(result_docs, []) |
719 | - # check that we are getting the correct number of results, corresponding |
720 | - # to the following Internet items: |
721 | + # check that we are getting the correct number of results, |
722 | + # corresponding to the following Internet items: |
723 | # Mangler, Midori, Midori Private Browsing, Psi |
724 | self.assertTrue(len(result_docs) == 4) |
725 | - from softwarecenter.ui.gtk3.widgets.spinner import SpinnerNotebook |
726 | - self.assertTrue(rec_cat_panel.spinner_notebook.get_current_page() == SpinnerNotebook.CONTENT_PAGE) |
727 | + self.assertEqual(rec_cat_panel.spinner_notebook.get_current_page(), |
728 | + SpinnerNotebook.CONTENT_PAGE) |
729 | # check that the tiles themselves are visible |
730 | self._p() |
731 | - self.assertTrue(rec_cat_panel.recommended_for_you_content.get_property("visible")) |
732 | - self.assertTrue(rec_cat_panel.recommended_for_you_content.get_children()[0].title.get_property("visible")) |
733 | + self.assertTrue(rec_cat_panel.recommended_for_you_content.get_property( |
734 | + "visible")) |
735 | + self.assertTrue(rec_cat_panel.recommended_for_you_content.get_children( |
736 | + )[0].title.get_property("visible")) |
737 | self._p() |
738 | # test clicking recommended_for_you More button |
739 | subcat_view.connect("category-selected", self._on_category_selected) |
740 | @@ -250,7 +270,6 @@ |
741 | self._p() |
742 | self.assertNotEqual(self._cat, None) |
743 | self.assertEqual(self._cat.name, "Recommended For You in Internet") |
744 | - win.destroy() |
745 | |
746 | def _p(self): |
747 | for i in range(5): |
748 | @@ -259,6 +278,89 @@ |
749 | Gtk.main_iteration() |
750 | |
751 | |
752 | +class ExhibitsTestCase(unittest.TestCase): |
753 | + """The test suite for the exhibits carousel.""" |
754 | + |
755 | + def setUp(self): |
756 | + self.datadir = softwarecenter.paths.datadir |
757 | + self.desktopdir = softwarecenter.paths.APP_INSTALL_PATH |
758 | + self.cache = FakedCache() |
759 | + self.db = StoreDatabase(cache=self.cache) |
760 | + self.lobby = catview_gtk.LobbyViewGtk(datadir=self.datadir, |
761 | + desktopdir=self.desktopdir, cache=self.cache, db=self.db, |
762 | + icons=None, apps_filter=None) |
763 | + self.addCleanup(self.lobby.destroy) |
764 | + |
765 | + def _get_banner_from_lobby(self): |
766 | + return self.lobby.vbox.get_children()[-1].get_child() |
767 | + |
768 | + def test_featured_exhibit_by_default(self): |
769 | + """Show the featured exhibit before querying the remote service.""" |
770 | + self.lobby._append_banner_ads() |
771 | + |
772 | + banner = self._get_banner_from_lobby() |
773 | + self.assertEqual(1, len(banner.exhibits)) |
774 | + self.assertIsInstance(banner.exhibits[0], catview_gtk.FeaturedExhibit) |
775 | + |
776 | + def test_no_exhibit_if_not_available(self): |
777 | + """The exhibit should not be shown if the package is not available.""" |
778 | + exhibit = Mock() |
779 | + exhibit.package_names = u'foobarbaz' |
780 | + |
781 | + sca = ObjectWithSignals() |
782 | + sca.query_exhibits = lambda: sca.emit('exhibits', sca, [exhibit]) |
783 | + |
784 | + with patch.object(catview_gtk, 'SoftwareCenterAgent', lambda: sca): |
785 | + self.lobby._append_banner_ads() |
786 | + |
787 | + banner = self._get_banner_from_lobby() |
788 | + self.assertEqual(1, len(banner.exhibits)) |
789 | + self.assertIsInstance(banner.exhibits[0], catview_gtk.FeaturedExhibit) |
790 | + |
791 | + def test_exhibit_if_available(self): |
792 | + """The exhibit should be shown if the package is available.""" |
793 | + exhibit = Mock() |
794 | + exhibit.package_names = u'foobarbaz' |
795 | + exhibit.banner_url = 'banner' |
796 | + exhibit.title_translated = '' |
797 | + |
798 | + self.cache[u'foobarbaz'] = Mock() |
799 | + |
800 | + sca = ObjectWithSignals() |
801 | + sca.query_exhibits = lambda: sca.emit('exhibits', sca, [exhibit]) |
802 | + |
803 | + with patch.object(catview_gtk, 'SoftwareCenterAgent', lambda: sca): |
804 | + self.lobby._append_banner_ads() |
805 | + |
806 | + banner = self._get_banner_from_lobby() |
807 | + self.assertEqual(1, len(banner.exhibits)) |
808 | + self.assertIs(banner.exhibits[0], exhibit) |
809 | + |
810 | + def test_exhibit_if_mixed_availability(self): |
811 | + """The exhibit should be shown even if some are not available.""" |
812 | + # available exhibit |
813 | + exhibit = Mock() |
814 | + exhibit.package_names = u'foobarbaz' |
815 | + exhibit.banner_url = 'banner' |
816 | + exhibit.title_translated = '' |
817 | + |
818 | + self.cache[u'foobarbaz'] = Mock() |
819 | + |
820 | + # not available exhibit |
821 | + other = Mock() |
822 | + other.package_names = u'not-there' |
823 | + |
824 | + sca = ObjectWithSignals() |
825 | + sca.query_exhibits = lambda: sca.emit('exhibits', sca, |
826 | + [exhibit, other]) |
827 | + |
828 | + with patch.object(catview_gtk, 'SoftwareCenterAgent', lambda: sca): |
829 | + self.lobby._append_banner_ads() |
830 | + |
831 | + banner = self._get_banner_from_lobby() |
832 | + self.assertEqual(1, len(banner.exhibits)) |
833 | + self.assertIs(banner.exhibits[0], exhibit) |
834 | + |
835 | |
836 | if __name__ == "__main__": |
837 | #import logging |
838 | |
839 | === modified file 'test/test_database.py' |
840 | --- test/test_database.py 2012-04-16 17:44:04 +0000 |
841 | +++ test/test_database.py 2012-05-22 14:21:21 +0000 |
842 | @@ -424,7 +424,7 @@ |
843 | self.assertTrue(len(details.tags) > 2) |
844 | |
845 | def test_app_enquire(self): |
846 | - db = StoreDatabase("/var/cache/software-center/xapian", self.cache) |
847 | + db = StoreDatabase(cache=self.cache) |
848 | db.open() |
849 | # test the AppEnquire engine |
850 | enquirer = AppEnquire(self.cache, db) |
851 | @@ -433,6 +433,12 @@ |
852 | self.assertTrue(len(enquirer.get_docids()) > 0) |
853 | # FIXME: test more of the interface |
854 | |
855 | + def test_is_pkgname_known(self): |
856 | + db = StoreDatabase(cache=self.cache) |
857 | + db.open() |
858 | + self.assertTrue(db.is_pkgname_known("apt")) |
859 | + self.assertFalse(db.is_pkgname_known("i+am-not-a-pkg")) |
860 | + |
861 | |
862 | class UtilsTestCase(unittest.TestCase): |
863 |
Thanks for your branch.
This is again a beauty to read and I much appreciate the effort that goes beyond the immediate fix and that
helps making the code and tests better. The ExhibitsTestCase code is a excellent example on a good and
clean test, very nice.
Some questions/comments: and_set_ exhibits( ) should probably have a small comment that its ok if result list is empty and that empty list is set via set_exhibits() as the exhibit banner will ignore empty lists
- what do you think about moving "_pkg_available()" out into a more generic place like db/utils.py ?
- _filter_
Why is
281 + if self.top_rated is None:
282 + return
needed? Is that for the tests when the spawn helper stuff is disabled?
This looks like its not really needed for the pkg: translated = ''
735 + pkg.banner_url = ''
736 + pkg.title_
or am I misreading this?
One thing that is maybe nice to test is a list with a existing and a non-existing pkgname.
But none of the above is a showstopper and I'm happy to approve it.
While looking at the test_catview.py I noticed quite a bit of older code that wasn't ideal so I took the
liberty to do some cleanup in lp:~mvo/software-center/test-catview-cleanup that is based on your branch.
I will propose this for merging.