Merge lp:~sinzui/bzr-gtk/ui-factory into lp:bzr-gtk

Proposed by Curtis Hovey
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: 781
Merged at revision: 780
Proposed branch: lp:~sinzui/bzr-gtk/ui-factory
Merge into: lp:bzr-gtk
Diff against target: 873 lines (+582/-110)
4 files modified
setup.py (+41/-24)
tests/__init__.py (+51/-19)
tests/test_ui.py (+375/-0)
ui.py (+115/-67)
To merge this branch: bzr merge lp:~sinzui/bzr-gtk/ui-factory
Reviewer Review Type Date Requested Status
Jelmer Vernooij (community) Approve
Review via email: mp+94866@code.launchpad.net

Description of the change

Add GtkUIFactory implementations and missing tests.

This branch was extracted from my effort to guarantee that the progress bar
shows when I use gpush.

--------------------------------------------------------------------

RULES

    * Add tests for all the implemented classes and specifically GtkUIFactory.
    * Add ._progress_updated() and .report_transport_activity() that are
      needed to update the progress widget.
    * Add the message, warning, and error dialogs.
      * Maybe update the Confirmation dialog to extend MessageDialog?
    * ADDENDUM: Let me run just one test module!

LINT

    added:
      tests/test_ui.py
    modified:
      setup.py
      tests/__init__.py
      ui.py

TEST

    ./setup.py check
    or
    ./setup.py check -m test_ui

IMPLEMENTATION

I was frustrated running the whole suite which takes 18 seconds on my
computer. I updated the test runner code to use existing 'discover'
option to run the whole suite, or accept a single module as an arg. The
ui tests complete in 0.107s on my computer.
    setup.py
    tests/__init__.py

I First added tests for the existing classes. I have never used this
many mocks in a test suite before, they definitely make the tests fast,
though I thought I might have used too many. I tried removing a few but
the tests could become may times slower or just never complete without
forcing the Gtk.main_loop to run.
    tests/test_ui.py

I then added test for new methods and refactored some of the code.
* I extracted the main_iteration code in one method to a decorator
  function that I used on several progress bar methods to ensure
  the UI updates immediately.
* I added the Info, Warning, and Error dialogs and refactored PromptDialog
  to extend Gtk.MessageDialog. This provides an icon with the text
  message, as well a guarantees that the spacing and layout conforms
  to Gnome HIG.
* After I extracted the main_loop iteration rules from update(), changed
  self.fraction to fraction because nothing used it. I changed the error
  to a ValueError because it can only be caused by insane inputs.
* I extracted the common progress window and panel methods to
  ProgressContainerMixin which was easy since I had already written
  tests to that used a similar mixin to verify their function.
* I updated ProgressPanel to GtkBox since GtkHBox is deprecated.
* I added show_user_warning, which is almost identical to the TextUI
  implementation.
* I added ._progress_updated() and .report_transport_activity() which made
  the gpush progress bar show sooner and update more often.
    ui.py
    tests/test_ui.py

To post a comment you must log in.
Revision history for this message
Jelmer Vernooij (jelmer) wrote :

This is *really* nice. Thanks, Curtis!

review: Approve
lp:~sinzui/bzr-gtk/ui-factory updated
779. By Curtis Hovey

Merged GtkUIFactory additions and tweaks.

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

This seems to break 'bzr selftest -s bp.gtk'.

review: Needs Fixing
Revision history for this message
Curtis Hovey (sinzui) wrote :

I suck. I will fix this right away.

lp:~sinzui/bzr-gtk/ui-factory updated
780. By Curtis Hovey

Support selftest, check, and check -m

781. By Curtis Hovey

DRY.

Revision history for this message
Curtis Hovey (sinzui) wrote :

I merged this branch into trunk a few hours before you discovered my badness. I have a branch that fixes this issue: https://code.launchpad.net/~sinzui/bzr-gtk/setup-fix/+merge/95013

Revision history for this message
Curtis Hovey (sinzui) wrote :

Oh never mind. either I never pushed my branch or you reverted. I have pushed my changes to this branch and these are my changes I reported in the other MP that I am deleting.

RULES

    * Consider reverting the changes if the fix cannot be made in a few
      hours
    * Setup.py can set the module to None, self tests passes a module
      object, and the user -m arg can be a string
      * Only filter the discovered tests if the module is a basestring.
      * ADDENDUM: It is trick using the native discover feature because
        it builds a module name in a different name space; not
        bzrlib.plugins.gtk. A listing of the directory using the same
        rules as discover might work to construct a module name that
        always works.

TEST

    ./setup.py check
    ./setup.py check -m ui
    BZR_PLUGINS_AT=gtk@./ bzr selftest -s bp.gtk

IMPLEMENTATION

I changed the default module in setup.py to None so that it was easy
to detect a basestring to select a test. I wrote my own function to
find tests module names because discover was not construction the module
name of bzrlib.plugins.gtk when run under selftest. The function uses
the same file matching rules as discover, but always uses the current
module name to construct the test module name. There are still few lines
of code to load tests then there were before.

Revision history for this message
Jelmer Vernooij (jelmer) wrote :

Thanks!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'setup.py'
2--- setup.py 2011-11-06 00:49:02 +0000
3+++ setup.py 2012-02-28 18:15:21 +0000
4@@ -1,33 +1,47 @@
5 #!/usr/bin/python
6 """GTK+ Frontends for various Bazaar commands."""
7
8-from info import *
9+import os
10+import sys
11+
12+from info import bzr_plugin_version
13
14 from distutils.core import setup, Command
15 from distutils.command.install_data import install_data
16 from distutils.command.build import build
17 from distutils.command.sdist import sdist
18 try:
19- from DistUtilsExtra.command import *
20+ from DistUtilsExtra.command import build_i18n
21 except ImportError:
22- # Python distutils extra is not available.
23- class cmd_build_i18n(Command):
24+ # Python distutils extra is not available.
25+ class cmd_build_i18n(build):
26+ user_options = []
27+
28+ def initialize_options(self):
29+ self.domain = None
30+ self.desktop_files = None
31+
32+ def finalize_options(self):
33+ pass
34+
35 def run(self):
36- print >> sys.stderr, "For internationalization support you'll need to install https://launchpad.net/python-distutils-extra"
37+ print >> sys.stderr, (
38+ "For internationalization support you'll need to install "
39+ "https://launchpad.net/python-distutils-extra")
40 else:
41 # Use build_i18n from DistUtilsExtra
42 cmd_build_i18n = build_i18n.build_i18n
43
44-import os
45-import sys
46
47 class Check(Command):
48 description = "Run unit tests"
49
50- user_options = []
51+ user_options = [
52+ ('module=', 'm', 'The test module to run'),
53+ ]
54
55 def initialize_options(self):
56- pass
57+ self.module = None
58
59 def finalize_options(self):
60 pass
61@@ -38,11 +52,12 @@
62 def run(self):
63 from bzrlib.tests import TestLoader, TestSuite, TextTestRunner
64 from bzrlib.plugin import PluginImporter
65- PluginImporter.specific_paths["bzrlib.plugins.gtk"] = os.path.dirname(__file__)
66+ PluginImporter.specific_paths["bzrlib.plugins.gtk"] = os.path.dirname(
67+ __file__)
68 from bzrlib.plugins.gtk.tests import load_tests
69 suite = TestSuite()
70 loader = TestLoader()
71- load_tests(suite, None, loader)
72+ load_tests(suite, self.module, loader)
73 runner = TextTestRunner()
74 result = runner.run(suite)
75 return result.wasSuccessful()
76@@ -63,7 +78,8 @@
77 return 'build_credits'
78
79 def run(self):
80- from bzrlib.plugin import load_plugins; load_plugins()
81+ from bzrlib.plugin import load_plugins
82+ load_plugins()
83 from bzrlib.branch import Branch
84 from bzrlib.plugins.stats.cmds import find_credits
85
86@@ -126,15 +142,15 @@
87 version = bzr_plugin_version[:3]
88 version_string = ".".join([str(x) for x in version])
89 setup(
90- name = "bzr-gtk",
91- version = version_string,
92- maintainer = "Jelmer Vernooij",
93- maintainer_email = "jelmer@samba.org",
94- description = "GTK+ Frontends for various Bazaar commands",
95- license = "GNU GPL v2 or later",
96- scripts = ['bzr-handle-patch', 'bzr-notify'],
97- url = "http://bazaar-vcs.org/BzrGtk",
98- package_dir = {
99+ name="bzr-gtk",
100+ version=version_string,
101+ maintainer="Jelmer Vernooij",
102+ maintainer_email="jelmer@samba.org",
103+ description="GTK+ Frontends for various Bazaar commands",
104+ license="GNU GPL v2 or later",
105+ scripts=['bzr-handle-patch', 'bzr-notify'],
106+ url="http://bazaar-vcs.org/BzrGtk",
107+ package_dir={
108 "bzrlib.plugins.gtk": ".",
109 "bzrlib.plugins.gtk.viz": "viz",
110 "bzrlib.plugins.gtk.annotate": "annotate",
111@@ -142,7 +158,7 @@
112 "bzrlib.plugins.gtk.branchview": "branchview",
113 "bzrlib.plugins.gtk.preferences": "preferences",
114 },
115- packages = [
116+ packages=[
117 "bzrlib.plugins.gtk",
118 "bzrlib.plugins.gtk.viz",
119 "bzrlib.plugins.gtk.annotate",
120@@ -150,7 +166,7 @@
121 "bzrlib.plugins.gtk.branchview",
122 "bzrlib.plugins.gtk.preferences",
123 ],
124- data_files=[ ('share/bzr-gtk', ['credits.pickle']),
125+ data_files=[('share/bzr-gtk', ['credits.pickle']),
126 ('share/bzr-gtk/icons', ['icons/commit.png',
127 'icons/commit16.png',
128 'icons/diff.png',
129@@ -176,7 +192,8 @@
130 'bzr-notify.desktop']),
131 ('share/application-registry', ['bzr-gtk.applications']),
132 ('share/pixmaps', ['icons/bzr-icon-64.png']),
133- ('share/icons/hicolor/scalable/apps', ['icons/bzr-panel.svg']),
134+ ('share/icons/hicolor/scalable/apps',
135+ ['icons/bzr-panel.svg']),
136 ('share/icons/hicolor/scalable/emblems',
137 ['icons/emblem-bzr-added.svg',
138 'icons/emblem-bzr-conflict.svg',
139
140=== modified file 'tests/__init__.py'
141--- tests/__init__.py 2012-02-03 18:59:38 +0000
142+++ tests/__init__.py 2012-02-28 18:15:21 +0000
143@@ -14,40 +14,72 @@
144 # along with this program; if not, write to the Free Software
145 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
146
147+__all__ = [
148+ 'load_tests',
149+ 'MockMethod',
150+ 'MockProperty',
151+ ]
152+
153+import os
154+
155+
156+def discover_test_names(module_or_name):
157+ if isinstance(module_or_name, basestring):
158+ match = module_or_name
159+ else:
160+ match = ''
161+ file_names = os.listdir(os.path.dirname(__file__))
162+ test_names = set()
163+ for file_name in file_names:
164+ name, ext = os.path.splitext(file_name)
165+ if name.startswith('test_') and ext == '.py' and match in name:
166+ test_names.add("%s.%s" % (__name__, name))
167+ return test_names
168+
169
170 def load_tests(basic_tests, module, loader):
171- testmod_names = [
172- 'test_annotate_config',
173- 'test_avatarsbox',
174- 'test_commit',
175- 'test_diff',
176- 'test_history',
177- 'test_graphcell',
178- 'test_linegraph',
179- 'test_notify',
180- 'test_revisionview',
181- 'test_treemodel',
182- ]
183-
184- basic_tests.addTest(loader.loadTestsFromModuleNames(
185- ["%s.%s" % (__name__, tmn) for tmn in testmod_names]))
186+ test_names = discover_test_names(module)
187+ basic_tests.addTest(loader.loadTestsFromModuleNames(test_names))
188 return basic_tests
189
190
191-class MockMethod():
192+class MockMethod(object):
193
194 @classmethod
195- def bind(klass, test_instance, obj, method_name):
196+ def bind(klass, test_instance, obj, method_name, return_value=None):
197 original_method = getattr(obj, method_name)
198 test_instance.addCleanup(setattr, obj, method_name, original_method)
199- setattr(obj, method_name, klass())
200+ setattr(obj, method_name, klass(return_value))
201
202- def __init__(self):
203+ def __init__(self, return_value=None):
204 self.called = False
205+ self.call_count = 0
206 self.args = None
207 self.kwargs = None
208+ self.return_value = return_value
209
210 def __call__(self, *args, **kwargs):
211 self.called = True
212+ self.call_count += 1
213 self.args = args
214 self.kwargs = kwargs
215+ return self.return_value
216+
217+
218+class MockProperty(MockMethod):
219+
220+ @classmethod
221+ def bind(klass, test_instance, obj, method_name, return_value=None):
222+ original_method = getattr(obj, method_name)
223+ test_instance.addCleanup(setattr, obj, method_name, original_method)
224+ mock = klass(return_value)
225+ setattr(obj, method_name, property(mock.get_value, mock.set_value))
226+ return mock
227+
228+ def get_value(self, other):
229+ self.called = True
230+ return self.return_value
231+
232+ def set_value(self, other, value):
233+ self.called = True
234+ self.return_value = value
235
236=== added file 'tests/test_ui.py'
237--- tests/test_ui.py 1970-01-01 00:00:00 +0000
238+++ tests/test_ui.py 2012-02-28 18:15:21 +0000
239@@ -0,0 +1,375 @@
240+# Copyright (C) 2012 Curtis Hovey <sinzui.is@verizon.net>
241+#
242+# This program is free software; you can redistribute it and/or modify
243+# it under the terms of the GNU General Public License as published by
244+# the Free Software Foundation; either version 2 of the License, or
245+# (at your option) any later version.
246+#
247+# This program is distributed in the hope that it will be useful,
248+# but WITHOUT ANY WARRANTY; without even the implied warranty of
249+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
250+# GNU General Public License for more details.
251+#
252+# You should have received a copy of the GNU General Public License
253+# along with this program; if not, write to the Free Software
254+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
255+
256+"""Test the ui functionality."""
257+
258+from gi.repository import Gtk
259+
260+from bzrlib import (
261+ tests,
262+ )
263+
264+from bzrlib.plugins.gtk import ui
265+from bzrlib.plugins.gtk.tests import (
266+ MockMethod,
267+ MockProperty,
268+ )
269+from bzrlib.progress import ProgressTask
270+
271+
272+class MainIterationTestCase(tests.TestCase):
273+
274+ def test_main_iteration(self):
275+ # The main_iteration decorator iterates over the pending Gtk events
276+ # after calling its function so that the UI is updated too.
277+ button = Gtk.ToggleButton(label='before')
278+
279+ def event_listener(button):
280+ button.props.label = 'after'
281+
282+ button.connect('clicked', event_listener)
283+
284+ def test_func(self):
285+ button.emit('clicked')
286+ return True
287+
288+ decorated_func = ui.main_iteration(test_func)
289+ result = decorated_func(object())
290+ self.assertIs(True, result)
291+ self.assertIs(False, Gtk.events_pending())
292+ self.assertEqual('after', button.props.label)
293+
294+
295+class PromptDialogTestCase(tests.TestCase):
296+
297+ def test_init(self):
298+ # The text and buttons are created.
299+ dialog = ui.PromptDialog('test 123')
300+ self.assertEqual('test 123', dialog.props.text)
301+ self.assertEqual(Gtk.MessageType.QUESTION, dialog.props.message_type)
302+ buttons = dialog.get_action_area().get_children()
303+ self.assertEqual('gtk-yes', buttons[0].props.label)
304+ self.assertEqual('gtk-no', buttons[1].props.label)
305+
306+
307+class InfoDialogTestCase(tests.TestCase):
308+
309+ def test_init(self):
310+ # The text and buttons are created.
311+ dialog = ui.InfoDialog('test 123')
312+ self.assertEqual('test 123', dialog.props.text)
313+ self.assertEqual(Gtk.MessageType.INFO, dialog.props.message_type)
314+ buttons = dialog.get_action_area().get_children()
315+ self.assertEqual('gtk-close', buttons[0].props.label)
316+
317+
318+class WarningDialogTestCase(tests.TestCase):
319+
320+ def test_init(self):
321+ # The text and buttons are created.
322+ dialog = ui.WarningDialog('test 123')
323+ self.assertEqual('test 123', dialog.props.text)
324+ self.assertEqual(Gtk.MessageType.WARNING, dialog.props.message_type)
325+ buttons = dialog.get_action_area().get_children()
326+ self.assertEqual('gtk-close', buttons[0].props.label)
327+
328+
329+class ErrorDialogTestCase(tests.TestCase):
330+
331+ def test_init(self):
332+ # The text and buttons are created, then shown.
333+ dialog = ui.ErrorDialog('test 123')
334+ self.assertEqual('test 123', dialog.props.text)
335+ self.assertEqual(Gtk.MessageType.ERROR, dialog.props.message_type)
336+ buttons = dialog.get_action_area().get_children()
337+ self.assertEqual('gtk-close', buttons[0].props.label)
338+
339+
340+class PasswordDialogTestCase(tests.TestCase):
341+
342+ def test_init(self):
343+ # The label, password entry, and buttons are created, then shown.
344+ MockMethod.bind(self, Gtk.Box, 'show_all')
345+ dialog = ui.PasswordDialog('test password')
346+ content_area = dialog.get_content_area()
347+ self.assertIs(True, dialog.get_content_area().show_all.called)
348+ widgets = content_area.get_children()
349+ self.assertEqual('test password', widgets[0].props.label)
350+ self.assertEqual(False, widgets[1].props.visibility)
351+ buttons = dialog.get_action_area().get_children()
352+ self.assertEqual('gtk-cancel', buttons[0].props.label)
353+ self.assertEqual(
354+ Gtk.ResponseType.CANCEL,
355+ dialog.get_response_for_widget(buttons[0]))
356+ self.assertEqual('gtk-ok', buttons[1].props.label)
357+ self.assertEqual(
358+ Gtk.ResponseType.OK,
359+ dialog.get_response_for_widget(buttons[1]))
360+
361+
362+class GtkProgressBarTestCase(tests.TestCase):
363+
364+ def test_init(self):
365+ progress_bar = ui.GtkProgressBar()
366+ self.assertEqual(0.0, progress_bar.props.fraction)
367+ self.assertIs(None, progress_bar.total)
368+ self.assertIs(None, progress_bar.current)
369+
370+ def test_tick(self):
371+ # tick() shows the widget, does one pulse, then handles the pending
372+ # events in the main loop.
373+ MockMethod.bind(self, ui.GtkProgressBar, 'show')
374+ MockMethod.bind(self, ui.GtkProgressBar, 'pulse')
375+ progress_bar = ui.GtkProgressBar()
376+ progress_bar.tick()
377+ self.assertIs(True, progress_bar.show.called)
378+ self.assertEqual('with_main_iteration', progress_bar.tick.__name__)
379+
380+ def test_update_with_data(self):
381+ # update() shows the widget, sets the fraction, then handles the
382+ # pending events in the main loop.
383+ MockMethod.bind(self, ui.GtkProgressBar, 'show')
384+ progress_bar = ui.GtkProgressBar()
385+ progress_bar.update(msg='test', current_cnt=5, total_cnt=10)
386+ self.assertIs(True, progress_bar.show.called)
387+ self.assertEqual(0.5, progress_bar.props.fraction)
388+ self.assertEqual(10, progress_bar.total)
389+ self.assertEqual(5, progress_bar.current)
390+ self.assertEqual('with_main_iteration', progress_bar.update.__name__)
391+
392+ def test_update_without_data(self):
393+ progress_bar = ui.GtkProgressBar()
394+ progress_bar.update(current_cnt=5, total_cnt=None)
395+ self.assertEqual(0.0, progress_bar.props.fraction)
396+ self.assertIs(None, progress_bar.total)
397+ self.assertEqual(5, progress_bar.current)
398+
399+ def test_update_with_insane_data(self):
400+ # The fraction must be between 0.0 and 1.0.
401+ progress_bar = ui.GtkProgressBar()
402+ self.assertRaises(
403+ ValueError, progress_bar.update, None, 20, 2)
404+
405+ def test_finished(self):
406+ # finished() hides the widget, resets the state, then handles the
407+ # pending events in the main loop.
408+ MockMethod.bind(self, ui.GtkProgressBar, 'hide')
409+ progress_bar = ui.GtkProgressBar()
410+ progress_bar.finished()
411+ self.assertIs(True, progress_bar.hide.called)
412+ self.assertEqual(0.0, progress_bar.props.fraction)
413+ self.assertIs(None, progress_bar.total)
414+ self.assertIs(None, progress_bar.current)
415+ self.assertEqual('with_main_iteration', progress_bar.finished.__name__)
416+
417+ def test_clear(self):
418+ # clear() is synonymous with finished.
419+ MockMethod.bind(self, ui.GtkProgressBar, 'finished')
420+ progress_bar = ui.GtkProgressBar()
421+ progress_bar.finished()
422+ self.assertIs(True, progress_bar.finished.called)
423+
424+
425+class ProgressContainerMixin:
426+
427+ def test_tick(self):
428+ progress_widget = self.progress_container()
429+ MockMethod.bind(self, progress_widget, 'show_all')
430+ MockMethod.bind(self, progress_widget.pb, 'tick')
431+ progress_widget.tick()
432+ self.assertIs(True, progress_widget.show_all.called)
433+ self.assertIs(True, progress_widget.pb.tick.called)
434+
435+ def test_update(self):
436+ progress_widget = self.progress_container()
437+ MockMethod.bind(self, progress_widget, 'show_all')
438+ MockMethod.bind(self, progress_widget.pb, 'update')
439+ progress_widget.update('test', 5, 10)
440+ self.assertIs(True, progress_widget.show_all.called)
441+ self.assertIs(True, progress_widget.pb.update.called)
442+ self.assertEqual(
443+ ('test', 5, 10), progress_widget.pb.update.args)
444+
445+ def test_finished(self):
446+ progress_widget = self.progress_container()
447+ MockMethod.bind(self, progress_widget, 'hide')
448+ MockMethod.bind(self, progress_widget.pb, 'finished')
449+ progress_widget.finished()
450+ self.assertIs(True, progress_widget.hide.called)
451+ self.assertIs(True, progress_widget.pb.finished.called)
452+
453+ def test_clear(self):
454+ progress_widget = self.progress_container()
455+ MockMethod.bind(self, progress_widget, 'hide')
456+ MockMethod.bind(self, progress_widget.pb, 'clear')
457+ progress_widget.clear()
458+ self.assertIs(True, progress_widget.hide.called)
459+ self.assertIs(True, progress_widget.pb.clear.called)
460+
461+
462+class ProgressBarWindowTestCase(ProgressContainerMixin, tests.TestCase):
463+
464+ progress_container = ui.ProgressBarWindow
465+
466+ def test_init(self):
467+ pb_window = ui.ProgressBarWindow()
468+ self.assertEqual('Progress', pb_window.props.title)
469+ self.assertEqual(
470+ Gtk.WindowPosition.CENTER_ALWAYS, pb_window.props.window_position)
471+ self.assertIsInstance(pb_window.pb, ui.GtkProgressBar)
472+
473+
474+class ProgressPanelTestCase(ProgressContainerMixin, tests.TestCase):
475+
476+ progress_container = ui.ProgressPanel
477+
478+ def test_init(self):
479+ pb_window = ui.ProgressPanel()
480+ self.assertEqual(
481+ Gtk.Orientation.HORIZONTAL, pb_window.props.orientation)
482+ self.assertEqual(5, pb_window.props.spacing)
483+ self.assertIsInstance(pb_window.pb, ui.GtkProgressBar)
484+ widgets = pb_window.get_children()
485+ # The image's stock and icon_name properties are always None?
486+ self.assertIsInstance(widgets[0], Gtk.Image)
487+
488+
489+class GtkUIFactoryTestCase(tests.TestCase):
490+
491+ def test__init(self):
492+ ui_factory = ui.GtkUIFactory()
493+ self.assertIs(None, ui_factory._progress_bar_widget)
494+
495+ def test_set_progress_bar_widget(self):
496+ ui_factory = ui.GtkUIFactory()
497+ progress_widget = ui.ProgressPanel()
498+ ui_factory.set_progress_bar_widget(progress_widget)
499+ self.assertIs(progress_widget, ui_factory._progress_bar_widget)
500+
501+ def test_get_boolean_true(self):
502+ ui_factory = ui.GtkUIFactory()
503+ MockMethod.bind(self, ui.PromptDialog, 'run', Gtk.ResponseType.YES)
504+ boolean_value = ui_factory.get_boolean('test')
505+ self.assertIs(True, ui.PromptDialog.run.called)
506+ self.assertIs(True, boolean_value)
507+
508+ def test_get_boolean_false(self):
509+ ui_factory = ui.GtkUIFactory()
510+ MockMethod.bind(self, ui.PromptDialog, 'run', Gtk.ResponseType.NO)
511+ boolean_value = ui_factory.get_boolean('test')
512+ self.assertIs(True, ui.PromptDialog.run.called)
513+ self.assertIs(False, boolean_value)
514+
515+ def test_show_message(self):
516+ ui_factory = ui.GtkUIFactory()
517+ MockMethod.bind(self, ui.InfoDialog, 'run', Gtk.ResponseType.CLOSE)
518+ ui_factory.show_message('test')
519+ self.assertIs(True, ui.InfoDialog.run.called)
520+
521+ def test_show_warning(self):
522+ ui_factory = ui.GtkUIFactory()
523+ MockMethod.bind(self, ui.WarningDialog, 'run', Gtk.ResponseType.CLOSE)
524+ ui_factory.show_warning('test')
525+ self.assertIs(True, ui.WarningDialog.run.called)
526+
527+ def test_show_Error(self):
528+ ui_factory = ui.GtkUIFactory()
529+ MockMethod.bind(self, ui.ErrorDialog, 'run', Gtk.ResponseType.CLOSE)
530+ ui_factory.show_error('test')
531+ self.assertIs(True, ui.ErrorDialog.run.called)
532+
533+ def test_show_user_warning(self):
534+ ui_factory = ui.GtkUIFactory()
535+ MockMethod.bind(self, ui.WarningDialog, 'run', Gtk.ResponseType.CLOSE)
536+ ui_factory.show_user_warning(
537+ 'recommend_upgrade', current_format_name='1.0', basedir='./test')
538+ self.assertIs(True, ui.WarningDialog.run.called)
539+
540+ def test_show_user_warning_supressed(self):
541+ ui_factory = ui.GtkUIFactory()
542+ ui_factory.suppressed_warnings.add('recommend_upgrade')
543+ MockMethod.bind(self, ui.WarningDialog, 'run', Gtk.ResponseType.CLOSE)
544+ ui_factory.show_user_warning(
545+ 'recommend_upgrade', current_format_name='1.0', basedir='./test')
546+ self.assertIs(False, ui.WarningDialog.run.called)
547+
548+ def test_get_password(self):
549+ ui_factory = ui.GtkUIFactory()
550+ MockMethod.bind(self, ui.PasswordDialog, 'run', Gtk.ResponseType.OK)
551+ mock_property = MockProperty.bind(
552+ self, ui.PasswordDialog, 'passwd', 'secret')
553+ password = ui_factory.get_password('test')
554+ self.assertIs(True, ui.PasswordDialog.run.called)
555+ self.assertIs(True, mock_property.called)
556+ self.assertEqual('secret', password)
557+
558+ def test_progress_all_finished_with_widget(self):
559+ ui_factory = ui.GtkUIFactory()
560+ progress_widget = ui.ProgressPanel()
561+ MockMethod.bind(self, progress_widget, 'finished')
562+ ui_factory.set_progress_bar_widget(progress_widget)
563+ self.assertIs(None, ui_factory._progress_all_finished())
564+ self.assertIs(True, progress_widget.finished.called)
565+
566+ def test_progress_all_finished_without_widget(self):
567+ ui_factory = ui.GtkUIFactory()
568+ self.assertIs(None, ui_factory._progress_all_finished())
569+
570+ def test_progress_updated_with_widget(self):
571+ ui_factory = ui.GtkUIFactory()
572+ progress_widget = ui.ProgressPanel()
573+ MockMethod.bind(self, progress_widget, 'update')
574+ ui_factory.set_progress_bar_widget(progress_widget)
575+ task = ProgressTask()
576+ task.msg = 'test'
577+ task.current_cnt = 1
578+ task.total_cnt = 2
579+ self.assertIs(None, ui_factory._progress_updated(task))
580+ self.assertIs(True, progress_widget.update.called)
581+ self.assertEqual(
582+ ('test', 1, 2), progress_widget.update.args)
583+
584+ def test_progress_updated_without_widget(self):
585+ ui_factory = ui.GtkUIFactory()
586+ MockMethod.bind(self, ui.ProgressBarWindow, 'update')
587+ task = ProgressTask()
588+ task.msg = 'test'
589+ task.current_cnt = 1
590+ task.total_cnt = 2
591+ self.assertIs(None, ui_factory._progress_updated(task))
592+ self.assertIsInstance(
593+ ui_factory._progress_bar_widget, ui.ProgressBarWindow)
594+ self.assertIs(True, ui_factory._progress_bar_widget.update.called)
595+ self.assertEqual(
596+ ('test', 1, 2), ui_factory._progress_bar_widget.update.args)
597+
598+ def test_report_transport_activity_with_widget(self):
599+ ui_factory = ui.GtkUIFactory()
600+ progress_widget = ui.ProgressPanel()
601+ MockMethod.bind(self, progress_widget, 'tick')
602+ ui_factory.set_progress_bar_widget(progress_widget)
603+ self.assertIs(
604+ None, ui_factory.report_transport_activity(None, None, None))
605+ self.assertIs(True, progress_widget.tick.called)
606+
607+ def test_report_transport_activity_without_widget(self):
608+ ui_factory = ui.GtkUIFactory()
609+ MockMethod.bind(self, ui.ProgressBarWindow, 'tick')
610+ self.assertIs(
611+ None, ui_factory.report_transport_activity(None, None, None))
612+ self.assertIsInstance(
613+ ui_factory._progress_bar_widget, ui.ProgressBarWindow)
614+ self.assertIs(True, ui.ProgressBarWindow.tick.called)
615
616=== modified file 'ui.py'
617--- ui.py 2011-09-08 03:11:06 +0000
618+++ ui.py 2012-02-28 18:15:21 +0000
619@@ -24,18 +24,45 @@
620 from bzrlib.ui import UIFactory
621
622
623-class PromptDialog(Gtk.Dialog):
624+def main_iteration(function):
625+ def with_main_iteration(self, *args, **kwargs):
626+ result = function(self, *args, **kwargs)
627+ while Gtk.events_pending():
628+ Gtk.main_iteration_do(False)
629+ return result
630+ return with_main_iteration
631+
632+
633+class PromptDialog(Gtk.MessageDialog):
634 """Prompt the user for a yes/no answer."""
635
636- def __init__(self, prompt):
637- super(PromptDialog, self).__init__()
638-
639- label = Gtk.Label(label=prompt)
640- self.get_content_area().pack_start(label, True, True, 10)
641- self.get_content_area().show_all()
642-
643- self.add_buttons(Gtk.STOCK_YES, Gtk.ResponseType.YES, Gtk.STOCK_NO,
644- Gtk.ResponseType.NO)
645+ def __init__(self, prompt, parent=None):
646+ super(PromptDialog, self).__init__(
647+ parent, Gtk.DialogFlags.DESTROY_WITH_PARENT,
648+ Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, prompt)
649+
650+
651+class InfoDialog(Gtk.MessageDialog):
652+ """Show the user an informational message."""
653+
654+ MESSAGE_TYPE = Gtk.MessageType.INFO
655+
656+ def __init__(self, prompt, parent=None):
657+ super(InfoDialog, self).__init__(
658+ parent, Gtk.DialogFlags.DESTROY_WITH_PARENT,
659+ self.MESSAGE_TYPE, Gtk.ButtonsType.CLOSE, prompt)
660+
661+
662+class WarningDialog(InfoDialog):
663+ """Show the user a warning message."""
664+
665+ MESSAGE_TYPE = Gtk.MessageType.WARNING
666+
667+
668+class ErrorDialog(InfoDialog):
669+ """Show the user a warning message."""
670+
671+ MESSAGE_TYPE = Gtk.MessageType.ERROR
672
673
674 class GtkProgressBar(Gtk.ProgressBar):
675@@ -46,10 +73,12 @@
676 self.current = None
677 self.total = None
678
679+ @main_iteration
680 def tick(self):
681 self.show()
682 self.pulse()
683
684+ @main_iteration
685 def update(self, msg=None, current_cnt=None, total_cnt=None):
686 self.show()
687 if current_cnt is not None:
688@@ -59,21 +88,43 @@
689 if msg is not None:
690 self.set_text(msg)
691 if None not in (self.current, self.total):
692- self.fraction = float(self.current) / self.total
693- if self.fraction < 0.0 or self.fraction > 1.0:
694- raise AssertionError
695- self.set_fraction(self.fraction)
696- while Gtk.events_pending():
697- Gtk.main_iteration()
698-
699- def finished(self):
700- self.hide()
701-
702- def clear(self):
703- self.hide()
704-
705-
706-class ProgressBarWindow(Gtk.Window):
707+ fraction = float(self.current) / self.total
708+ if fraction < 0.0 or fraction > 1.0:
709+ raise ValueError
710+ self.set_fraction(fraction)
711+
712+ @main_iteration
713+ def finished(self):
714+ self.set_fraction(0.0)
715+ self.current = None
716+ self.total = None
717+ self.hide()
718+
719+ def clear(self):
720+ self.finished()
721+
722+
723+class ProgressContainerMixin:
724+ """Expose GtkProgressBar methods to a container class."""
725+
726+ def tick(self, *args, **kwargs):
727+ self.show_all()
728+ self.pb.tick(*args, **kwargs)
729+
730+ def update(self, *args, **kwargs):
731+ self.show_all()
732+ self.pb.update(*args, **kwargs)
733+
734+ def finished(self):
735+ self.hide()
736+ self.pb.finished()
737+
738+ def clear(self):
739+ self.hide()
740+ self.pb.clear()
741+
742+
743+class ProgressBarWindow(ProgressContainerMixin, Gtk.Window):
744
745 def __init__(self):
746 super(ProgressBarWindow, self).__init__(type=Gtk.WindowType.TOPLEVEL)
747@@ -85,54 +136,20 @@
748 self.resize(250, 15)
749 self.set_resizable(False)
750
751- def tick(self, *args, **kwargs):
752- self.show_all()
753- self.pb.tick(*args, **kwargs)
754-
755- def update(self, *args, **kwargs):
756- self.show_all()
757- self.pb.update(*args, **kwargs)
758-
759- def finished(self):
760- self.pb.finished()
761- self.hide()
762- self.destroy()
763-
764- def clear(self):
765- self.pb.clear()
766- self.hide()
767-
768-
769-class ProgressPanel(Gtk.HBox):
770+
771+class ProgressPanel(ProgressContainerMixin, Gtk.Box):
772
773 def __init__(self):
774- super(ProgressPanel, self).__init__()
775+ super(ProgressPanel, self).__init__(Gtk.Orientation.HORIZONTAL, 5)
776 image_loading = Gtk.Image.new_from_stock(Gtk.STOCK_REFRESH,
777 Gtk.IconSize.BUTTON)
778 image_loading.show()
779
780 self.pb = GtkProgressBar()
781- self.set_spacing(5)
782 self.set_border_width(5)
783 self.pack_start(image_loading, False, False, 0)
784 self.pack_start(self.pb, True, True, 0)
785
786- def tick(self, *args, **kwargs):
787- self.show_all()
788- self.pb.tick(*args, **kwargs)
789-
790- def update(self, *args, **kwargs):
791- self.show_all()
792- self.pb.update(*args, **kwargs)
793-
794- def finished(self):
795- self.pb.finished()
796- self.hide()
797-
798- def clear(self):
799- self.pb.clear()
800- self.hide()
801-
802
803 class PasswordDialog(Gtk.Dialog):
804 """ Prompt the user for a password. """
805@@ -176,6 +193,30 @@
806 dialog.destroy()
807 return (response == Gtk.ResponseType.YES)
808
809+ def show_message(self, msg):
810+ """See UIFactory.show_message."""
811+ dialog = InfoDialog(msg)
812+ dialog.run()
813+ dialog.destroy()
814+
815+ def show_warning(self, msg):
816+ """See UIFactory.show_warning."""
817+ dialog = WarningDialog(msg)
818+ dialog.run()
819+ dialog.destroy()
820+
821+ def show_error(self, msg):
822+ """See UIFactory.show_error."""
823+ dialog = ErrorDialog(msg)
824+ dialog.run()
825+ dialog.destroy()
826+
827+ def show_user_warning(self, warning_id, **message_args):
828+ """See UIFactory.show_user_warning."""
829+ if warning_id not in self.suppressed_warnings:
830+ message = self.format_user_warning(warning_id, message_args)
831+ self.show_warning(message)
832+
833 def get_password(self, prompt='', **kwargs):
834 """Prompt the user for a password.
835
836@@ -183,7 +224,7 @@
837 :param kwargs: Arguments which will be expanded into the prompt.
838 This lets front ends display different things if
839 they so choose.
840- :return: The password string, return None if the user
841+ :return: The password string, return None if the user
842 canceled the request.
843 """
844 dialog = PasswordDialog(prompt % kwargs)
845@@ -196,17 +237,24 @@
846 return None
847
848 def _progress_all_finished(self):
849- """See UIFactory._progress_all_finished"""
850+ """See UIFactory._progress_all_finished."""
851 pbw = self._progress_bar_widget
852 if pbw:
853 pbw.finished()
854
855- def _progress_updated(self, task):
856- """See UIFactory._progress_updated"""
857+ def _ensure_progress_widget(self):
858 if self._progress_bar_widget is None:
859- # Default to a window since nobody gave us a better mean to report
860+ # Default to a window since nobody gave us a better means to report
861 # progress.
862 self.set_progress_bar_widget(ProgressBarWindow())
863+
864+ def _progress_updated(self, task):
865+ """See UIFactory._progress_updated."""
866+ self._ensure_progress_widget()
867 self._progress_bar_widget.update(task.msg,
868 task.current_cnt, task.total_cnt)
869
870+ def report_transport_activity(self, transport, byte_count, direction):
871+ """See UIFactory.report_transport_activity."""
872+ self._ensure_progress_widget()
873+ self._progress_bar_widget.tick()

Subscribers

People subscribed via source and target branches

to all changes: