Ubuntu Software Center

Merge lp:~nataliabidart/software-center/fix-977931 into lp:software-center/5.2

Proposed by Natalia Bidart on 2012-05-16
Status: Merged
Merged at revision: 3027
Proposed branch: lp:~nataliabidart/software-center/fix-977931
Merge into: lp:software-center/5.2
Diff against target: 1180 lines (+627/-146) 19 files modified
To merge this branch: bzr merge lp:~nataliabidart/software-center/fix-977931
Reviewer Review Type Date Requested Status
Michael Vogt 2012-05-16 Approve on 2012-05-22
Review via email: mp+106011@code.launchpad.net

This proposal supersedes a proposal from 2012-04-17.

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.

To post a comment you must log in.
Michael Vogt (mvo) wrote : Posted in a previous version of this proposal

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.

Natalia Bidart (nataliabidart) wrote : Posted in a previous version of this proposal

Merged the branch suggested by mvo adding a minor tweaks about detetcting .deb files.

2993. By Natalia Bidart on 2012-05-18

Merged 5.2 in.

2994. By Natalia Bidart on 2012-05-21

Merged 5.2 in.

Michael Vogt (mvo) wrote :

This looks great, I will merge it now into 5.2 (and it will be merged into trunk via 5.2 too).

review: Approve

Preview Diff

1=== modified file 'README'
2--- README 2012-03-08 18:08:49 +0000
3+++ README 2012-05-21 15:37:20 +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 'softwarecenter/backend/spawn_helper.py'
14--- softwarecenter/backend/spawn_helper.py 2012-03-19 13:35:47 +0000
15+++ softwarecenter/backend/spawn_helper.py 2012-05-21 15:37:20 +0000
16@@ -86,6 +86,9 @@
17 self.run(cmd)
18
19 def run(self, cmd):
20+ # only useful for debugging
21+ if "SOFTWARE_CENTER_DISABLE_SPAWN_HELPER" in os.environ:
22+ return
23 self._cmd = cmd
24 (pid, stdin, stdout, stderr) = GObject.spawn_async(
25 cmd, flags=GObject.SPAWN_DO_NOT_REAP_CHILD,
26
27=== modified file 'softwarecenter/db/__init__.py'
28--- softwarecenter/db/__init__.py 2012-03-15 04:30:04 +0000
29+++ softwarecenter/db/__init__.py 2012-05-21 15:37:20 +0000
30@@ -1,9 +1,14 @@
31 import logging
32+
33 try:
34- from debfile import DebFileApplication
35+ from debfile import DebFileApplication, DebFileOpenError
36 DebFileApplication # pyflakes
37+ DebFileOpenError # pyflakes
38 except:
39 logging.exception("DebFileApplication import")
40
41- class DebFileApplication():
42+ class DebFileApplication(object):
43+ pass
44+
45+ class DebFileOpenError(Exception):
46 pass
47
48=== modified file 'softwarecenter/db/database.py'
49--- softwarecenter/db/database.py 2012-04-12 10:15:36 +0000
50+++ softwarecenter/db/database.py 2012-05-21 15:37:20 +0000
51@@ -139,6 +139,10 @@
52
53 def __init__(self, pathname=None, cache=None):
54 GObject.GObject.__init__(self)
55+ # initialize at creation time to avoid spurious AttributeError
56+ self._use_agent = False
57+ self._use_axi = False
58+
59 if pathname is None:
60 pathname = softwarecenter.paths.XAPIAN_PATH
61 self._db_pathname = pathname
62
63=== modified file 'softwarecenter/db/debfile.py'
64--- softwarecenter/db/debfile.py 2012-03-29 00:18:23 +0000
65+++ softwarecenter/db/debfile.py 2012-05-21 15:37:20 +0000
66@@ -27,12 +27,28 @@
67 from softwarecenter.utils import ExecutionTime, utf8
68
69
70+DEB_MIME_TYPE = 'application/x-debian-package'
71+
72+
73+def is_deb_file(debfile):
74+ mtype = guess_type(debfile)
75+ return mtype is not None and DEB_MIME_TYPE in mtype
76+
77+
78+class DebFileOpenError(Exception):
79+ """ Raised if a DebFile fails to open """
80+
81+ def __init__(self, msg, path):
82+ super(DebFileOpenError, self).__init__(msg)
83+ self.path = path
84+
85+
86 class DebFileApplication(Application):
87
88 def __init__(self, debfile):
89- # sanity check
90- if not debfile.endswith(".deb"):
91- raise ValueError("Need a deb file, got '%s'" % debfile)
92+ if not is_deb_file(debfile):
93+ raise DebFileOpenError("Could not open %r." % debfile, debfile)
94+
95 # work out debname/appname
96 debname = os.path.splitext(os.path.basename(debfile))[0]
97 pkgname = debname.split('_')[0].lower()
98@@ -50,7 +66,21 @@
99 def __init__(self, db, doc=None, application=None):
100 super(AppDetailsDebFile, self).__init__(db, doc, application)
101 if doc:
102- raise ValueError("doc must be None for deb files")
103+ raise DebFileOpenError("AppDetailsDebFile: doc must be None.")
104+
105+ self._error = None
106+ # check errors before creating the DebPackage
107+ if not os.path.exists(self._app.request):
108+ self._error = _("Not found")
109+ self._error_not_found = utf8(_(u"The file \u201c%s\u201d "
110+ "does not exist.")) % utf8(self._app.request)
111+ elif not is_deb_file(self._app.request):
112+ self._error = _("Not found")
113+ self._error_not_found = utf8(_(u"The file \u201c%s\u201d "
114+ "is not a software package.")) % utf8(self._app.request)
115+
116+ if self._error is not None:
117+ return
118
119 try:
120 with ExecutionTime("create DebPackage"):
121@@ -59,23 +89,10 @@
122 self._deb = DebPackage(self._app.request, self._cache._cache)
123 except:
124 self._deb = None
125- self._pkg = None
126- if not os.path.exists(self._app.request):
127- self._error = _("Not found")
128- self._error_not_found = utf8(_(u"The file \u201c%s\u201d "
129- "does not exist.")) % utf8(self._app.request)
130- else:
131- mimetype = guess_type(self._app.request)
132- if mimetype[0] != "application/x-debian-package":
133- self._error = _("Not found")
134- self._error_not_found = utf8(_(u"The file \u201c%s\u201d "
135- "is not a software package.")) % utf8(
136- self._app.request)
137- else:
138- # deb files which are corrupt
139- self._error = _("Internal Error")
140- self._error_not_found = utf8(_(u"The file \u201c%s\u201d "
141- "could not be opened.")) % utf8(self._app.request)
142+ # deb files which are corrupt
143+ self._error = _("Internal Error")
144+ self._error_not_found = utf8(_(u"The file \u201c%s\u201d "
145+ "could not be opened.")) % utf8(self._app.request)
146 return
147
148 if self.pkgname and self.pkgname != self._app.pkgname:
149
150=== modified file 'softwarecenter/enums.py'
151--- softwarecenter/enums.py 2012-03-21 20:54:46 +0000
152+++ softwarecenter/enums.py 2012-05-21 15:37:20 +0000
153@@ -237,6 +237,12 @@
154 REPAIR = "repair_dependencies"
155
156
157+# Search separators
158+class SearchSeparators:
159+ REGULAR = " "
160+ PACKAGE = ","
161+
162+
163 # mouse event codes for back/forward buttons
164 # TODO: consider whether we ought to get these values from gconf so that we
165 # can be sure to use the corresponding values used by Nautilus:
166
167=== modified file 'softwarecenter/testutils.py'
168--- softwarecenter/testutils.py 2012-03-19 14:23:52 +0000
169+++ softwarecenter/testutils.py 2012-05-21 15:37:20 +0000
170@@ -22,6 +22,7 @@
171 import tempfile
172 import time
173
174+from mock import Mock
175
176 m_dbus = m_polkit = m_aptd = None
177
178@@ -145,7 +146,6 @@
179 """ take a application and return a app where the details are a mock
180 of the real details so they can easily be modified
181 """
182- from mock import Mock
183 import copy
184 app = copy.copy(real_app)
185 db = get_test_db()
186@@ -160,6 +160,16 @@
187 return app
188
189
190+def get_mock_options():
191+ """Return a mock suitable to act as SoftwareCenterAppGtk3's options."""
192+ mock_options = Mock()
193+ mock_options.display_navlog = False
194+ mock_options.disable_apt_xapian_index = False
195+ mock_options.disable_buy = False
196+
197+ return mock_options
198+
199+
200 def setup_test_env():
201 """ Setup environment suitable for running the test/* code in a checkout.
202 This includes PYTHONPATH, sys.path and softwarecenter.paths.datadir.
203
204=== modified file 'softwarecenter/ui/gtk3/SimpleGtkbuilderApp.py'
205--- softwarecenter/ui/gtk3/SimpleGtkbuilderApp.py 2012-03-15 09:32:18 +0000
206+++ softwarecenter/ui/gtk3/SimpleGtkbuilderApp.py 2012-05-21 15:37:20 +0000
207@@ -23,9 +23,10 @@
208
209
210 # based on SimpleGladeApp
211-class SimpleGtkbuilderApp:
212+class SimpleGtkbuilderApp(object):
213
214 def __init__(self, path, domain):
215+ super(SimpleGtkbuilderApp, self).__init__()
216 self.builder = Gtk.Builder()
217 self.builder.set_translation_domain(domain)
218 self.builder.add_from_file(path)
219
220=== modified file 'softwarecenter/ui/gtk3/app.py'
221--- softwarecenter/ui/gtk3/app.py 2012-05-15 18:37:27 +0000
222+++ softwarecenter/ui/gtk3/app.py 2012-05-21 15:37:20 +0000
223@@ -31,6 +31,7 @@
224 import gettext
225 import logging
226 import os
227+import re
228 import subprocess
229 import sys
230 import xapian
231@@ -47,28 +48,35 @@
232
233 # db imports
234 from softwarecenter.db.application import Application
235-from softwarecenter.db import DebFileApplication
236+from softwarecenter.db import DebFileApplication, DebFileOpenError
237 from softwarecenter.i18n import init_locale
238
239 # misc imports
240 from softwarecenter.plugin import PluginManager
241 from softwarecenter.paths import SOFTWARE_CENTER_PLUGIN_DIRS
242-from softwarecenter.enums import (Icons,
243- PkgStates,
244- ViewPages,
245- AppActions,
246- DB_SCHEMA_VERSION,
247- MOUSE_EVENT_FORWARD_BUTTON,
248- MOUSE_EVENT_BACK_BUTTON,
249- SOFTWARE_CENTER_TOS_LINK,
250- SOFTWARE_CENTER_NAME_KEYRING)
251-from softwarecenter.utils import (clear_token_from_ubuntu_sso_sync,
252- get_http_proxy_string_from_gsettings,
253- wait_for_apt_cache_ready,
254- ExecutionTime,
255- is_unity_running)
256-from softwarecenter.ui.gtk3.utils import (get_sc_icon_theme,
257- init_sc_css_provider)
258+from softwarecenter.enums import (
259+ AppActions,
260+ DB_SCHEMA_VERSION,
261+ Icons,
262+ MOUSE_EVENT_FORWARD_BUTTON,
263+ MOUSE_EVENT_BACK_BUTTON,
264+ PkgStates,
265+ SearchSeparators,
266+ SOFTWARE_CENTER_TOS_LINK,
267+ SOFTWARE_CENTER_NAME_KEYRING,
268+ ViewPages,
269+)
270+from softwarecenter.utils import (
271+ clear_token_from_ubuntu_sso_sync,
272+ get_http_proxy_string_from_gsettings,
273+ wait_for_apt_cache_ready,
274+ ExecutionTime,
275+ is_unity_running,
276+)
277+from softwarecenter.ui.gtk3.utils import (
278+ get_sc_icon_theme,
279+ init_sc_css_provider,
280+)
281 from softwarecenter.version import VERSION
282 from softwarecenter.db.database import StoreDatabase
283 try:
284@@ -87,10 +95,14 @@
285 from softwarecenter.ui.gtk3.panes.historypane import HistoryPane
286 from softwarecenter.ui.gtk3.panes.globalpane import GlobalPane
287 from softwarecenter.ui.gtk3.panes.pendingpane import PendingPane
288-from softwarecenter.ui.gtk3.session.appmanager import (ApplicationManager,
289- get_appmanager)
290+from softwarecenter.ui.gtk3.session.appmanager import (
291+ ApplicationManager,
292+ get_appmanager,
293+ )
294 from softwarecenter.ui.gtk3.session.viewmanager import (
295- ViewManager, get_viewmanager)
296+ ViewManager,
297+ get_viewmanager,
298+ )
299 from softwarecenter.ui.gtk3.widgets.recommendations import (
300 RecommendationsOptInDialog)
301
302@@ -112,6 +124,10 @@
303 from gi.repository import Gdk
304
305 LOG = logging.getLogger(__name__)
306+PACKAGE_PREFIX = 'apt:'
307+# "apt:///" is a valid prefix for 'apt:pkgname' in alt+F2 in gnome
308+PACKAGE_PREFIX_REGEX = re.compile('^%s(?:/{2,3})*' % PACKAGE_PREFIX)
309+SEARCH_PREFIX = 'search:'
310
311
312 # py3 compat
313@@ -119,6 +135,64 @@
314 return isinstance(func, collections.Callable)
315
316
317+def parse_packages_args(packages):
318+ search_text = ''
319+ app = None
320+
321+ # avoid treating strings as sequences ('foo' should not be 'f', 'o', 'o')
322+ if isinstance(packages, basestring):
323+ packages = (packages,)
324+
325+ if not isinstance(packages, collections.Iterable):
326+ LOG.warning('show_available_packages: argument is not an iterable %r',
327+ packages)
328+ return search_text, app
329+
330+ items = [] # make a copy of the given sequence
331+ for arg in packages:
332+ # support both "pkg1 pkg" and "pkg1,pkg2" (and "pkg1,pkg2 pkg3")
333+ if "," in arg:
334+ items.extend(arg.split(SearchSeparators.PACKAGE))
335+ else:
336+ items.append(arg)
337+
338+ if len(items) > 0:
339+ # allow s-c to be called with a search term
340+ if items[0].startswith(SEARCH_PREFIX):
341+ # remove the initial search prefix
342+ items[0] = items[0].replace(SEARCH_PREFIX, '', 1)
343+ search_text = SearchSeparators.REGULAR.join(items)
344+ else:
345+ # strip away the initial apt: prefix, if present
346+ items[0] = re.sub(PACKAGE_PREFIX_REGEX, '', items[0])
347+ if len(items) > 1:
348+ # turn multiple packages into a search with "," as separator
349+ search_text = SearchSeparators.PACKAGE.join(items)
350+
351+ if not search_text and len(items) == 1:
352+ request = items[0]
353+ # are we dealing with a path?
354+ if os.path.exists(request) and not os.path.isdir(request):
355+ if not request.startswith('/'):
356+ # we may have been given a relative path
357+ request = os.path.abspath(request)
358+ # will raise DebOpenFileError if request is invalid
359+ app = DebFileApplication(request)
360+ else:
361+ # package from archive
362+ # if there is a "/" in the string consider it as tuple
363+ # of (pkgname, appname) for exact matching (used by
364+ # e.g. unity
365+ (pkgname, sep, appname) = request.partition("/")
366+ if pkgname or appname:
367+ app = Application(appname, pkgname)
368+ else:
369+ LOG.warning('show_available_packages: received %r but '
370+ 'can not build an Application from it.', request)
371+
372+ return search_text, app
373+
374+
375 class SoftwarecenterDbusController(dbus.service.Object):
376 """
377 This is a helper to provide the SoftwarecenterIFace
378@@ -158,13 +232,16 @@
379 # the size of the icon for dialogs
380 APP_ICON_SIZE = Gtk.IconSize.DIALOG
381
382+ START_DBUS = True
383+
384 def __init__(self, datadir, xapian_base_path, options, args=None):
385- # setup dbus and exit if there is another instance already
386- # running
387- self.setup_dbus_or_bring_other_instance_to_front(args)
388+ self.dbusControler = None
389+ if self.START_DBUS:
390+ # setup dbus and exit if there is another instance already running
391+ self.setup_dbus_or_bring_other_instance_to_front(args)
392
393 self.datadir = datadir
394- SimpleGtkbuilderApp.__init__(self,
395+ super(SoftwareCenterAppGtk3, self).__init__(
396 datadir + "/ui/gtk3/SoftwareCenter.ui",
397 "software-center")
398 gettext.bindtextdomain("software-center", "/usr/share/locale")
399@@ -525,6 +602,18 @@
400 def on_review_stats_loaded(self, reviews):
401 LOG.debug("on_review_stats_loaded: '%s'" % len(reviews))
402
403+ def destroy(self):
404+ """Destroy this instance and every used resource."""
405+ self.window_main.destroy()
406+
407+ # remove global instances of Managers
408+ self.app_manager.destroy()
409+ self.view_manager.destroy()
410+
411+ if self.dbusControler is not None:
412+ # ensure that the dbus controller is really gone
413+ self.dbusControler.stop()
414+
415 def close_app(self):
416 """ perform tasks like save-state etc when the application is
417 exited
418@@ -539,15 +628,14 @@
419 if hasattr(self, "glaunchpad"):
420 self.glaunchpad.shutdown()
421 self.save_state()
422+ self.destroy()
423+
424 # this will not throw exceptions in pygi but "only" log via g_critical
425 # to the terminal but it might in the future so we add a handler here
426 try:
427 Gtk.main_quit()
428 except:
429 LOG.exception("Gtk.main_quit failed")
430- # ensure that the dbus controller is really gone, just for good
431- # measure
432- self.dbusControler.stop()
433 # exit here explictely to ensure that no further gtk event loops or
434 # threads run and cause havoc on exit (LP: #914393)
435 sys.exit(0)
436@@ -1217,75 +1305,49 @@
437 bus_name = dbus.service.BusName('com.ubuntu.Softwarecenter', bus)
438 self.dbusControler = SoftwarecenterDbusController(self, bus_name)
439
440+ @wait_for_apt_cache_ready
441+ def show_app(self, app):
442+ """Show 'app' in the installed pane if is installed.
443+
444+ If 'app' is not installed, show it in the available pane.
445+
446+ """
447+ if (app.pkgname in self.cache and self.cache[app.pkgname].installed):
448+ with ExecutionTime("installed_pane.init_view()"):
449+ self.installed_pane.init_view()
450+ with ExecutionTime("installed_pane.show_app()"):
451+ self.installed_pane.show_app(app)
452+ else:
453+ self.available_pane.init_view()
454+ self.available_pane.show_app(app)
455+
456 def show_available_packages(self, packages):
457 """ Show packages given as arguments in the available_pane
458 If the list of packages is only one element long show that,
459 otherwise turn it into a comma seperated search
460 """
461- # strip away the apt: prefix
462- if packages and packages[0].startswith("apt:///"):
463- # this is for 'apt:pkgname' in alt+F2 in gnome
464- packages[0] = packages[0].partition("apt:///")[2]
465- elif packages and packages[0].startswith("apt://"):
466- packages[0] = packages[0].partition("apt://")[2]
467- elif packages and packages[0].startswith("apt:"):
468- packages[0] = packages[0].partition("apt:")[2]
469-
470- # allow s-c to be called with a search term
471- if packages and packages[0].startswith("search:"):
472- packages[0] = packages[0].partition("search:")[2]
473- self.available_pane.init_view()
474- self.available_pane.searchentry.set_text(" ".join(packages))
475- return
476-
477- if len(packages) == 1:
478- request = packages[0]
479-
480- # are we dealing with a path?
481- if os.path.exists(request) and not os.path.isdir(request):
482- if not request.startswith('/'):
483- # we may have been given a relative path
484- request = os.path.join(os.getcwd(), request)
485- try:
486- app = DebFileApplication(request)
487- except ValueError as e:
488- LOG.error("can not open %s: %s" % (request, e))
489- from softwarecenter.ui.gtk3.dialogs import error
490- error(None,
491+ try:
492+ search_text, app = parse_packages_args(packages)
493+ except DebFileOpenError as e:
494+ LOG.exception("show_available_packages: can not open %r, error:",
495+ packages)
496+ dialogs.error(None,
497 _("Error"),
498- _("The file ā€œ%sā€ could not be opened.") % request)
499- app = None
500- else:
501- # package from archive
502- # if there is a "/" in the string consider it as tuple
503- # of (pkgname, appname) for exact matching (used by
504- # e.g. unity
505- (pkgname, sep, appname) = packages[0].partition("/")
506- app = Application(appname, pkgname)
507-
508- @wait_for_apt_cache_ready
509- def show_app(self, app):
510- # if the pkg is installed, show it in the installed pane
511- if (app.pkgname in self.cache and
512- self.cache[app.pkgname].installed):
513- with ExecutionTime("installed_pane.init_view()"):
514- self.installed_pane.init_view()
515- with ExecutionTime("installed_pane.show_app()"):
516- self.installed_pane.show_app(app)
517- else:
518- self.available_pane.init_view()
519- self.available_pane.show_app(app)
520- if app:
521- show_app(self, app)
522- return
523- elif len(packages) > 1:
524- # turn multiple packages into a search with ","
525+ _("The file ā€œ%sā€ could not be opened.") % e.path)
526+ search_text = app = None
527+
528+ LOG.info('show_available_packages: search_text is %r, app is %r.',
529+ search_text, app)
530+
531+ if search_text:
532 self.available_pane.init_view()
533- self.available_pane.searchentry.set_text(",".join(packages))
534- return
535- # normal startup, show the lobby (it will have a spinner when
536- # its not ready yet) - it will also initialize the view
537- self.view_manager.set_active_view(ViewPages.AVAILABLE)
538+ self.available_pane.searchentry.set_text(search_text)
539+ elif app is not None:
540+ self.show_app(app)
541+ else:
542+ # normal startup, show the lobby (it will have a spinner when
543+ # its not ready yet) - it will also initialize the view
544+ self.view_manager.set_active_view(ViewPages.AVAILABLE)
545
546 def restore_state(self):
547 if self.config.has_option("general", "size"):
548
549=== modified file 'softwarecenter/ui/gtk3/models/appstore2.py'
550--- softwarecenter/ui/gtk3/models/appstore2.py 2012-05-18 05:47:10 +0000
551+++ softwarecenter/ui/gtk3/models/appstore2.py 2012-05-21 15:37:20 +0000
552@@ -164,6 +164,7 @@
553 if doc.installed is None:
554 pkgname = self.get_pkgname(doc)
555 doc.installed = (self.is_available(doc) and
556+ pkgname in self.cache and
557 self.cache[pkgname].is_installed)
558 return doc.installed
559
560
561=== modified file 'softwarecenter/ui/gtk3/session/appmanager.py'
562--- softwarecenter/ui/gtk3/session/appmanager.py 2012-03-15 00:16:03 +0000
563+++ softwarecenter/ui/gtk3/session/appmanager.py 2012-05-21 15:37:20 +0000
564@@ -71,6 +71,11 @@
565 else:
566 _appmanager = self
567
568+ def destroy(self):
569+ """Destroy the global instance."""
570+ global _appmanager
571+ _appmanager = None
572+
573 def request_action(self, app, addons_install, addons_remove, action):
574 """callback when an app action is requested from the appview,
575 if action is "remove", must check if other dependencies have to be
576
577=== modified file 'softwarecenter/ui/gtk3/session/viewmanager.py'
578--- softwarecenter/ui/gtk3/session/viewmanager.py 2012-03-15 00:16:03 +0000
579+++ softwarecenter/ui/gtk3/session/viewmanager.py 2012-05-21 15:37:20 +0000
580@@ -69,6 +69,11 @@
581 else:
582 _viewmanager = self
583
584+ def destroy(self):
585+ """Destroy the global instance."""
586+ global _viewmanager
587+ _viewmanager = None
588+
589 def on_search_terms_changed(self, widget, new_text):
590 pane = self.get_current_view_widget()
591 if hasattr(pane, "on_search_terms_changed"):
592
593=== modified file 'softwarecenter/ui/gtk3/views/appdetailsview.py'
594--- softwarecenter/ui/gtk3/views/appdetailsview.py 2012-05-14 03:16:07 +0000
595+++ softwarecenter/ui/gtk3/views/appdetailsview.py 2012-05-21 15:37:20 +0000
596@@ -1738,7 +1738,7 @@
597 # update all (but skip the addons calculation if this is a
598 # DebFileApplication as this is not useful for this case and it
599 # increases the view load time dramatically)
600- skip_update_addons = type(self.app) == DebFileApplication
601+ skip_update_addons = isinstance(self.app, DebFileApplication)
602 self._update_all(self.app_details,
603 skip_update_addons=skip_update_addons)
604
605@@ -2013,7 +2013,7 @@
606 self.addons_manager.addons_to_remove,
607 self.app.archive_suite)
608 total_download_size, total_install_size = res
609- if res == (0, 0) and type(self.app) == DebFileApplication:
610+ if res == (0, 0) and isinstance(self.app, DebFileApplication):
611 total_install_size = self.app_details.installed_size
612 if total_download_size > 0:
613 download_size = GLib.format_size(total_download_size)
614
615=== modified file 'softwarecenter/ui/gtk3/widgets/videoplayer.py'
616--- softwarecenter/ui/gtk3/widgets/videoplayer.py 2012-03-08 17:42:36 +0000
617+++ softwarecenter/ui/gtk3/widgets/videoplayer.py 2012-05-21 15:37:20 +0000
618@@ -72,7 +72,11 @@
619 # uri property
620 def _set_uri(self, v):
621 self._uri = v or ""
622- self.webkit.load_uri(self._uri)
623+ if self._uri:
624+ # only load the uri if it's defined, otherwise we may get:
625+ # Program received signal SIGSEGV, Segmentation fault.
626+ # webkit_web_frame_load_uri () from /usr/lib/libwebkitgtk-3.0.so.0
627+ self.webkit.load_uri(self._uri)
628
629 def _get_uri(self):
630 return self._uri
631
632=== added file 'test/gtk3/__init__.py'
633=== added file 'test/gtk3/test_app.py'
634--- test/gtk3/test_app.py 1970-01-01 00:00:00 +0000
635+++ test/gtk3/test_app.py 2012-05-21 15:37:20 +0000
636@@ -0,0 +1,357 @@
637+#!/usr/bin/python
638+
639+import os
640+import unittest
641+
642+from collections import defaultdict
643+from functools import partial
644+
645+from mock import Mock
646+
647+from testutils import get_mock_options, setup_test_env
648+setup_test_env()
649+
650+import softwarecenter.paths
651+from softwarecenter.db import DebFileApplication, DebFileOpenError
652+from softwarecenter.enums import PkgStates, SearchSeparators
653+from softwarecenter.ui.gtk3 import app
654+
655+
656+class FakedCache(dict):
657+ """A faked cache."""
658+
659+ def __init__(self, *a, **kw):
660+ super(FakedCache, self).__init__()
661+ self._callbacks = defaultdict(list)
662+ self.ready = False
663+
664+ def open(self):
665+ """Open this cache."""
666+ self.ready = True
667+
668+ def connect(self, signal, callback):
669+ """Connect a signal with a callback."""
670+ self._callbacks[signal].append(callback)
671+
672+ def disconnect_by_func(self, callback):
673+ """Disconnect 'callback' from every signal."""
674+ for signal, cb in self._callbacks.iteritems():
675+ if cb == callback:
676+ self._callbacks[signal].remove(callback)
677+ if len(self._callbacks[signal]) == 0:
678+ self._callbacks.pop(signal)
679+
680+ def get_addons(self, pkgname):
681+ """Return (recommended, suggested) addons for 'pkgname'."""
682+ return ([],[])
683+
684+ def get_total_size_on_install(self,pkgname, addons_to_install,
685+ addons_to_remove, archive_suite):
686+ """Return a fake (total_download_size, total_install_size) result."""
687+ return (0, 0)
688+
689+
690+class ParsePackagesArgsTestCase(unittest.TestCase):
691+ """Test suite for the parse_packages_args helper."""
692+
693+ pkg_name = 'foo'
694+
695+ def transform_for_test(self, items):
696+ """Transform a sequence into a comma separated string."""
697+ return app.SEARCH_PREFIX + SearchSeparators.REGULAR.join(items)
698+
699+ def do_check(self, apps, items=None):
700+ """Check that the available_pane was shown."""
701+ if items is None:
702+ items = self.transform_for_test(apps)
703+
704+ search_text, result_app = app.parse_packages_args(items)
705+
706+ self.assertEqual(SearchSeparators.REGULAR.join(apps), search_text)
707+ self.assertIsNone(result_app)
708+
709+ def test_empty(self):
710+ """Pass an empty argument, show the 'available' view."""
711+ self.do_check(apps=())
712+
713+ def test_single_empty_item(self):
714+ """Pass a single empty item, show the 'available' view."""
715+ self.do_check(apps=('',))
716+
717+ def test_single_item(self):
718+ """Pass a single item, show the 'available' view."""
719+ self.do_check(apps=(self.pkg_name,))
720+
721+ def test_two_items(self):
722+ """Pass two items, show the 'available' view."""
723+ self.do_check(apps=(self.pkg_name, 'bar'))
724+
725+ def test_several_items(self):
726+ """Pass several items, show the 'available' view."""
727+ self.do_check(apps=(self.pkg_name, 'firefox', 'software-center'))
728+
729+
730+class ParsePackageArgsAsFileTestCase(unittest.TestCase):
731+
732+ def test_item_is_a_file(self):
733+ """Pass an item that is an existing file."""
734+ # pass a real deb here
735+ fname = os.path.join(os.path.dirname(os.path.abspath(__file__)),
736+ "..", "data", "test_debs", "gdebi-test1.deb")
737+ assert os.path.exists(fname)
738+ # test once as string and as list
739+ for items in ( fname, [fname] ):
740+ search_text, result_app = app.parse_packages_args(fname)
741+ self.assertIsInstance(result_app, DebFileApplication)
742+
743+ def test_item_is_invalid_file(self):
744+ """ Pass an invalid file item """
745+ fname = __file__
746+ assert os.path.exists(fname)
747+ self.assertRaises(DebFileOpenError, app.parse_packages_args, fname)
748+
749+
750+class ParsePackagesWithAptPrefixTestCase(ParsePackagesArgsTestCase):
751+
752+ installed = None
753+
754+ def setUp(self):
755+ super(ParsePackagesWithAptPrefixTestCase, self).setUp()
756+
757+ self.cache = FakedCache()
758+ self.db = app.StoreDatabase(cache=self.cache)
759+
760+ assert self.pkg_name not in self.cache
761+ if self.installed is not None:
762+ mock_cache_entry = Mock()
763+ mock_cache_entry.website = None
764+ mock_cache_entry.license = None
765+ mock_cache_entry.installed_files = []
766+ mock_cache_entry.candidate = Mock()
767+ mock_cache_entry.candidate.version = '1.0'
768+ mock_cache_entry.candidate.description = 'A nonsense app.'
769+ mock_cache_entry.candidate.origins = ()
770+ mock_cache_entry.versions = (Mock(),)
771+ mock_cache_entry.versions[0].version = '0.99'
772+ mock_cache_entry.versions[0].origins = (Mock(),)
773+ mock_cache_entry.versions[0].origins[0].archive = 'test'
774+ mock_cache_entry.is_installed = self.installed
775+ if self.installed:
776+ mock_cache_entry.installed = Mock()
777+ mock_cache_entry.installed.version = '0.90'
778+ mock_cache_entry.installed.installed_size = 0
779+ else:
780+ mock_cache_entry.installed = None
781+
782+ self.cache[self.pkg_name] = mock_cache_entry
783+ self.addCleanup(self.cache.pop, self.pkg_name)
784+
785+
786+ def transform_for_test(self, items):
787+ """Do nothing."""
788+ return items
789+
790+ def check_package_availability(self, name):
791+ """Check whether the package 'name' is available."""
792+ if name not in self.cache:
793+ state = PkgStates.NOT_FOUND
794+ elif self.cache[name].installed:
795+ state = PkgStates.INSTALLED
796+ else:
797+ state = PkgStates.UNINSTALLED
798+ return state
799+
800+ def do_check(self, apps, items=None):
801+ """Check that the available_pane was shown."""
802+ if items is None:
803+ items = self.transform_for_test(apps)
804+
805+ search_text, result_app = app.parse_packages_args(items)
806+
807+ if apps and len(apps) == 1 and apps[0] and not os.path.isfile(apps[0]):
808+ self.assertIsNotNone(result_app)
809+ app_details = result_app.get_details(self.db)
810+
811+ self.assertEqual(apps[0], app_details.name)
812+ state = self.check_package_availability(app_details.name)
813+ self.assertEqual(state, app_details.pkg_state)
814+ else:
815+ self.assertIsNone(result_app)
816+
817+ if apps and (len(apps) > 1 or os.path.isfile(apps[0])):
818+ self.assertEqual(SearchSeparators.PACKAGE.join(apps), search_text)
819+ else:
820+ self.assertEqual('', search_text)
821+
822+ def test_item_with_prefix(self):
823+ """Pass a item with the item prefix."""
824+ for prefix in ('apt:', 'apt://', 'apt:///'):
825+ for case in (self.pkg_name, app.PACKAGE_PREFIX + self.pkg_name):
826+ self.do_check(apps=(case,), items=(prefix + case,))
827+
828+
829+class ParsePackagesNotInstalledTestCase(ParsePackagesWithAptPrefixTestCase):
830+ """Test suite for parsing/searching/loading package lists."""
831+
832+ installed = False
833+
834+
835+class ParsePackagesInstalledTestCase(ParsePackagesWithAptPrefixTestCase):
836+ """Test suite for parsing/searching/loading package lists."""
837+
838+ installed = True
839+
840+
841+class ParsePackagesArgsStringTestCase(ParsePackagesWithAptPrefixTestCase):
842+ """Test suite for parsing/loading package lists from strings."""
843+
844+ def transform_for_test(self, items):
845+ """Transform a sequence into a comma separated string."""
846+ return SearchSeparators.PACKAGE.join(items)
847+
848+
849+class AppTestCase(unittest.TestCase):
850+ """Test suite for the app module."""
851+
852+ def setUp(self):
853+ super(AppTestCase, self).setUp()
854+ self.called = defaultdict(list)
855+ self.addCleanup(self.called.clear)
856+
857+ orig = app.SoftwareCenterAppGtk3.START_DBUS
858+ self.addCleanup(setattr, app.SoftwareCenterAppGtk3, 'START_DBUS', orig)
859+ app.SoftwareCenterAppGtk3.START_DBUS = False
860+
861+ orig = app.get_pkg_info
862+ self.addCleanup(setattr, app, 'get_pkg_info', orig)
863+ app.get_pkg_info = lambda: FakedCache()
864+
865+ datadir = softwarecenter.paths.datadir
866+ xapianpath = softwarecenter.paths.XAPIAN_BASE_PATH
867+ options = get_mock_options()
868+ self.app = app.SoftwareCenterAppGtk3(datadir, xapianpath, options)
869+ self.addCleanup(self.app.destroy)
870+
871+ self.app.cache.open()
872+
873+ # connect some signals of interest
874+ cid = self.app.installed_pane.connect('installed-pane-created',
875+ partial(self.track_calls, 'pane-created'))
876+ self.addCleanup(self.app.installed_pane.disconnect, cid)
877+
878+ cid = self.app.available_pane.connect('available-pane-created',
879+ partial(self.track_calls, 'pane-created'))
880+ self.addCleanup(self.app.available_pane.disconnect, cid)
881+
882+ def track_calls(self, name, *a, **kw):
883+ """Record the callback for 'name' using 'args' and 'kwargs'."""
884+ self.called[name].append((a, kw))
885+
886+
887+class ShowPackagesTestCase(AppTestCase):
888+ """Test suite for parsing/searching/loading package lists."""
889+
890+ def do_check(self, packages, search_text):
891+ """Check that the available_pane was shown."""
892+ self.app.show_available_packages(packages=packages)
893+
894+ self.assertEqual(self.called,
895+ {'pane-created': [((self.app.available_pane,), {})]})
896+
897+ actual = self.app.available_pane.searchentry.get_text()
898+ self.assertEqual(search_text, actual,
899+ 'Expected search text %r (got %r instead) for packages %r.' %
900+ (search_text, actual, packages))
901+
902+ self.assertIsNone(self.app.available_pane.app_details_view.app_details)
903+
904+ def test_show_available_packages_search_prefix(self):
905+ """Check that the available_pane was shown."""
906+ self.do_check(packages='search:foo,bar baz', search_text='foo bar baz')
907+
908+ def test_show_available_packages_apt_prefix(self):
909+ """Check that the available_pane was shown."""
910+ for prefix in ('apt:', 'apt://', 'apt:///'):
911+ self.do_check(packages=prefix + 'foo,bar,baz',
912+ search_text='foo,bar,baz')
913+
914+
915+class ShowPackagesOnePackageTestCase(AppTestCase):
916+ """Test suite for parsing/searching/loading package lists."""
917+
918+ pkg_name = 'foo'
919+ installed = None
920+
921+ def setUp(self):
922+ super(ShowPackagesOnePackageTestCase, self).setUp()
923+ assert self.pkg_name not in self.app.cache
924+ if self.installed is not None:
925+ mock_cache_entry = Mock()
926+ mock_cache_entry.website = None
927+ mock_cache_entry.license = None
928+ mock_cache_entry.installed_files = []
929+ mock_cache_entry.candidate = Mock()
930+ mock_cache_entry.candidate.version = '1.0'
931+ mock_cache_entry.candidate.description = 'A nonsense app.'
932+ mock_cache_entry.candidate.origins = ()
933+ mock_cache_entry.versions = (Mock(),)
934+ mock_cache_entry.versions[0].version = '0.99'
935+ mock_cache_entry.versions[0].origins = (Mock(),)
936+ mock_cache_entry.versions[0].origins[0].archive = 'test'
937+ mock_cache_entry.is_installed = self.installed
938+ if self.installed:
939+ mock_cache_entry.installed = Mock()
940+ mock_cache_entry.installed.version = '0.90'
941+ mock_cache_entry.installed.installed_size = 0
942+ else:
943+ mock_cache_entry.installed = None
944+
945+ self.app.cache[self.pkg_name] = mock_cache_entry
946+ self.addCleanup(self.app.cache.pop, self.pkg_name)
947+
948+ def check_package_availability(self, name):
949+ """Check whether the package 'name' is available."""
950+ pane = self.app.available_pane
951+ if name not in self.app.cache:
952+ state = PkgStates.NOT_FOUND
953+ elif self.app.cache[name].installed:
954+ state = PkgStates.INSTALLED
955+ pane = self.app.installed_pane
956+ else:
957+ state = PkgStates.UNINSTALLED
958+
959+ self.assertEqual(state, pane.app_details_view.app_details.pkg_state)
960+
961+ return pane
962+
963+ def test_show_available_packages(self):
964+ """Check that the available_pane was shown."""
965+ self.app.show_available_packages(packages=self.pkg_name)
966+
967+ expected_pane = self.check_package_availability(self.pkg_name)
968+ name = expected_pane.app_details_view.app_details.name
969+ self.assertEqual(self.pkg_name, name)
970+
971+ self.assertEqual('', self.app.available_pane.searchentry.get_text())
972+
973+ self.assertEqual(self.called,
974+ {'pane-created': [((expected_pane,), {})]})
975+
976+
977+class ShowPackagesNotInstalledTestCase(ShowPackagesOnePackageTestCase):
978+ """Test suite for parsing/searching/loading package lists."""
979+
980+ installed = False
981+
982+
983+class ShowPackagesInstalledTestCase(ShowPackagesOnePackageTestCase):
984+ """Test suite for parsing/searching/loading package lists."""
985+
986+ installed = True
987+
988+
989+if __name__ == "__main__":
990+ # avoid spawning recommender-agent, reviews, software-center-agent etc,
991+ # cuts ~5s or so
992+ os.environ["SOFTWARE_CENTER_DISABLE_SPAWN_HELPER"] = "1"
993+ unittest.main()
994
995=== modified file 'test/gtk3/test_debfile_view.py'
996--- test/gtk3/test_debfile_view.py 2012-02-23 16:40:27 +0000
997+++ test/gtk3/test_debfile_view.py 2012-05-21 15:37:20 +0000
998@@ -3,22 +3,18 @@
999 import time
1000 import unittest
1001
1002-from mock import Mock
1003-
1004-from testutils import setup_test_env, do_events
1005+from testutils import do_events, get_mock_options, setup_test_env
1006 setup_test_env()
1007
1008 import softwarecenter.paths
1009 from softwarecenter.ui.gtk3.app import SoftwareCenterAppGtk3
1010 from softwarecenter.ui.gtk3.panes.availablepane import AvailablePane
1011
1012+
1013 class DebFileOpenTestCase(unittest.TestCase):
1014
1015 def test_deb_file_view_error(self):
1016- mock_options = Mock()
1017- mock_options.display_navlog = False
1018- mock_options.disable_apt_xapian_index = False
1019- mock_options.disable_buy = False
1020+ mock_options = get_mock_options()
1021 xapianpath = softwarecenter.paths.XAPIAN_BASE_PATH
1022 app = SoftwareCenterAppGtk3(
1023 softwarecenter.paths.datadir, xapianpath, mock_options)
1024@@ -37,7 +33,6 @@
1025 # this is deb that is not installable
1026 action_button = app.available_pane.app_details_view.pkg_statusbar.button
1027 self.assertFalse(action_button.get_property("visible"))
1028-
1029
1030
1031 if __name__ == "__main__":
1032
1033=== modified file 'test/gtk3/test_purchase.py'
1034--- test/gtk3/test_purchase.py 2012-03-19 14:38:10 +0000
1035+++ test/gtk3/test_purchase.py 2012-05-21 15:37:20 +0000
1036@@ -3,12 +3,12 @@
1037 import time
1038 import unittest
1039
1040-from mock import Mock,patch
1041+from mock import Mock, patch
1042
1043 from testutils import setup_test_env
1044 setup_test_env()
1045
1046-from softwarecenter.testutils import do_events
1047+from softwarecenter.testutils import do_events, get_mock_options
1048 from softwarecenter.ui.gtk3.app import SoftwareCenterAppGtk3
1049 from softwarecenter.ui.gtk3.panes.availablepane import AvailablePane
1050 import softwarecenter.paths
1051@@ -35,7 +35,7 @@
1052 self.assertTrue("skipping" in mock.debug.call_args[0][0])
1053 self.assertFalse("consumer_secret" in mock.debug.call_args[0][0])
1054 mock.reset_mock()
1055-
1056+
1057 # run another one
1058 win.destroy()
1059
1060@@ -71,25 +71,21 @@
1061 # run another one
1062 win.destroy()
1063
1064-
1065 def test_reinstall_previous_purchase_display(self):
1066- mock_options = Mock()
1067- mock_options.display_navlog = False
1068- mock_options.disable_apt_xapian_index = False
1069- mock_options.disable_buy = False
1070+ mock_options = get_mock_options()
1071 xapiandb = "/var/cache/software-center/"
1072 app = SoftwareCenterAppGtk3(
1073 softwarecenter.paths.datadir, xapiandb, mock_options)
1074- # real app opens cache async
1075- app.cache.open()
1076- # show it
1077+ # real app opens cache async
1078+ app.cache.open()
1079+ # show it
1080 app.window_main.show_all()
1081 app.available_pane.init_view()
1082 self._p()
1083 app.on_menuitem_reinstall_purchases_activate(None)
1084 # it can take a bit until the sso client is ready
1085 for i in range(100):
1086- if (app.available_pane.get_current_page() ==
1087+ if (app.available_pane.get_current_page() ==
1088 AvailablePane.Pages.LIST):
1089 break
1090 self._p()
1091
1092=== modified file 'test/test_database.py'
1093--- test/test_database.py 2012-04-13 16:32:26 +0000
1094+++ test/test_database.py 2012-05-21 15:37:20 +0000
1095@@ -406,9 +406,13 @@
1096 packagesize 3 # package size
1097 app-popcon 4 # app-install .desktop popcon rank
1098 """
1099- open("axi-test-values","w").write(s)
1100+ fname = "axi-test-values"
1101+ with open(fname, "w") as f:
1102+ f.write(s)
1103+ self.addCleanup(os.remove, fname)
1104+
1105 #db = StoreDatabase("/var/cache/software-center/xapian", self.cache)
1106- axi_values = parse_axi_values_file("axi-test-values")
1107+ axi_values = parse_axi_values_file(fname)
1108 self.assertNotEqual(axi_values, {})
1109 print axi_values
1110
1111@@ -428,7 +432,8 @@
1112 nonblocking_load=False)
1113 self.assertTrue(len(enquirer.get_docids()) > 0)
1114 # FIXME: test more of the interface
1115-
1116+
1117+
1118 class UtilsTestCase(unittest.TestCase):
1119
1120 def test_utils_get_installed_package_list(self):
1121
1122=== modified file 'test/test_debfileapplication.py'
1123--- test/test_debfileapplication.py 2012-02-23 16:40:27 +0000
1124+++ test/test_debfileapplication.py 2012-05-21 15:37:20 +0000
1125@@ -7,7 +7,7 @@
1126 setup_test_env()
1127
1128 from softwarecenter.enums import PkgStates
1129-from softwarecenter.db.debfile import DebFileApplication
1130+from softwarecenter.db.debfile import DebFileApplication, DebFileOpenError
1131 from softwarecenter.testutils import get_test_db
1132
1133 DEBFILE_PATH = './data/test_debs/gdebi-test9.deb'
1134@@ -22,6 +22,7 @@
1135 DEBFILE_PATH_CORRUPT = './data/test_debs/corrupt.deb'
1136 DEBFILE_NOT_INSTALLABLE = './data/test_debs/gdebi-test1.deb'
1137
1138+
1139 class TestDebFileApplication(unittest.TestCase):
1140 """ Test the class DebFileApplication """
1141
1142@@ -31,7 +32,7 @@
1143 def test_get_name(self):
1144 debfileapplication = DebFileApplication(DEBFILE_PATH)
1145 debfiledetails = debfileapplication.get_details(self.db)
1146-
1147+
1148 self.assertEquals(debfiledetails.name, DEBFILE_NAME)
1149
1150 def test_get_description(self):
1151@@ -64,9 +65,10 @@
1152 debfileapplication = DebFileApplication(DEBFILE_PATH_NOTFOUND)
1153 debfiledetails = debfileapplication.get_details(self.db)
1154 self.assertEquals(debfiledetails.pkg_state, PkgStates.NOT_FOUND)
1155-
1156+
1157 def test_get_pkg_state_not_a_deb(self):
1158- self.assertRaises(ValueError, DebFileApplication, DEBFILE_PATH_NOTADEB)
1159+ self.assertRaises(DebFileOpenError,
1160+ DebFileApplication, DEBFILE_PATH_NOTADEB)
1161
1162 def test_get_pkg_state_corrupt(self):
1163 debfileapplication = DebFileApplication(DEBFILE_PATH_CORRUPT)
1164@@ -87,12 +89,14 @@
1165 debfileapplication = DebFileApplication(DEBFILE_PATH)
1166 debfiledetails = debfileapplication.get_details(self.db)
1167 self.assertEquals(debfiledetails.installed_size, 0)
1168-
1169+
1170 def test_get_warning(self):
1171 debfileapplication = DebFileApplication(DEBFILE_PATH)
1172 debfiledetails = debfileapplication.get_details(self.db)
1173 self.assertEquals(debfiledetails.warning, DEBFILE_WARNING)
1174-
1175+
1176+
1177 if __name__ == "__main__":
1178 logging.basicConfig(level=logging.DEBUG)
1179 unittest.main()
1180+

Subscribers

People subscribed via source and target branches