Merge lp:~nataliabidart/software-center/fix-977931 into lp:software-center
- fix-977931
- Merge into trunk
Status: | Superseded |
---|---|
Proposed branch: | lp:~nataliabidart/software-center/fix-977931 |
Merge into: | lp:software-center |
Diff against target: |
1310 lines (+667/-156) (has conflicts) 21 files modified
README (+1/-0) debian/changelog (+17/-2) softwarecenter/backend/spawn_helper.py (+3/-0) softwarecenter/db/__init__.py (+7/-2) softwarecenter/db/database.py (+4/-0) softwarecenter/db/debfile.py (+38/-21) softwarecenter/enums.py (+6/-0) softwarecenter/testutils.py (+11/-1) softwarecenter/ui/gtk3/SimpleGtkbuilderApp.py (+2/-1) softwarecenter/ui/gtk3/app.py (+162/-93) softwarecenter/ui/gtk3/models/appstore2.py (+1/-0) softwarecenter/ui/gtk3/session/appmanager.py (+5/-0) softwarecenter/ui/gtk3/session/viewmanager.py (+5/-0) softwarecenter/ui/gtk3/views/appdetailsview.py (+2/-2) softwarecenter/ui/gtk3/widgets/reviews.py (+12/-4) softwarecenter/ui/gtk3/widgets/videoplayer.py (+5/-1) test/gtk3/test_app.py (+357/-0) test/gtk3/test_debfile_view.py (+3/-8) test/gtk3/test_purchase.py (+8/-12) test/test_database.py (+8/-3) test/test_debfileapplication.py (+10/-6) Text conflict in debian/changelog |
To merge this branch: | bzr merge lp:~nataliabidart/software-center/fix-977931 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
software-store-developers | Pending | ||
Review via email:
|
This proposal has been superseded by a proposal from 2012-05-16.
Commit message
- Unified package string parsing into a single method that will be used from either the command line arguments, or from the dbus method 'bringToFront'. This way, search will be consistent between all entry points.
- Also added proper test suites for the above.
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Michael Vogt (mvo) wrote : | # |
- 2991. By Natalia Bidart
-
Merged 5.2 in.
* lp:~evfool/software-center/lp987801:
- Only show the version label once for each version in
reviews (LP: #987801)
* lp:~mvo/software-center/fix-lp994632:
- add a dep to ensure we get the correct ubuntu-sso-client- gtk
(LP: #994632)
* lp:~mvo/software-center/proper-error-on-unknown-files:
- show a proper error message when attempting to open unknown
file types (LP: #944868)
* lp:~mvo/software-center/lp921799:
- fix crash when decoding screenshots JSON (LP: #921799)
* lp:~mvo/software-center/lp959612:
- fix crash when selecting the Installed view very quickly
after startup (LP: #959612)[ Ken VanDine ]
* lp:~ken-vandine/software-center/lp_982567:
- Check if the proxy is enabled, if the proxy host is set but not
enabled we shouldn't attempt to use the proxy (LP: #982567)[ Gary Lasker ]
* lp:~gary-lasker/software-center/fix-crash-lp969732:
- need to explicitly declare the needs-refresh signal in the
AppTreeStore class to prevent a crash (LP: #969732)
* lp:~gary-lasker/software-center/fix-crash-lp870822:
- don't crash if we don't get a pkgversion object back
(LP: #870822)
* lp:~gary-lasker/software-center/fix-shutdown-crash-lp996333:
- never crash when writing out the software center config file on
shutdown (LP: #996333)
* lp:~gary-lasker/software-center/fix-makedirs-race-crashes:
- fix crash on race when creating the cache or config directories
(LP: #743003, LP: #621182)
* lp:~gary-lasker/software-center/fix-crash_lp973379:
- ensure that the cache is ready before using the recommender
service (LP: #973379 )
* lp:~gary-lasker/software-center/toolbar-buttons-insensitive-during-startup:
- set the toolbar buttons insensitive for the duration of time that the
lobby panels are initializing (LP: #999486, LP: #994341)
[ Robert Roth ]
* lp:~evfool/software-center/bug532072:
- escape markup for support info and license (LP: #993279)
- ellipsize summary at trailing end (LP: #532072)
* lp:~evfool/software-center/lp626037:
- subtle background color tweak for consistency
between views (LP: #626037)
* lp:~evfool/software-center/lp835005:
- no need for the software-properties dialog to be modal
to software-center (LP: #835005)
* lp:~evfool/software-center/lp839389:
- don't display an "icon not found" image in the Unity
launcher when a report a review window is opened (LP: #839389)
* lp:~evfool/software-center/867588:
- capitalize the first letter of the package summary
(LP: #867588)
* lp:~evfool/software-center/lp876657:
- make sure the search field does not disappear when an
install or remove is in progress (LP: #876657)
* lp:~evfool/software-center/lp987797:
- fix alignment in the reviews part of the appdetails (LP: #987797)
* lp:~evfool/software-center/lp844768:
- show a indeterminate progress bar when software-center waits for
another packagemanager to exit (LP: #844768)
[ Dave Morley ]
* lp:~davmor2/software-center/add-performance:
- test code change only! add memory and cpt stats to the
test script - 2992. By Natalia Bidart
-
Applying changes from review.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Natalia Bidart (nataliabidart) wrote : | # |
Merged the branch suggested by mvo adding a minor tweaks about detetcting .deb files.
- 2993. By Natalia Bidart
-
Merged 5.2 in.
- 2994. By Natalia Bidart
-
Merged 5.2 in.
Unmerged revisions
Preview Diff
1 | === modified file 'README' |
2 | --- README 2012-03-08 18:08:49 +0000 |
3 | +++ README 2012-05-16 15:58:19 +0000 |
4 | @@ -53,6 +53,7 @@ |
5 | SOFTWARE_CENTER_DISTRO_CODENAME - overwrite "lsb_release -c -s" output |
6 | SOFTWARE_CENTER_ARCHITECTURE - overwrite the current architecture |
7 | SOFTWARE_CENTER_NO_SC_AGENT - disable the software-center-agent |
8 | +SOFTWARE_CENTER_DISABLE_SPAWN_HELPER - disable everything that is run via the "SpawnHelper", i.e. recommender-agent, software-center-agent, reviews |
9 | SOFTWARE_CENTER_DEBUG_TABS - show notebook tabs for debugging |
10 | SOFTWARE_CENTER_FORCE_DISABLE_CERTS_CHECK - disables certificates checking in webkit views (for use in test environments) |
11 | SOFTWARE_CENTER_FORCE_NON_SSL - disable SSL (for use in test environments) |
12 | |
13 | === modified file 'debian/changelog' |
14 | --- debian/changelog 2012-05-15 09:23:50 +0000 |
15 | +++ debian/changelog 2012-05-16 15:58:19 +0000 |
16 | @@ -1,3 +1,4 @@ |
17 | +<<<<<<< TREE |
18 | BRANCH INSTEAD until "quantal" gets the first upload |
19 | |
20 | ALSO DO NOT MERGE TRUNK BACK INTO 5.2, ONLY 5.2 INTO TRUNK |
21 | @@ -19,6 +20,17 @@ |
22 | -- Michael Vogt <michael.vogt@ubuntu.com> Fri, 11 May 2012 10:06:18 +0200 |
23 | |
24 | software-center (5.2.2) UNRELEASED; urgency=low |
25 | +======= |
26 | +software-center (5.2.3) UNRELEASED; urgency=low |
27 | + |
28 | + * lp:~evfool/software-center/lp987801: |
29 | + - Only show the version label once for each version in |
30 | + reviews (LP: #987801) |
31 | + |
32 | + -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 16 May 2012 08:58:34 +0200 |
33 | + |
34 | +software-center (5.2.2) precise-proposed; urgency=low |
35 | +>>>>>>> MERGE-SOURCE |
36 | |
37 | [ Michael Vogt ] |
38 | * lp:~mvo/software-center/whatsnew-leak-lp985389: |
39 | @@ -57,7 +69,10 @@ |
40 | * lp:~gary-lasker/software-center/fix-crash_lp973379: |
41 | - ensure that the cache is ready before using the recommender |
42 | service (LP: #973379 ) |
43 | - |
44 | + * lp:~gary-lasker/software-center/toolbar-buttons-insensitive-during-startup: |
45 | + - set the toolbar buttons insensitive for the duration of time that the |
46 | + lobby panels are initializing (LP: #999486, LP: #994341) |
47 | + |
48 | [ Robert Roth ] |
49 | * lp:~evfool/software-center/bug532072: |
50 | - escape markup for support info and license (LP: #993279) |
51 | @@ -88,7 +103,7 @@ |
52 | - test code change only! add memory and cpt stats to the |
53 | test script |
54 | |
55 | - -- Gary Lasker <gary.lasker@canonical.com> Mon, 14 May 2012 22:36:15 -0400 |
56 | + -- Michael Vogt <michael.vogt@ubuntu.com> Tue, 15 May 2012 20:22:02 +0200 |
57 | |
58 | software-center (5.2.1) precise-proposed; urgency=low |
59 | |
60 | |
61 | === modified file 'softwarecenter/backend/spawn_helper.py' |
62 | --- softwarecenter/backend/spawn_helper.py 2012-03-19 13:35:47 +0000 |
63 | +++ softwarecenter/backend/spawn_helper.py 2012-05-16 15:58:19 +0000 |
64 | @@ -86,6 +86,9 @@ |
65 | self.run(cmd) |
66 | |
67 | def run(self, cmd): |
68 | + # only useful for debugging |
69 | + if "SOFTWARE_CENTER_DISABLE_SPAWN_HELPER" in os.environ: |
70 | + return |
71 | self._cmd = cmd |
72 | (pid, stdin, stdout, stderr) = GObject.spawn_async( |
73 | cmd, flags=GObject.SPAWN_DO_NOT_REAP_CHILD, |
74 | |
75 | === modified file 'softwarecenter/db/__init__.py' |
76 | --- softwarecenter/db/__init__.py 2012-03-15 04:30:04 +0000 |
77 | +++ softwarecenter/db/__init__.py 2012-05-16 15:58:19 +0000 |
78 | @@ -1,9 +1,14 @@ |
79 | import logging |
80 | + |
81 | try: |
82 | - from debfile import DebFileApplication |
83 | + from debfile import DebFileApplication, DebFileOpenError |
84 | DebFileApplication # pyflakes |
85 | + DebFileOpenError # pyflakes |
86 | except: |
87 | logging.exception("DebFileApplication import") |
88 | |
89 | - class DebFileApplication(): |
90 | + class DebFileApplication(object): |
91 | + pass |
92 | + |
93 | + class DebFileOpenError(Exception): |
94 | pass |
95 | |
96 | === modified file 'softwarecenter/db/database.py' |
97 | --- softwarecenter/db/database.py 2012-04-12 10:15:36 +0000 |
98 | +++ softwarecenter/db/database.py 2012-05-16 15:58:19 +0000 |
99 | @@ -139,6 +139,10 @@ |
100 | |
101 | def __init__(self, pathname=None, cache=None): |
102 | GObject.GObject.__init__(self) |
103 | + # initialize at creation time to avoid spurious AttributeError |
104 | + self._use_agent = False |
105 | + self._use_axi = False |
106 | + |
107 | if pathname is None: |
108 | pathname = softwarecenter.paths.XAPIAN_PATH |
109 | self._db_pathname = pathname |
110 | |
111 | === modified file 'softwarecenter/db/debfile.py' |
112 | --- softwarecenter/db/debfile.py 2012-03-29 00:18:23 +0000 |
113 | +++ softwarecenter/db/debfile.py 2012-05-16 15:58:19 +0000 |
114 | @@ -27,12 +27,28 @@ |
115 | from softwarecenter.utils import ExecutionTime, utf8 |
116 | |
117 | |
118 | +DEB_MIME_TYPE = 'application/x-debian-package' |
119 | + |
120 | + |
121 | +def is_deb_file(debfile): |
122 | + mtype = guess_type(debfile) |
123 | + return mtype is not None and DEB_MIME_TYPE in mtype |
124 | + |
125 | + |
126 | +class DebFileOpenError(Exception): |
127 | + """ Raised if a DebFile fails to open """ |
128 | + |
129 | + def __init__(self, msg, path): |
130 | + super(DebFileOpenError, self).__init__(msg) |
131 | + self.path = path |
132 | + |
133 | + |
134 | class DebFileApplication(Application): |
135 | |
136 | def __init__(self, debfile): |
137 | - # sanity check |
138 | - if not debfile.endswith(".deb"): |
139 | - raise ValueError("Need a deb file, got '%s'" % debfile) |
140 | + if not is_deb_file(debfile): |
141 | + raise DebFileOpenError("Could not open %r." % debfile, debfile) |
142 | + |
143 | # work out debname/appname |
144 | debname = os.path.splitext(os.path.basename(debfile))[0] |
145 | pkgname = debname.split('_')[0].lower() |
146 | @@ -50,7 +66,21 @@ |
147 | def __init__(self, db, doc=None, application=None): |
148 | super(AppDetailsDebFile, self).__init__(db, doc, application) |
149 | if doc: |
150 | - raise ValueError("doc must be None for deb files") |
151 | + raise DebFileOpenError("AppDetailsDebFile: doc must be None.") |
152 | + |
153 | + self._error = None |
154 | + # check errors before creating the DebPackage |
155 | + if not os.path.exists(self._app.request): |
156 | + self._error = _("Not found") |
157 | + self._error_not_found = utf8(_(u"The file \u201c%s\u201d " |
158 | + "does not exist.")) % utf8(self._app.request) |
159 | + elif not is_deb_file(self._app.request): |
160 | + self._error = _("Not found") |
161 | + self._error_not_found = utf8(_(u"The file \u201c%s\u201d " |
162 | + "is not a software package.")) % utf8(self._app.request) |
163 | + |
164 | + if self._error is not None: |
165 | + return |
166 | |
167 | try: |
168 | with ExecutionTime("create DebPackage"): |
169 | @@ -59,23 +89,10 @@ |
170 | self._deb = DebPackage(self._app.request, self._cache._cache) |
171 | except: |
172 | self._deb = None |
173 | - self._pkg = None |
174 | - if not os.path.exists(self._app.request): |
175 | - self._error = _("Not found") |
176 | - self._error_not_found = utf8(_(u"The file \u201c%s\u201d " |
177 | - "does not exist.")) % utf8(self._app.request) |
178 | - else: |
179 | - mimetype = guess_type(self._app.request) |
180 | - if mimetype[0] != "application/x-debian-package": |
181 | - self._error = _("Not found") |
182 | - self._error_not_found = utf8(_(u"The file \u201c%s\u201d " |
183 | - "is not a software package.")) % utf8( |
184 | - self._app.request) |
185 | - else: |
186 | - # deb files which are corrupt |
187 | - self._error = _("Internal Error") |
188 | - self._error_not_found = utf8(_(u"The file \u201c%s\u201d " |
189 | - "could not be opened.")) % utf8(self._app.request) |
190 | + # deb files which are corrupt |
191 | + self._error = _("Internal Error") |
192 | + self._error_not_found = utf8(_(u"The file \u201c%s\u201d " |
193 | + "could not be opened.")) % utf8(self._app.request) |
194 | return |
195 | |
196 | if self.pkgname and self.pkgname != self._app.pkgname: |
197 | |
198 | === modified file 'softwarecenter/enums.py' |
199 | --- softwarecenter/enums.py 2012-03-21 20:54:46 +0000 |
200 | +++ softwarecenter/enums.py 2012-05-16 15:58:19 +0000 |
201 | @@ -237,6 +237,12 @@ |
202 | REPAIR = "repair_dependencies" |
203 | |
204 | |
205 | +# Search separators |
206 | +class SearchSeparators: |
207 | + REGULAR = " " |
208 | + PACKAGE = "," |
209 | + |
210 | + |
211 | # mouse event codes for back/forward buttons |
212 | # TODO: consider whether we ought to get these values from gconf so that we |
213 | # can be sure to use the corresponding values used by Nautilus: |
214 | |
215 | === modified file 'softwarecenter/testutils.py' |
216 | --- softwarecenter/testutils.py 2012-03-19 14:23:52 +0000 |
217 | +++ softwarecenter/testutils.py 2012-05-16 15:58:19 +0000 |
218 | @@ -22,6 +22,7 @@ |
219 | import tempfile |
220 | import time |
221 | |
222 | +from mock import Mock |
223 | |
224 | m_dbus = m_polkit = m_aptd = None |
225 | |
226 | @@ -145,7 +146,6 @@ |
227 | """ take a application and return a app where the details are a mock |
228 | of the real details so they can easily be modified |
229 | """ |
230 | - from mock import Mock |
231 | import copy |
232 | app = copy.copy(real_app) |
233 | db = get_test_db() |
234 | @@ -160,6 +160,16 @@ |
235 | return app |
236 | |
237 | |
238 | +def get_mock_options(): |
239 | + """Return a mock suitable to act as SoftwareCenterAppGtk3's options.""" |
240 | + mock_options = Mock() |
241 | + mock_options.display_navlog = False |
242 | + mock_options.disable_apt_xapian_index = False |
243 | + mock_options.disable_buy = False |
244 | + |
245 | + return mock_options |
246 | + |
247 | + |
248 | def setup_test_env(): |
249 | """ Setup environment suitable for running the test/* code in a checkout. |
250 | This includes PYTHONPATH, sys.path and softwarecenter.paths.datadir. |
251 | |
252 | === modified file 'softwarecenter/ui/gtk3/SimpleGtkbuilderApp.py' |
253 | --- softwarecenter/ui/gtk3/SimpleGtkbuilderApp.py 2012-03-15 09:32:18 +0000 |
254 | +++ softwarecenter/ui/gtk3/SimpleGtkbuilderApp.py 2012-05-16 15:58:19 +0000 |
255 | @@ -23,9 +23,10 @@ |
256 | |
257 | |
258 | # based on SimpleGladeApp |
259 | -class SimpleGtkbuilderApp: |
260 | +class SimpleGtkbuilderApp(object): |
261 | |
262 | def __init__(self, path, domain): |
263 | + super(SimpleGtkbuilderApp, self).__init__() |
264 | self.builder = Gtk.Builder() |
265 | self.builder.set_translation_domain(domain) |
266 | self.builder.add_from_file(path) |
267 | |
268 | === modified file 'softwarecenter/ui/gtk3/app.py' |
269 | --- softwarecenter/ui/gtk3/app.py 2012-05-15 09:23:50 +0000 |
270 | +++ softwarecenter/ui/gtk3/app.py 2012-05-16 15:58:19 +0000 |
271 | @@ -31,6 +31,7 @@ |
272 | import gettext |
273 | import logging |
274 | import os |
275 | +import re |
276 | import subprocess |
277 | import sys |
278 | import xapian |
279 | @@ -47,28 +48,35 @@ |
280 | |
281 | # db imports |
282 | from softwarecenter.db.application import Application |
283 | -from softwarecenter.db import DebFileApplication |
284 | +from softwarecenter.db import DebFileApplication, DebFileOpenError |
285 | from softwarecenter.i18n import init_locale |
286 | |
287 | # misc imports |
288 | from softwarecenter.plugin import PluginManager |
289 | from softwarecenter.paths import SOFTWARE_CENTER_PLUGIN_DIRS |
290 | -from softwarecenter.enums import (Icons, |
291 | - PkgStates, |
292 | - ViewPages, |
293 | - AppActions, |
294 | - DB_SCHEMA_VERSION, |
295 | - MOUSE_EVENT_FORWARD_BUTTON, |
296 | - MOUSE_EVENT_BACK_BUTTON, |
297 | - SOFTWARE_CENTER_TOS_LINK, |
298 | - SOFTWARE_CENTER_NAME_KEYRING) |
299 | -from softwarecenter.utils import (clear_token_from_ubuntu_sso_sync, |
300 | - get_http_proxy_string_from_gsettings, |
301 | - wait_for_apt_cache_ready, |
302 | - ExecutionTime, |
303 | - is_unity_running) |
304 | -from softwarecenter.ui.gtk3.utils import (get_sc_icon_theme, |
305 | - init_sc_css_provider) |
306 | +from softwarecenter.enums import ( |
307 | + AppActions, |
308 | + DB_SCHEMA_VERSION, |
309 | + Icons, |
310 | + MOUSE_EVENT_FORWARD_BUTTON, |
311 | + MOUSE_EVENT_BACK_BUTTON, |
312 | + PkgStates, |
313 | + SearchSeparators, |
314 | + SOFTWARE_CENTER_TOS_LINK, |
315 | + SOFTWARE_CENTER_NAME_KEYRING, |
316 | + ViewPages, |
317 | +) |
318 | +from softwarecenter.utils import ( |
319 | + clear_token_from_ubuntu_sso_sync, |
320 | + get_http_proxy_string_from_gsettings, |
321 | + wait_for_apt_cache_ready, |
322 | + ExecutionTime, |
323 | + is_unity_running, |
324 | +) |
325 | +from softwarecenter.ui.gtk3.utils import ( |
326 | + get_sc_icon_theme, |
327 | + init_sc_css_provider, |
328 | +) |
329 | from softwarecenter.version import VERSION |
330 | from softwarecenter.db.database import StoreDatabase |
331 | try: |
332 | @@ -87,10 +95,14 @@ |
333 | from softwarecenter.ui.gtk3.panes.historypane import HistoryPane |
334 | from softwarecenter.ui.gtk3.panes.globalpane import GlobalPane |
335 | from softwarecenter.ui.gtk3.panes.pendingpane import PendingPane |
336 | -from softwarecenter.ui.gtk3.session.appmanager import (ApplicationManager, |
337 | - get_appmanager) |
338 | +from softwarecenter.ui.gtk3.session.appmanager import ( |
339 | + ApplicationManager, |
340 | + get_appmanager, |
341 | + ) |
342 | from softwarecenter.ui.gtk3.session.viewmanager import ( |
343 | - ViewManager, get_viewmanager) |
344 | + ViewManager, |
345 | + get_viewmanager, |
346 | + ) |
347 | from softwarecenter.ui.gtk3.widgets.recommendations import ( |
348 | RecommendationsOptInDialog) |
349 | |
350 | @@ -112,6 +124,10 @@ |
351 | from gi.repository import Gdk |
352 | |
353 | LOG = logging.getLogger(__name__) |
354 | +PACKAGE_PREFIX = 'apt:' |
355 | +# "apt:///" is a valid prefix for 'apt:pkgname' in alt+F2 in gnome |
356 | +PACKAGE_PREFIX_REGEX = re.compile('^%s(?:/{2,3})*' % PACKAGE_PREFIX) |
357 | +SEARCH_PREFIX = 'search:' |
358 | |
359 | |
360 | # py3 compat |
361 | @@ -119,6 +135,64 @@ |
362 | return isinstance(func, collections.Callable) |
363 | |
364 | |
365 | +def parse_packages_args(packages): |
366 | + search_text = '' |
367 | + app = None |
368 | + |
369 | + # avoid treating strings as sequences ('foo' should not be 'f', 'o', 'o') |
370 | + if isinstance(packages, basestring): |
371 | + packages = (packages,) |
372 | + |
373 | + if not isinstance(packages, collections.Iterable): |
374 | + LOG.warning('show_available_packages: argument is not an iterable %r', |
375 | + packages) |
376 | + return search_text, app |
377 | + |
378 | + items = [] # make a copy of the given sequence |
379 | + for arg in packages: |
380 | + # support both "pkg1 pkg" and "pkg1,pkg2" (and "pkg1,pkg2 pkg3") |
381 | + if "," in arg: |
382 | + items.extend(arg.split(SearchSeparators.PACKAGE)) |
383 | + else: |
384 | + items.append(arg) |
385 | + |
386 | + if len(items) > 0: |
387 | + # allow s-c to be called with a search term |
388 | + if items[0].startswith(SEARCH_PREFIX): |
389 | + # remove the initial search prefix |
390 | + items[0] = items[0].replace(SEARCH_PREFIX, '', 1) |
391 | + search_text = SearchSeparators.REGULAR.join(items) |
392 | + else: |
393 | + # strip away the initial apt: prefix, if present |
394 | + items[0] = re.sub(PACKAGE_PREFIX_REGEX, '', items[0]) |
395 | + if len(items) > 1: |
396 | + # turn multiple packages into a search with "," as separator |
397 | + search_text = SearchSeparators.PACKAGE.join(items) |
398 | + |
399 | + if not search_text and len(items) == 1: |
400 | + request = items[0] |
401 | + # are we dealing with a path? |
402 | + if os.path.exists(request) and not os.path.isdir(request): |
403 | + if not request.startswith('/'): |
404 | + # we may have been given a relative path |
405 | + request = os.path.abspath(request) |
406 | + # will raise DebOpenFileError if request is invalid |
407 | + app = DebFileApplication(request) |
408 | + else: |
409 | + # package from archive |
410 | + # if there is a "/" in the string consider it as tuple |
411 | + # of (pkgname, appname) for exact matching (used by |
412 | + # e.g. unity |
413 | + (pkgname, sep, appname) = request.partition("/") |
414 | + if pkgname or appname: |
415 | + app = Application(appname, pkgname) |
416 | + else: |
417 | + LOG.warning('show_available_packages: received %r but ' |
418 | + 'can not build an Application from it.', request) |
419 | + |
420 | + return search_text, app |
421 | + |
422 | + |
423 | class SoftwarecenterDbusController(dbus.service.Object): |
424 | """ |
425 | This is a helper to provide the SoftwarecenterIFace |
426 | @@ -158,13 +232,16 @@ |
427 | # the size of the icon for dialogs |
428 | APP_ICON_SIZE = Gtk.IconSize.DIALOG |
429 | |
430 | + START_DBUS = True |
431 | + |
432 | def __init__(self, datadir, xapian_base_path, options, args=None): |
433 | - # setup dbus and exit if there is another instance already |
434 | - # running |
435 | - self.setup_dbus_or_bring_other_instance_to_front(args) |
436 | + self.dbusControler = None |
437 | + if self.START_DBUS: |
438 | + # setup dbus and exit if there is another instance already running |
439 | + self.setup_dbus_or_bring_other_instance_to_front(args) |
440 | |
441 | self.datadir = datadir |
442 | - SimpleGtkbuilderApp.__init__(self, |
443 | + super(SoftwareCenterAppGtk3, self).__init__( |
444 | datadir + "/ui/gtk3/SoftwareCenter.ui", |
445 | "software-center") |
446 | gettext.bindtextdomain("software-center", "/usr/share/locale") |
447 | @@ -273,6 +350,10 @@ |
448 | self.vbox1.pack_start(self.global_pane, False, False, 0) |
449 | self.vbox1.reorder_child(self.global_pane, 1) |
450 | |
451 | + # start with the toolbar buttons insensitive and don't make them |
452 | + # sensitive until the panel elements are ready |
453 | + #self.global_pane.view_switcher.set_sensitive(False) |
454 | + |
455 | # available pane |
456 | self.available_pane = AvailablePane(self.cache, |
457 | self.db, |
458 | @@ -292,8 +373,8 @@ |
459 | self.distro, |
460 | self.icons, |
461 | self.datadir) |
462 | - #~ self.installed_pane.connect("installed-pane-created", |
463 | - #~ self.on_installed_pane_created) |
464 | + self.installed_pane.connect("installed-pane-created", |
465 | + self.on_installed_pane_created) |
466 | self.view_manager.register(self.installed_pane, |
467 | ViewPages.INSTALLED) |
468 | |
469 | @@ -472,9 +553,12 @@ |
470 | "recommendations-opt-out", |
471 | self._on_recommendations_opt_out) |
472 | self.menuitem_recommendations.set_sensitive(True) |
473 | + # set the main toolbar buttons sensitive |
474 | + self.global_pane.view_switcher.set_sensitive(True) |
475 | |
476 | - #~ def on_installed_pane_created(self, widget): |
477 | - #~ pass |
478 | + def on_installed_pane_created(self, widget): |
479 | + # set the main toolbar buttons sensitive |
480 | + self.global_pane.view_switcher.set_sensitive(True) |
481 | |
482 | def _on_recommendations_opt_in(self, rec_panel): |
483 | self._update_recommendations_menuitem(opted_in=True) |
484 | @@ -510,6 +594,18 @@ |
485 | def on_review_stats_loaded(self, reviews): |
486 | LOG.debug("on_review_stats_loaded: '%s'" % len(reviews)) |
487 | |
488 | + def destroy(self): |
489 | + """Destroy this instance and every used resource.""" |
490 | + self.window_main.destroy() |
491 | + |
492 | + # remove global instances of Managers |
493 | + self.app_manager.destroy() |
494 | + self.view_manager.destroy() |
495 | + |
496 | + if self.dbusControler is not None: |
497 | + # ensure that the dbus controller is really gone |
498 | + self.dbusControler.stop() |
499 | + |
500 | def close_app(self): |
501 | """ perform tasks like save-state etc when the application is |
502 | exited |
503 | @@ -524,15 +620,14 @@ |
504 | if hasattr(self, "glaunchpad"): |
505 | self.glaunchpad.shutdown() |
506 | self.save_state() |
507 | + self.destroy() |
508 | + |
509 | # this will not throw exceptions in pygi but "only" log via g_critical |
510 | # to the terminal but it might in the future so we add a handler here |
511 | try: |
512 | Gtk.main_quit() |
513 | except: |
514 | LOG.exception("Gtk.main_quit failed") |
515 | - # ensure that the dbus controller is really gone, just for good |
516 | - # measure |
517 | - self.dbusControler.stop() |
518 | # exit here explictely to ensure that no further gtk event loops or |
519 | # threads run and cause havoc on exit (LP: #914393) |
520 | sys.exit(0) |
521 | @@ -1202,75 +1297,49 @@ |
522 | bus_name = dbus.service.BusName('com.ubuntu.Softwarecenter', bus) |
523 | self.dbusControler = SoftwarecenterDbusController(self, bus_name) |
524 | |
525 | + @wait_for_apt_cache_ready |
526 | + def show_app(self, app): |
527 | + """Show 'app' in the installed pane if is installed. |
528 | + |
529 | + If 'app' is not installed, show it in the available pane. |
530 | + |
531 | + """ |
532 | + if (app.pkgname in self.cache and self.cache[app.pkgname].installed): |
533 | + with ExecutionTime("installed_pane.init_view()"): |
534 | + self.installed_pane.init_view() |
535 | + with ExecutionTime("installed_pane.show_app()"): |
536 | + self.installed_pane.show_app(app) |
537 | + else: |
538 | + self.available_pane.init_view() |
539 | + self.available_pane.show_app(app) |
540 | + |
541 | def show_available_packages(self, packages): |
542 | """ Show packages given as arguments in the available_pane |
543 | If the list of packages is only one element long show that, |
544 | otherwise turn it into a comma seperated search |
545 | """ |
546 | - # strip away the apt: prefix |
547 | - if packages and packages[0].startswith("apt:///"): |
548 | - # this is for 'apt:pkgname' in alt+F2 in gnome |
549 | - packages[0] = packages[0].partition("apt:///")[2] |
550 | - elif packages and packages[0].startswith("apt://"): |
551 | - packages[0] = packages[0].partition("apt://")[2] |
552 | - elif packages and packages[0].startswith("apt:"): |
553 | - packages[0] = packages[0].partition("apt:")[2] |
554 | - |
555 | - # allow s-c to be called with a search term |
556 | - if packages and packages[0].startswith("search:"): |
557 | - packages[0] = packages[0].partition("search:")[2] |
558 | - self.available_pane.init_view() |
559 | - self.available_pane.searchentry.set_text(" ".join(packages)) |
560 | - return |
561 | - |
562 | - if len(packages) == 1: |
563 | - request = packages[0] |
564 | - |
565 | - # are we dealing with a path? |
566 | - if os.path.exists(request) and not os.path.isdir(request): |
567 | - if not request.startswith('/'): |
568 | - # we may have been given a relative path |
569 | - request = os.path.join(os.getcwd(), request) |
570 | - try: |
571 | - app = DebFileApplication(request) |
572 | - except ValueError as e: |
573 | - LOG.error("can not open %s: %s" % (request, e)) |
574 | - from softwarecenter.ui.gtk3.dialogs import error |
575 | - error(None, |
576 | + try: |
577 | + search_text, app = parse_packages_args(packages) |
578 | + except DebFileOpenError as e: |
579 | + LOG.exception("show_available_packages: can not open %r, error:", |
580 | + packages) |
581 | + dialogs.error(None, |
582 | _("Error"), |
583 | - _("The file “%s” could not be opened.") % request) |
584 | - app = None |
585 | - else: |
586 | - # package from archive |
587 | - # if there is a "/" in the string consider it as tuple |
588 | - # of (pkgname, appname) for exact matching (used by |
589 | - # e.g. unity |
590 | - (pkgname, sep, appname) = packages[0].partition("/") |
591 | - app = Application(appname, pkgname) |
592 | - |
593 | - @wait_for_apt_cache_ready |
594 | - def show_app(self, app): |
595 | - # if the pkg is installed, show it in the installed pane |
596 | - if (app.pkgname in self.cache and |
597 | - self.cache[app.pkgname].installed): |
598 | - with ExecutionTime("installed_pane.init_view()"): |
599 | - self.installed_pane.init_view() |
600 | - with ExecutionTime("installed_pane.show_app()"): |
601 | - self.installed_pane.show_app(app) |
602 | - else: |
603 | - self.available_pane.init_view() |
604 | - self.available_pane.show_app(app) |
605 | - if app: |
606 | - show_app(self, app) |
607 | - return |
608 | - elif len(packages) > 1: |
609 | - # turn multiple packages into a search with "," |
610 | + _("The file “%s” could not be opened.") % e.path) |
611 | + search_text = app = None |
612 | + |
613 | + LOG.info('show_available_packages: search_text is %r, app is %r.', |
614 | + search_text, app) |
615 | + |
616 | + if search_text: |
617 | self.available_pane.init_view() |
618 | - self.available_pane.searchentry.set_text(",".join(packages)) |
619 | - return |
620 | - # normal startup, show the lobby (it will have a spinner when |
621 | - # its not ready yet) - it will also initialize the view |
622 | - self.view_manager.set_active_view(ViewPages.AVAILABLE) |
623 | + self.available_pane.searchentry.set_text(search_text) |
624 | + elif app is not None: |
625 | + self.show_app(app) |
626 | + else: |
627 | + # normal startup, show the lobby (it will have a spinner when |
628 | + # its not ready yet) - it will also initialize the view |
629 | + self.view_manager.set_active_view(ViewPages.AVAILABLE) |
630 | |
631 | def restore_state(self): |
632 | if self.config.has_option("general", "size"): |
633 | |
634 | === modified file 'softwarecenter/ui/gtk3/models/appstore2.py' |
635 | --- softwarecenter/ui/gtk3/models/appstore2.py 2012-05-11 13:48:04 +0000 |
636 | +++ softwarecenter/ui/gtk3/models/appstore2.py 2012-05-16 15:58:19 +0000 |
637 | @@ -163,6 +163,7 @@ |
638 | if doc.installed is None: |
639 | pkgname = self.get_pkgname(doc) |
640 | doc.installed = (self.is_available(doc) and |
641 | + pkgname in self.cache and |
642 | self.cache[pkgname].is_installed) |
643 | return doc.installed |
644 | |
645 | |
646 | === modified file 'softwarecenter/ui/gtk3/session/appmanager.py' |
647 | --- softwarecenter/ui/gtk3/session/appmanager.py 2012-03-15 00:16:03 +0000 |
648 | +++ softwarecenter/ui/gtk3/session/appmanager.py 2012-05-16 15:58:19 +0000 |
649 | @@ -71,6 +71,11 @@ |
650 | else: |
651 | _appmanager = self |
652 | |
653 | + def destroy(self): |
654 | + """Destroy the global instance.""" |
655 | + global _appmanager |
656 | + _appmanager = None |
657 | + |
658 | def request_action(self, app, addons_install, addons_remove, action): |
659 | """callback when an app action is requested from the appview, |
660 | if action is "remove", must check if other dependencies have to be |
661 | |
662 | === modified file 'softwarecenter/ui/gtk3/session/viewmanager.py' |
663 | --- softwarecenter/ui/gtk3/session/viewmanager.py 2012-03-15 00:16:03 +0000 |
664 | +++ softwarecenter/ui/gtk3/session/viewmanager.py 2012-05-16 15:58:19 +0000 |
665 | @@ -69,6 +69,11 @@ |
666 | else: |
667 | _viewmanager = self |
668 | |
669 | + def destroy(self): |
670 | + """Destroy the global instance.""" |
671 | + global _viewmanager |
672 | + _viewmanager = None |
673 | + |
674 | def on_search_terms_changed(self, widget, new_text): |
675 | pane = self.get_current_view_widget() |
676 | if hasattr(pane, "on_search_terms_changed"): |
677 | |
678 | === modified file 'softwarecenter/ui/gtk3/views/appdetailsview.py' |
679 | --- softwarecenter/ui/gtk3/views/appdetailsview.py 2012-05-14 03:16:07 +0000 |
680 | +++ softwarecenter/ui/gtk3/views/appdetailsview.py 2012-05-16 15:58:19 +0000 |
681 | @@ -1738,7 +1738,7 @@ |
682 | # update all (but skip the addons calculation if this is a |
683 | # DebFileApplication as this is not useful for this case and it |
684 | # increases the view load time dramatically) |
685 | - skip_update_addons = type(self.app) == DebFileApplication |
686 | + skip_update_addons = isinstance(self.app, DebFileApplication) |
687 | self._update_all(self.app_details, |
688 | skip_update_addons=skip_update_addons) |
689 | |
690 | @@ -2013,7 +2013,7 @@ |
691 | self.addons_manager.addons_to_remove, |
692 | self.app.archive_suite) |
693 | total_download_size, total_install_size = res |
694 | - if res == (0, 0) and type(self.app) == DebFileApplication: |
695 | + if res == (0, 0) and isinstance(self.app, DebFileApplication): |
696 | total_install_size = self.app_details.installed_size |
697 | if total_download_size > 0: |
698 | download_size = GLib.format_size(total_download_size) |
699 | |
700 | === modified file 'softwarecenter/ui/gtk3/widgets/reviews.py' |
701 | --- softwarecenter/ui/gtk3/widgets/reviews.py 2012-05-15 08:37:41 +0000 |
702 | +++ softwarecenter/ui/gtk3/widgets/reviews.py 2012-05-16 15:58:19 +0000 |
703 | @@ -194,11 +194,18 @@ |
704 | UI vbox out of them |
705 | """ |
706 | self.logged_in_person = get_person_from_config() |
707 | + is_first_for_version = None |
708 | if self.reviews: |
709 | + previous_review = None |
710 | for r in self.reviews: |
711 | pkgversion = self._parent.app_details.version |
712 | + if previous_review: |
713 | + is_first_for_version = previous_review.version != r.version |
714 | + else: |
715 | + is_first_for_version = True |
716 | + previous_review = r |
717 | review = UIReview(r, pkgversion, self.logged_in_person, |
718 | - self.useful_votes) |
719 | + self.useful_votes, is_first_for_version) |
720 | review.show_all() |
721 | self.vbox.pack_start(review, True, True, 0) |
722 | |
723 | @@ -399,7 +406,8 @@ |
724 | useful/inappropriate etc |
725 | """ |
726 | def __init__(self, review_data=None, app_version=None, |
727 | - logged_in_person=None, useful_votes=None): |
728 | + logged_in_person=None, useful_votes=None, |
729 | + first_for_version=True): |
730 | GObject.GObject.__init__(self) |
731 | self.set_spacing(StockEms.SMALL) |
732 | |
733 | @@ -439,8 +447,8 @@ |
734 | self.usefulness_error = False |
735 | self.delete_error = False |
736 | self.modify_error = False |
737 | - |
738 | - self.pack_start(self.version_label, False, False, 0) |
739 | + if first_for_version: |
740 | + self.pack_start(self.version_label, False, False, 0) |
741 | self.pack_start(self.header, False, False, 0) |
742 | self.pack_start(self.body, False, False, 0) |
743 | self.pack_start(self.footer, False, False, StockEms.SMALL) |
744 | |
745 | === modified file 'softwarecenter/ui/gtk3/widgets/videoplayer.py' |
746 | --- softwarecenter/ui/gtk3/widgets/videoplayer.py 2012-03-08 17:42:36 +0000 |
747 | +++ softwarecenter/ui/gtk3/widgets/videoplayer.py 2012-05-16 15:58:19 +0000 |
748 | @@ -72,7 +72,11 @@ |
749 | # uri property |
750 | def _set_uri(self, v): |
751 | self._uri = v or "" |
752 | - self.webkit.load_uri(self._uri) |
753 | + if self._uri: |
754 | + # only load the uri if it's defined, otherwise we may get: |
755 | + # Program received signal SIGSEGV, Segmentation fault. |
756 | + # webkit_web_frame_load_uri () from /usr/lib/libwebkitgtk-3.0.so.0 |
757 | + self.webkit.load_uri(self._uri) |
758 | |
759 | def _get_uri(self): |
760 | return self._uri |
761 | |
762 | === added file 'test/gtk3/__init__.py' |
763 | === added file 'test/gtk3/test_app.py' |
764 | --- test/gtk3/test_app.py 1970-01-01 00:00:00 +0000 |
765 | +++ test/gtk3/test_app.py 2012-05-16 15:58:19 +0000 |
766 | @@ -0,0 +1,357 @@ |
767 | +#!/usr/bin/python |
768 | + |
769 | +import os |
770 | +import unittest |
771 | + |
772 | +from collections import defaultdict |
773 | +from functools import partial |
774 | + |
775 | +from mock import Mock |
776 | + |
777 | +from testutils import get_mock_options, setup_test_env |
778 | +setup_test_env() |
779 | + |
780 | +import softwarecenter.paths |
781 | +from softwarecenter.db import DebFileApplication, DebFileOpenError |
782 | +from softwarecenter.enums import PkgStates, SearchSeparators |
783 | +from softwarecenter.ui.gtk3 import app |
784 | + |
785 | + |
786 | +class FakedCache(dict): |
787 | + """A faked cache.""" |
788 | + |
789 | + def __init__(self, *a, **kw): |
790 | + super(FakedCache, self).__init__() |
791 | + self._callbacks = defaultdict(list) |
792 | + self.ready = False |
793 | + |
794 | + def open(self): |
795 | + """Open this cache.""" |
796 | + self.ready = True |
797 | + |
798 | + def connect(self, signal, callback): |
799 | + """Connect a signal with a callback.""" |
800 | + self._callbacks[signal].append(callback) |
801 | + |
802 | + def disconnect_by_func(self, callback): |
803 | + """Disconnect 'callback' from every signal.""" |
804 | + for signal, cb in self._callbacks.iteritems(): |
805 | + if cb == callback: |
806 | + self._callbacks[signal].remove(callback) |
807 | + if len(self._callbacks[signal]) == 0: |
808 | + self._callbacks.pop(signal) |
809 | + |
810 | + def get_addons(self, pkgname): |
811 | + """Return (recommended, suggested) addons for 'pkgname'.""" |
812 | + return ([],[]) |
813 | + |
814 | + def get_total_size_on_install(self,pkgname, addons_to_install, |
815 | + addons_to_remove, archive_suite): |
816 | + """Return a fake (total_download_size, total_install_size) result.""" |
817 | + return (0, 0) |
818 | + |
819 | + |
820 | +class ParsePackagesArgsTestCase(unittest.TestCase): |
821 | + """Test suite for the parse_packages_args helper.""" |
822 | + |
823 | + pkg_name = 'foo' |
824 | + |
825 | + def transform_for_test(self, items): |
826 | + """Transform a sequence into a comma separated string.""" |
827 | + return app.SEARCH_PREFIX + SearchSeparators.REGULAR.join(items) |
828 | + |
829 | + def do_check(self, apps, items=None): |
830 | + """Check that the available_pane was shown.""" |
831 | + if items is None: |
832 | + items = self.transform_for_test(apps) |
833 | + |
834 | + search_text, result_app = app.parse_packages_args(items) |
835 | + |
836 | + self.assertEqual(SearchSeparators.REGULAR.join(apps), search_text) |
837 | + self.assertIsNone(result_app) |
838 | + |
839 | + def test_empty(self): |
840 | + """Pass an empty argument, show the 'available' view.""" |
841 | + self.do_check(apps=()) |
842 | + |
843 | + def test_single_empty_item(self): |
844 | + """Pass a single empty item, show the 'available' view.""" |
845 | + self.do_check(apps=('',)) |
846 | + |
847 | + def test_single_item(self): |
848 | + """Pass a single item, show the 'available' view.""" |
849 | + self.do_check(apps=(self.pkg_name,)) |
850 | + |
851 | + def test_two_items(self): |
852 | + """Pass two items, show the 'available' view.""" |
853 | + self.do_check(apps=(self.pkg_name, 'bar')) |
854 | + |
855 | + def test_several_items(self): |
856 | + """Pass several items, show the 'available' view.""" |
857 | + self.do_check(apps=(self.pkg_name, 'firefox', 'software-center')) |
858 | + |
859 | + |
860 | +class ParsePackageArgsAsFileTestCase(unittest.TestCase): |
861 | + |
862 | + def test_item_is_a_file(self): |
863 | + """Pass an item that is an existing file.""" |
864 | + # pass a real deb here |
865 | + fname = os.path.join(os.path.dirname(os.path.abspath(__file__)), |
866 | + "..", "data", "test_debs", "gdebi-test1.deb") |
867 | + assert os.path.exists(fname) |
868 | + # test once as string and as list |
869 | + for items in ( fname, [fname] ): |
870 | + search_text, result_app = app.parse_packages_args(fname) |
871 | + self.assertIsInstance(result_app, DebFileApplication) |
872 | + |
873 | + def test_item_is_invalid_file(self): |
874 | + """ Pass an invalid file item """ |
875 | + fname = __file__ |
876 | + assert os.path.exists(fname) |
877 | + self.assertRaises(DebFileOpenError, app.parse_packages_args, fname) |
878 | + |
879 | + |
880 | +class ParsePackagesWithAptPrefixTestCase(ParsePackagesArgsTestCase): |
881 | + |
882 | + installed = None |
883 | + |
884 | + def setUp(self): |
885 | + super(ParsePackagesWithAptPrefixTestCase, self).setUp() |
886 | + |
887 | + self.cache = FakedCache() |
888 | + self.db = app.StoreDatabase(cache=self.cache) |
889 | + |
890 | + assert self.pkg_name not in self.cache |
891 | + if self.installed is not None: |
892 | + mock_cache_entry = Mock() |
893 | + mock_cache_entry.website = None |
894 | + mock_cache_entry.license = None |
895 | + mock_cache_entry.installed_files = [] |
896 | + mock_cache_entry.candidate = Mock() |
897 | + mock_cache_entry.candidate.version = '1.0' |
898 | + mock_cache_entry.candidate.description = 'A nonsense app.' |
899 | + mock_cache_entry.candidate.origins = () |
900 | + mock_cache_entry.versions = (Mock(),) |
901 | + mock_cache_entry.versions[0].version = '0.99' |
902 | + mock_cache_entry.versions[0].origins = (Mock(),) |
903 | + mock_cache_entry.versions[0].origins[0].archive = 'test' |
904 | + mock_cache_entry.is_installed = self.installed |
905 | + if self.installed: |
906 | + mock_cache_entry.installed = Mock() |
907 | + mock_cache_entry.installed.version = '0.90' |
908 | + mock_cache_entry.installed.installed_size = 0 |
909 | + else: |
910 | + mock_cache_entry.installed = None |
911 | + |
912 | + self.cache[self.pkg_name] = mock_cache_entry |
913 | + self.addCleanup(self.cache.pop, self.pkg_name) |
914 | + |
915 | + |
916 | + def transform_for_test(self, items): |
917 | + """Do nothing.""" |
918 | + return items |
919 | + |
920 | + def check_package_availability(self, name): |
921 | + """Check whether the package 'name' is available.""" |
922 | + if name not in self.cache: |
923 | + state = PkgStates.NOT_FOUND |
924 | + elif self.cache[name].installed: |
925 | + state = PkgStates.INSTALLED |
926 | + else: |
927 | + state = PkgStates.UNINSTALLED |
928 | + return state |
929 | + |
930 | + def do_check(self, apps, items=None): |
931 | + """Check that the available_pane was shown.""" |
932 | + if items is None: |
933 | + items = self.transform_for_test(apps) |
934 | + |
935 | + search_text, result_app = app.parse_packages_args(items) |
936 | + |
937 | + if apps and len(apps) == 1 and apps[0] and not os.path.isfile(apps[0]): |
938 | + self.assertIsNotNone(result_app) |
939 | + app_details = result_app.get_details(self.db) |
940 | + |
941 | + self.assertEqual(apps[0], app_details.name) |
942 | + state = self.check_package_availability(app_details.name) |
943 | + self.assertEqual(state, app_details.pkg_state) |
944 | + else: |
945 | + self.assertIsNone(result_app) |
946 | + |
947 | + if apps and (len(apps) > 1 or os.path.isfile(apps[0])): |
948 | + self.assertEqual(SearchSeparators.PACKAGE.join(apps), search_text) |
949 | + else: |
950 | + self.assertEqual('', search_text) |
951 | + |
952 | + def test_item_with_prefix(self): |
953 | + """Pass a item with the item prefix.""" |
954 | + for prefix in ('apt:', 'apt://', 'apt:///'): |
955 | + for case in (self.pkg_name, app.PACKAGE_PREFIX + self.pkg_name): |
956 | + self.do_check(apps=(case,), items=(prefix + case,)) |
957 | + |
958 | + |
959 | +class ParsePackagesNotInstalledTestCase(ParsePackagesWithAptPrefixTestCase): |
960 | + """Test suite for parsing/searching/loading package lists.""" |
961 | + |
962 | + installed = False |
963 | + |
964 | + |
965 | +class ParsePackagesInstalledTestCase(ParsePackagesWithAptPrefixTestCase): |
966 | + """Test suite for parsing/searching/loading package lists.""" |
967 | + |
968 | + installed = True |
969 | + |
970 | + |
971 | +class ParsePackagesArgsStringTestCase(ParsePackagesWithAptPrefixTestCase): |
972 | + """Test suite for parsing/loading package lists from strings.""" |
973 | + |
974 | + def transform_for_test(self, items): |
975 | + """Transform a sequence into a comma separated string.""" |
976 | + return SearchSeparators.PACKAGE.join(items) |
977 | + |
978 | + |
979 | +class AppTestCase(unittest.TestCase): |
980 | + """Test suite for the app module.""" |
981 | + |
982 | + def setUp(self): |
983 | + super(AppTestCase, self).setUp() |
984 | + self.called = defaultdict(list) |
985 | + self.addCleanup(self.called.clear) |
986 | + |
987 | + orig = app.SoftwareCenterAppGtk3.START_DBUS |
988 | + self.addCleanup(setattr, app.SoftwareCenterAppGtk3, 'START_DBUS', orig) |
989 | + app.SoftwareCenterAppGtk3.START_DBUS = False |
990 | + |
991 | + orig = app.get_pkg_info |
992 | + self.addCleanup(setattr, app, 'get_pkg_info', orig) |
993 | + app.get_pkg_info = lambda: FakedCache() |
994 | + |
995 | + datadir = softwarecenter.paths.datadir |
996 | + xapianpath = softwarecenter.paths.XAPIAN_BASE_PATH |
997 | + options = get_mock_options() |
998 | + self.app = app.SoftwareCenterAppGtk3(datadir, xapianpath, options) |
999 | + self.addCleanup(self.app.destroy) |
1000 | + |
1001 | + self.app.cache.open() |
1002 | + |
1003 | + # connect some signals of interest |
1004 | + cid = self.app.installed_pane.connect('installed-pane-created', |
1005 | + partial(self.track_calls, 'pane-created')) |
1006 | + self.addCleanup(self.app.installed_pane.disconnect, cid) |
1007 | + |
1008 | + cid = self.app.available_pane.connect('available-pane-created', |
1009 | + partial(self.track_calls, 'pane-created')) |
1010 | + self.addCleanup(self.app.available_pane.disconnect, cid) |
1011 | + |
1012 | + def track_calls(self, name, *a, **kw): |
1013 | + """Record the callback for 'name' using 'args' and 'kwargs'.""" |
1014 | + self.called[name].append((a, kw)) |
1015 | + |
1016 | + |
1017 | +class ShowPackagesTestCase(AppTestCase): |
1018 | + """Test suite for parsing/searching/loading package lists.""" |
1019 | + |
1020 | + def do_check(self, packages, search_text): |
1021 | + """Check that the available_pane was shown.""" |
1022 | + self.app.show_available_packages(packages=packages) |
1023 | + |
1024 | + self.assertEqual(self.called, |
1025 | + {'pane-created': [((self.app.available_pane,), {})]}) |
1026 | + |
1027 | + actual = self.app.available_pane.searchentry.get_text() |
1028 | + self.assertEqual(search_text, actual, |
1029 | + 'Expected search text %r (got %r instead) for packages %r.' % |
1030 | + (search_text, actual, packages)) |
1031 | + |
1032 | + self.assertIsNone(self.app.available_pane.app_details_view.app_details) |
1033 | + |
1034 | + def test_show_available_packages_search_prefix(self): |
1035 | + """Check that the available_pane was shown.""" |
1036 | + self.do_check(packages='search:foo,bar baz', search_text='foo bar baz') |
1037 | + |
1038 | + def test_show_available_packages_apt_prefix(self): |
1039 | + """Check that the available_pane was shown.""" |
1040 | + for prefix in ('apt:', 'apt://', 'apt:///'): |
1041 | + self.do_check(packages=prefix + 'foo,bar,baz', |
1042 | + search_text='foo,bar,baz') |
1043 | + |
1044 | + |
1045 | +class ShowPackagesOnePackageTestCase(AppTestCase): |
1046 | + """Test suite for parsing/searching/loading package lists.""" |
1047 | + |
1048 | + pkg_name = 'foo' |
1049 | + installed = None |
1050 | + |
1051 | + def setUp(self): |
1052 | + super(ShowPackagesOnePackageTestCase, self).setUp() |
1053 | + assert self.pkg_name not in self.app.cache |
1054 | + if self.installed is not None: |
1055 | + mock_cache_entry = Mock() |
1056 | + mock_cache_entry.website = None |
1057 | + mock_cache_entry.license = None |
1058 | + mock_cache_entry.installed_files = [] |
1059 | + mock_cache_entry.candidate = Mock() |
1060 | + mock_cache_entry.candidate.version = '1.0' |
1061 | + mock_cache_entry.candidate.description = 'A nonsense app.' |
1062 | + mock_cache_entry.candidate.origins = () |
1063 | + mock_cache_entry.versions = (Mock(),) |
1064 | + mock_cache_entry.versions[0].version = '0.99' |
1065 | + mock_cache_entry.versions[0].origins = (Mock(),) |
1066 | + mock_cache_entry.versions[0].origins[0].archive = 'test' |
1067 | + mock_cache_entry.is_installed = self.installed |
1068 | + if self.installed: |
1069 | + mock_cache_entry.installed = Mock() |
1070 | + mock_cache_entry.installed.version = '0.90' |
1071 | + mock_cache_entry.installed.installed_size = 0 |
1072 | + else: |
1073 | + mock_cache_entry.installed = None |
1074 | + |
1075 | + self.app.cache[self.pkg_name] = mock_cache_entry |
1076 | + self.addCleanup(self.app.cache.pop, self.pkg_name) |
1077 | + |
1078 | + def check_package_availability(self, name): |
1079 | + """Check whether the package 'name' is available.""" |
1080 | + pane = self.app.available_pane |
1081 | + if name not in self.app.cache: |
1082 | + state = PkgStates.NOT_FOUND |
1083 | + elif self.app.cache[name].installed: |
1084 | + state = PkgStates.INSTALLED |
1085 | + pane = self.app.installed_pane |
1086 | + else: |
1087 | + state = PkgStates.UNINSTALLED |
1088 | + |
1089 | + self.assertEqual(state, pane.app_details_view.app_details.pkg_state) |
1090 | + |
1091 | + return pane |
1092 | + |
1093 | + def test_show_available_packages(self): |
1094 | + """Check that the available_pane was shown.""" |
1095 | + self.app.show_available_packages(packages=self.pkg_name) |
1096 | + |
1097 | + expected_pane = self.check_package_availability(self.pkg_name) |
1098 | + name = expected_pane.app_details_view.app_details.name |
1099 | + self.assertEqual(self.pkg_name, name) |
1100 | + |
1101 | + self.assertEqual('', self.app.available_pane.searchentry.get_text()) |
1102 | + |
1103 | + self.assertEqual(self.called, |
1104 | + {'pane-created': [((expected_pane,), {})]}) |
1105 | + |
1106 | + |
1107 | +class ShowPackagesNotInstalledTestCase(ShowPackagesOnePackageTestCase): |
1108 | + """Test suite for parsing/searching/loading package lists.""" |
1109 | + |
1110 | + installed = False |
1111 | + |
1112 | + |
1113 | +class ShowPackagesInstalledTestCase(ShowPackagesOnePackageTestCase): |
1114 | + """Test suite for parsing/searching/loading package lists.""" |
1115 | + |
1116 | + installed = True |
1117 | + |
1118 | + |
1119 | +if __name__ == "__main__": |
1120 | + # avoid spawning recommender-agent, reviews, software-center-agent etc, |
1121 | + # cuts ~5s or so |
1122 | + os.environ["SOFTWARE_CENTER_DISABLE_SPAWN_HELPER"] = "1" |
1123 | + unittest.main() |
1124 | |
1125 | === modified file 'test/gtk3/test_debfile_view.py' |
1126 | --- test/gtk3/test_debfile_view.py 2012-02-23 16:40:27 +0000 |
1127 | +++ test/gtk3/test_debfile_view.py 2012-05-16 15:58:19 +0000 |
1128 | @@ -3,22 +3,18 @@ |
1129 | import time |
1130 | import unittest |
1131 | |
1132 | -from mock import Mock |
1133 | - |
1134 | -from testutils import setup_test_env, do_events |
1135 | +from testutils import do_events, get_mock_options, setup_test_env |
1136 | setup_test_env() |
1137 | |
1138 | import softwarecenter.paths |
1139 | from softwarecenter.ui.gtk3.app import SoftwareCenterAppGtk3 |
1140 | from softwarecenter.ui.gtk3.panes.availablepane import AvailablePane |
1141 | |
1142 | + |
1143 | class DebFileOpenTestCase(unittest.TestCase): |
1144 | |
1145 | def test_deb_file_view_error(self): |
1146 | - mock_options = Mock() |
1147 | - mock_options.display_navlog = False |
1148 | - mock_options.disable_apt_xapian_index = False |
1149 | - mock_options.disable_buy = False |
1150 | + mock_options = get_mock_options() |
1151 | xapianpath = softwarecenter.paths.XAPIAN_BASE_PATH |
1152 | app = SoftwareCenterAppGtk3( |
1153 | softwarecenter.paths.datadir, xapianpath, mock_options) |
1154 | @@ -37,7 +33,6 @@ |
1155 | # this is deb that is not installable |
1156 | action_button = app.available_pane.app_details_view.pkg_statusbar.button |
1157 | self.assertFalse(action_button.get_property("visible")) |
1158 | - |
1159 | |
1160 | |
1161 | if __name__ == "__main__": |
1162 | |
1163 | === modified file 'test/gtk3/test_purchase.py' |
1164 | --- test/gtk3/test_purchase.py 2012-03-19 14:38:10 +0000 |
1165 | +++ test/gtk3/test_purchase.py 2012-05-16 15:58:19 +0000 |
1166 | @@ -3,12 +3,12 @@ |
1167 | import time |
1168 | import unittest |
1169 | |
1170 | -from mock import Mock,patch |
1171 | +from mock import Mock, patch |
1172 | |
1173 | from testutils import setup_test_env |
1174 | setup_test_env() |
1175 | |
1176 | -from softwarecenter.testutils import do_events |
1177 | +from softwarecenter.testutils import do_events, get_mock_options |
1178 | from softwarecenter.ui.gtk3.app import SoftwareCenterAppGtk3 |
1179 | from softwarecenter.ui.gtk3.panes.availablepane import AvailablePane |
1180 | import softwarecenter.paths |
1181 | @@ -35,7 +35,7 @@ |
1182 | self.assertTrue("skipping" in mock.debug.call_args[0][0]) |
1183 | self.assertFalse("consumer_secret" in mock.debug.call_args[0][0]) |
1184 | mock.reset_mock() |
1185 | - |
1186 | + |
1187 | # run another one |
1188 | win.destroy() |
1189 | |
1190 | @@ -71,25 +71,21 @@ |
1191 | # run another one |
1192 | win.destroy() |
1193 | |
1194 | - |
1195 | def test_reinstall_previous_purchase_display(self): |
1196 | - mock_options = Mock() |
1197 | - mock_options.display_navlog = False |
1198 | - mock_options.disable_apt_xapian_index = False |
1199 | - mock_options.disable_buy = False |
1200 | + mock_options = get_mock_options() |
1201 | xapiandb = "/var/cache/software-center/" |
1202 | app = SoftwareCenterAppGtk3( |
1203 | softwarecenter.paths.datadir, xapiandb, mock_options) |
1204 | - # real app opens cache async |
1205 | - app.cache.open() |
1206 | - # show it |
1207 | + # real app opens cache async |
1208 | + app.cache.open() |
1209 | + # show it |
1210 | app.window_main.show_all() |
1211 | app.available_pane.init_view() |
1212 | self._p() |
1213 | app.on_menuitem_reinstall_purchases_activate(None) |
1214 | # it can take a bit until the sso client is ready |
1215 | for i in range(100): |
1216 | - if (app.available_pane.get_current_page() == |
1217 | + if (app.available_pane.get_current_page() == |
1218 | AvailablePane.Pages.LIST): |
1219 | break |
1220 | self._p() |
1221 | |
1222 | === modified file 'test/test_database.py' |
1223 | --- test/test_database.py 2012-04-13 16:32:26 +0000 |
1224 | +++ test/test_database.py 2012-05-16 15:58:19 +0000 |
1225 | @@ -406,9 +406,13 @@ |
1226 | packagesize 3 # package size |
1227 | app-popcon 4 # app-install .desktop popcon rank |
1228 | """ |
1229 | - open("axi-test-values","w").write(s) |
1230 | + fname = "axi-test-values" |
1231 | + with open(fname, "w") as f: |
1232 | + f.write(s) |
1233 | + self.addCleanup(os.remove, fname) |
1234 | + |
1235 | #db = StoreDatabase("/var/cache/software-center/xapian", self.cache) |
1236 | - axi_values = parse_axi_values_file("axi-test-values") |
1237 | + axi_values = parse_axi_values_file(fname) |
1238 | self.assertNotEqual(axi_values, {}) |
1239 | print axi_values |
1240 | |
1241 | @@ -428,7 +432,8 @@ |
1242 | nonblocking_load=False) |
1243 | self.assertTrue(len(enquirer.get_docids()) > 0) |
1244 | # FIXME: test more of the interface |
1245 | - |
1246 | + |
1247 | + |
1248 | class UtilsTestCase(unittest.TestCase): |
1249 | |
1250 | def test_utils_get_installed_package_list(self): |
1251 | |
1252 | === modified file 'test/test_debfileapplication.py' |
1253 | --- test/test_debfileapplication.py 2012-02-23 16:40:27 +0000 |
1254 | +++ test/test_debfileapplication.py 2012-05-16 15:58:19 +0000 |
1255 | @@ -7,7 +7,7 @@ |
1256 | setup_test_env() |
1257 | |
1258 | from softwarecenter.enums import PkgStates |
1259 | -from softwarecenter.db.debfile import DebFileApplication |
1260 | +from softwarecenter.db.debfile import DebFileApplication, DebFileOpenError |
1261 | from softwarecenter.testutils import get_test_db |
1262 | |
1263 | DEBFILE_PATH = './data/test_debs/gdebi-test9.deb' |
1264 | @@ -22,6 +22,7 @@ |
1265 | DEBFILE_PATH_CORRUPT = './data/test_debs/corrupt.deb' |
1266 | DEBFILE_NOT_INSTALLABLE = './data/test_debs/gdebi-test1.deb' |
1267 | |
1268 | + |
1269 | class TestDebFileApplication(unittest.TestCase): |
1270 | """ Test the class DebFileApplication """ |
1271 | |
1272 | @@ -31,7 +32,7 @@ |
1273 | def test_get_name(self): |
1274 | debfileapplication = DebFileApplication(DEBFILE_PATH) |
1275 | debfiledetails = debfileapplication.get_details(self.db) |
1276 | - |
1277 | + |
1278 | self.assertEquals(debfiledetails.name, DEBFILE_NAME) |
1279 | |
1280 | def test_get_description(self): |
1281 | @@ -64,9 +65,10 @@ |
1282 | debfileapplication = DebFileApplication(DEBFILE_PATH_NOTFOUND) |
1283 | debfiledetails = debfileapplication.get_details(self.db) |
1284 | self.assertEquals(debfiledetails.pkg_state, PkgStates.NOT_FOUND) |
1285 | - |
1286 | + |
1287 | def test_get_pkg_state_not_a_deb(self): |
1288 | - self.assertRaises(ValueError, DebFileApplication, DEBFILE_PATH_NOTADEB) |
1289 | + self.assertRaises(DebFileOpenError, |
1290 | + DebFileApplication, DEBFILE_PATH_NOTADEB) |
1291 | |
1292 | def test_get_pkg_state_corrupt(self): |
1293 | debfileapplication = DebFileApplication(DEBFILE_PATH_CORRUPT) |
1294 | @@ -87,12 +89,14 @@ |
1295 | debfileapplication = DebFileApplication(DEBFILE_PATH) |
1296 | debfiledetails = debfileapplication.get_details(self.db) |
1297 | self.assertEquals(debfiledetails.installed_size, 0) |
1298 | - |
1299 | + |
1300 | def test_get_warning(self): |
1301 | debfileapplication = DebFileApplication(DEBFILE_PATH) |
1302 | debfiledetails = debfileapplication.get_details(self.db) |
1303 | self.assertEquals(debfiledetails.warning, DEBFILE_WARNING) |
1304 | - |
1305 | + |
1306 | + |
1307 | if __name__ == "__main__": |
1308 | logging.basicConfig(level=logging.DEBUG) |
1309 | unittest.main() |
1310 | + |
Thanks Natalia for this branch and sorry for my slow reply.
First let me say thanks for the work and the refactor and the excellent test, we shall use that
as a example.
This looks fine, there have been some changes in the 5.2 branch so I merged it and resolved
conflicts and tweaked it a bit more, please remerge lp:~mvo/software-center/fix-977931/ with this branch
and let me know what you think.
I was curious how to reproduce/ understand the changes in appview.py, reviews.py that cause the model to
be "None". I revert that change but for me its not crashing so I wonder if that is still needed? Or is it
a random issue? Except for this (which I just would like to understand better how it happens) the branch
is +10 approve ;)
Additional thanks for unifying get_mock_options() and the with open() fix (and the cleanup) in test_database.py.