[POS-commit] r5362 - in kiwi/trunk: . kiwi tests/ui

Johan Dahlin jdahlin at async.com.br
Tue Nov 21 14:21:07 BRST 2006


Author: jdahlin
Date: Tue Nov 21 14:21:07 2006
New Revision: 5362

Added:
   kiwi/trunk/kiwi/_kiwi.c
   kiwi/trunk/kiwi/ui/test/runner.py
Removed:
   kiwi/trunk/kiwi/ui/test/player.py
Modified:
   kiwi/trunk/Makefile
   kiwi/trunk/kiwi/   (props changed)
   kiwi/trunk/kiwi/ui/test/common.py
   kiwi/trunk/kiwi/ui/test/main.py
   kiwi/trunk/kiwi/ui/test/recorder.py
   kiwi/trunk/setup.py
   kiwi/trunk/tests/ui/diary.py
   kiwi/trunk/tests/ui/diary2.py
   kiwi/trunk/tests/ui/personalinformation.py

Log:
Rewrite UI testing framework to use doctests and a gdk event handler function. Port the old ui tests to the new syntax

Modified: kiwi/trunk/Makefile
==============================================================================
--- kiwi/trunk/Makefile	(original)
+++ kiwi/trunk/Makefile	Tue Nov 21 14:21:07 2006
@@ -6,6 +6,9 @@
 DEBVERSION=$(shell dpkg-parsechangelog -ldebian/changelog |grep Version|cut -d: -f3)
 DLDIR=/mondo/htdocs/download.stoq.com.br/ubuntu
 
+all:
+	python setup.py build_ext -i
+
 clean-docs:
 	rm -fr doc/api
 	rm -fr doc/howto
@@ -14,6 +17,7 @@
 	debclean
 	rm -fr $(BUILDDIR)
 	rm -f MANIFEST
+	rm -fr kiwi/_kiwi.so
 
 docs:
 	make -s -C doc api howto

Added: kiwi/trunk/kiwi/_kiwi.c
==============================================================================
--- (empty file)
+++ kiwi/trunk/kiwi/_kiwi.c	Tue Nov 21 14:21:07 2006
@@ -0,0 +1,266 @@
+/* -*- Mode: C; c-basic-offset: 4 -*-
+ * Kiwi: a Framework and Enhanced Widgets for Python
+ *
+ * Copyright (C) 2006 Async Open Source
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+ * USA
+ *
+ * Author(s): Johan Dahlin <jdahlin at async.com.br>
+ */
+
+/* This module contains backports of pygobject/pygtk functions,
+ * so we don't have to require the latest versions of them
+ */
+#include <Python.h>
+#include <pygobject.h>
+#include <pygtk/pygtk.h>
+
+#if PY_VERSION_HEX < 0x02050000
+typedef int Py_ssize_t;
+#define PY_SSIZE_T_MAX INT_MAX
+#define PY_SSIZE_T_MIN INT_MIN
+typedef inquiry lenfunc;
+#endif
+
+typedef struct {
+    PyObject *func, *data;
+} PyGtkCustomNotify;
+
+static void
+pygtk_custom_destroy_notify(gpointer user_data)
+{
+    PyGtkCustomNotify *cunote = user_data;
+    PyGILState_STATE state;
+
+    g_return_if_fail(user_data);
+    state = pyg_gil_state_ensure();
+    Py_XDECREF(cunote->func);
+    Py_XDECREF(cunote->data);
+    pyg_gil_state_release(state);
+    
+    g_free(cunote);
+}
+
+static gboolean
+marshal_emission_hook(GSignalInvocationHint *ihint,
+		      guint n_param_values,
+		      const GValue *param_values,
+		      gpointer user_data)
+{
+    PyGILState_STATE state;
+    gboolean retval = FALSE;
+    PyObject *func, *args;
+    PyObject *retobj;
+    PyObject *params;
+    guint i;
+
+    state = pyg_gil_state_ensure();
+
+    /* construct Python tuple for the parameter values */
+    params = PyTuple_New(n_param_values);
+
+    for (i = 0; i < n_param_values; i++) {
+	PyObject *item = pyg_value_as_pyobject(&param_values[i], FALSE);
+	
+	/* error condition */
+	if (!item) {
+	    goto out;
+	}
+	PyTuple_SetItem(params, i, item);
+    }
+
+    args = (PyObject *)user_data;
+    func = PyTuple_GetItem(args, 0);
+    args = PySequence_Concat(params, PyTuple_GetItem(args, 1));
+    Py_DECREF(params);
+
+    /* params passed to function may have extra arguments */
+
+    retobj = PyObject_CallObject(func, args);
+    Py_DECREF(args);
+    if (retobj == NULL) {
+        PyErr_Print();
+    }
+    
+    retval = (retobj == Py_True ? TRUE : FALSE);
+    Py_XDECREF(retobj);
+out:
+    pyg_gil_state_release(state);
+    return retval;
+}
+
+static PyObject *
+pyg_add_emission_hook(PyGObject *self, PyObject *args)
+{
+    PyObject *first, *callback, *extra_args, *data;
+    gchar *name;
+    gulong hook_id;
+    guint sigid;
+    Py_ssize_t len;
+    GQuark detail = 0;
+    GType gtype;
+    PyObject *pygtype;
+
+    len = PyTuple_Size(args);
+    if (len < 3) {
+	PyErr_SetString(PyExc_TypeError,
+			"gobject.add_emission_hook requires at least 3 arguments");
+	return NULL;
+    }
+    first = PySequence_GetSlice(args, 0, 3);
+    if (!PyArg_ParseTuple(first, "OsO:add_emission_hook",
+			  &pygtype, &name, &callback)) {
+	Py_DECREF(first);
+	return NULL;
+    }
+    Py_DECREF(first);
+    
+    if ((gtype = pyg_type_from_object(pygtype)) == 0) {
+	return NULL;
+    }
+    if (!PyCallable_Check(callback)) {
+	PyErr_SetString(PyExc_TypeError, "third argument must be callable");
+	return NULL;
+    }
+
+    if (!g_signal_parse_name(name, gtype, &sigid, &detail, TRUE)) {
+	PyErr_Format(PyExc_TypeError, "%s: unknown signal name: %s",
+		     PyString_AsString(PyObject_Repr((PyObject*)self)),
+		     name);
+	return NULL;
+    }
+    extra_args = PySequence_GetSlice(args, 3, len);
+    if (extra_args == NULL)
+	return NULL;
+
+    data = Py_BuildValue("(ON)", callback, extra_args);
+    if (data == NULL)
+      return NULL;
+    
+    hook_id = g_signal_add_emission_hook(sigid, detail,
+					 marshal_emission_hook,
+					 data,
+					 (GDestroyNotify)pyg_destroy_notify);
+        
+    return PyLong_FromUnsignedLong(hook_id);
+}
+
+static PyObject *
+pyg_remove_emission_hook(PyGObject *self, PyObject *args)
+{
+    PyObject *pygtype;
+    char *name;
+    guint signal_id;
+    gulong hook_id;
+    GType gtype;
+    
+    if (!PyArg_ParseTuple(args, "Osk:gobject.remove_emission_hook",
+			  &pygtype, &name, &hook_id))
+	return NULL;
+    
+    if ((gtype = pyg_type_from_object(pygtype)) == 0) {
+	return NULL;
+    }
+    
+    if (!g_signal_parse_name(name, gtype, &signal_id, NULL, TRUE)) {
+	PyErr_Format(PyExc_TypeError, "%s: unknown signal name: %s",
+		     PyString_AsString(PyObject_Repr((PyObject*)self)),
+		     name);
+	return NULL;
+    }
+
+    g_signal_remove_emission_hook(signal_id, hook_id);
+    
+    Py_INCREF(Py_None);
+    return Py_None;
+}
+
+static void
+pygdk_event_handler_marshal(GdkEvent *event, gpointer data)
+{
+    PyGILState_STATE state;
+    PyGtkCustomNotify *cunote = data;
+    PyObject *retobj;
+    PyObject *pyevent;
+
+    g_assert (cunote->func);
+
+    state = pyg_gil_state_ensure();
+
+    pyevent = pyg_boxed_new(GDK_TYPE_EVENT, event, TRUE, TRUE);
+    if (cunote->data)
+        retobj = PyEval_CallFunction(cunote->func, "(NO)",
+				     pyevent, cunote->data);
+    else
+        retobj = PyEval_CallFunction(cunote->func, "(N)", pyevent);
+
+    if (retobj == NULL) {
+        PyErr_Print();
+    } else
+        Py_DECREF(retobj);
+
+    pyg_gil_state_release(state);
+}
+
+static PyObject *
+_wrap_gdk_event_handler_set(PyObject *self, PyObject *args, PyObject *kwargs)
+{
+    PyObject *pyfunc, *pyarg = NULL;
+    PyGtkCustomNotify *cunote;
+
+    if (!PyArg_ParseTuple(args, "O|O:event_handler_set",
+                          &pyfunc, &pyarg))
+        return NULL;
+
+    if (pyfunc == Py_None) {
+	gdk_event_handler_set(NULL, NULL, NULL);
+    } else {
+	cunote = g_new0(PyGtkCustomNotify, 1);
+	cunote->func = pyfunc;
+	cunote->data = pyarg;
+	Py_INCREF(cunote->func);
+	Py_XINCREF(cunote->data);
+
+	gdk_event_handler_set(pygdk_event_handler_marshal,
+			      cunote,
+			      pygtk_custom_destroy_notify);
+    }
+
+    Py_INCREF(Py_None);
+    return Py_None;
+}
+
+static PyMethodDef _kiwi_functions[] = {
+    { "add_emission_hook",
+      (PyCFunction)pyg_add_emission_hook, METH_VARARGS },
+    { "remove_emission_hook",
+      (PyCFunction)pyg_remove_emission_hook, METH_VARARGS },
+    { "event_handler_set",
+      (PyCFunction)_wrap_gdk_event_handler_set, METH_VARARGS },
+
+    { NULL, NULL, 0 }
+};
+
+DL_EXPORT(void)
+init_kiwi(void)
+{
+
+  init_pygobject();
+  init_pygtk();
+  
+  Py_InitModule("kiwi._kiwi", _kiwi_functions);
+
+}

Modified: kiwi/trunk/kiwi/ui/test/common.py
==============================================================================
--- kiwi/trunk/kiwi/ui/test/common.py	(original)
+++ kiwi/trunk/kiwi/ui/test/common.py	Tue Nov 21 14:21:07 2006
@@ -29,25 +29,123 @@
 
 import gobject
 import gtk
+from gtk import gdk
 
-class Base(object):
-    """
-    Base class used by L{kiwi.ui.test.recorder.Recorder} and
-    L{kiwi.ui.test.player.Player}.
-
-    The most important functionallity here is the ability track
-    creation and destruction of windows. When a new window is
-    located all the children are traversed recursively.
-    """
+from kiwi.utils import gsignal
+
+try:
+    from gtk.gdk import event_handler_set
+except ImportError:
+    try:
+        from kiwi._kiwi import event_handler_set
+    except ImportError:
+        event_handler_set = None
+
+class WidgetIntrospecter(gobject.GObject):
+    gsignal('window-added', object, str, object)
+    gsignal('window-removed', object, str)
 
     def __init__(self):
-        self._windows = {}
-        self._window_list = self._list_windows()
-        gobject.timeout_add(25, self._check_windows)
+        gobject.GObject.__init__(self)
         self._objects = {}
+        self._id_to_obj = {} # GdkWindow -> GtkWindow
+        self._windows = {} # toplevels ?
+
+    def _event_handler(self, event):
+        # Separate method so we can use return inside
+        self._check_event(event)
+        gtk.main_do_event(event)
+
+    def _check_event(self, event):
+        if not event.window:
+            return
+
+        window = event.window
+        event_type = event.type
+        window_type = window.get_window_type()
+        try:
+            widget = window.get_user_data()
+        except ValueError:
+            widget = self._id_to_obj.get(window)
+
+        if not isinstance(widget, gtk.Window):
+            return
+        widget_name = widget.get_name()
+
+        if event_type == gdk.MAP:
+            if window_type != gdk.WINDOW_TOPLEVEL:
+                # For non toplevels we only care about those which has a menu
+                # as the child
+                child = widget.child
+                if not child or not isinstance(child, gtk.Menu):
+                    return
+
+                # Hack to get all the children of a popup menu in
+                # the same namespace as the window they were launched in.
+                parent_menu = child.get_data('parent-menu')
+                if parent_menu:
+                    main = parent_menu.get_toplevel()
+                    widget_name = main.get_name()
+            else:
+                self._window_added(widget, widget_name)
+                self._id_to_obj[window] = widget
+        elif (event_type == gdk.DELETE or
+              (event_type == gdk.WINDOW_STATE and
+               event.new_window_state == gdk.WINDOW_STATE_WITHDRAWN)):
+            self._window_removed(widget, widget_name)
+
+    def _window_added(self, window, name):
+        if name in self._windows:
+            return
+        self._windows[name] = window
+
+        # Toplevel
+        self.parse_one(window, window)
+        ns = self._objects[name]
+        self.emit('window-added', window, name, ns)
+
+    def _window_removed(self, window, name):
+        if not name in self._windows:
+            # Error?
+            return
+
+        del self._windows[name]
+        self.emit('window-removed', window, name)
+
+    def _add_widget(self, toplevel, widget, name):
+        toplevel_widgets = self._objects.setdefault(toplevel.get_name(), {})
+        if name in toplevel_widgets:
+            return
+
+        toplevel_widgets[name] = widget
+
+        # Listen to when the widget is removed from the interface, eg when
+        # ::parent changes to None. At that time remove the widget and all
+        # the children from the namespace.
+
+        def on_widget__notify_parent(widget, pspec, name, widgets,
+                                     signal_container):
+            # Only take action when the widget is removed from a parent
+            if widget.parent is not None:
+                return
+
+            for child_name, child in widgets.items():
+                if child.is_ancestor(widget):
+                    del widgets[child_name]
+            widget.disconnect(signal_container.pop())
+
+        signal_container = []
+        sig_id = widget.connect('notify::parent', on_widget__notify_parent,
+                                name, toplevel_widgets, signal_container)
+        signal_container.append(sig_id)
 
     # Public API
 
+    def register_event_handler(self):
+        if not event_handler_set:
+            raise NotImplementedError
+        event_handler_set(self._event_handler)
+
     def parse_one(self, toplevel, gobj):
         """
         @param toplevel:
@@ -69,96 +167,15 @@
 
             gtype = gobject.type_parent(gtype)
 
-    def get_object(self, attr):
-        """
-        @param attr: name of toplevel object to get
-        @returns: toplevel object
-        """
-        return self._objects[attr]
-
-    # Override in subclass
-
-    def window_added(self, window):
-        """
-        This will be called when a window is displayed
-        @param window:
-        """
-
-    def window_removed(self, window):
-        """
-        This will be called when a window is destroyed
-        @param window:
-        """
-
-    # Private
-
-    def _on_window_name_change(self, window, pspec, old_name):
-        # Update datastructures, no need to notify that the dialog
-        # was added, we already know about it and all its children
-        self._windows[window.get_name()] = self._windows.pop(old_name)
-
-    def _list_windows(self):
-        # We're only interested in toplevels for now, tooltip windows are
-        # popups for example
-        rv = []
-        for window in gtk.window_list_toplevels():
-            if window.type != gtk.WINDOW_TOPLEVEL:
-                if not isinstance(window.child, gtk.Menu):
-                    continue
-
-                # Hack to get all the entries of a popup menu in
-                # the same namespace as the window they were launched
-                # in.
-                parent_menu = window.child.get_data('parent-menu')
-                if parent_menu:
-                    main = parent_menu.get_toplevel()
-                    rv.append((main.get_name(), window))
-            else:
-                rv.append((window.get_name(), window))
-
-        return sets.Set(rv)
-
-    def _check_windows(self):
-        new_windows = self._list_windows()
-        if self._windows != new_windows:
-            for name, window in new_windows.difference(self._window_list):
-                # Popup window, eg menu popups needs to be treated
-                # specially, only parse the contained widgets, do not
-                # add it or listen to name changes, we don't care about them
-                if window.type == gtk.WINDOW_POPUP:
-                    # XXX: This is trigged by stoq.test.gui.salewizard
-                    if name in self._windows:
-                        toplevel = self._windows[name]
-                        self.parse_one(toplevel, window)
-                else:
-                    self.parse_one(window, window)
-
-                    window.connect('notify::name', self._on_window_name_change,
-                                   window.get_name())
-                    self.window_added(window)
-                    self._windows[name] = window
-
-            for name, window in self._window_list.difference(new_windows):
-                # We don care about popup windows, see above
-                if window.type == gtk.WINDOW_POPUP:
-                    continue
-
-                self.window_removed(window)
-                del self._windows[name]
-
-            self._window_list = new_windows
-        return True
+    #
+    # Special widget handling
+    #
 
     def ignore(self, toplevel, gobj):
         pass
 
     GtkSeparatorMenuItem = GtkTearoffMenuItem = ignore
 
-    def _add_widget(self, toplevel, widget, name):
-        toplevel_widgets = self._objects.setdefault(toplevel.get_name(), {})
-        if not name in toplevel_widgets:
-            toplevel_widgets[name] = widget
-
     def GtkWidget(self, toplevel, widget):
         """
         Called when a GtkWidget is about to be traversed
@@ -204,3 +221,5 @@
 
     def GtkToolButton(self, toplevel, item):
         item.child.set_name(item.get_name())
+
+gobject.type_register(WidgetIntrospecter)

Modified: kiwi/trunk/kiwi/ui/test/main.py
==============================================================================
--- kiwi/trunk/kiwi/ui/test/main.py	(original)
+++ kiwi/trunk/kiwi/ui/test/main.py	Tue Nov 21 14:21:07 2006
@@ -28,23 +28,29 @@
 from kiwi.log import set_log_level
 
 def _play(options, filename, args):
-    from kiwi.ui.test.player import play_file
+    from kiwi.ui.test.runner import play_file
 
-    play_file(filename, args)
+    play_file(filename, options.command, args)
 
 def _record(options, filename, args):
     from kiwi.ui.test.recorder import Recorder
 
-    Recorder(filename, args)
+    recorder = Recorder(filename)
+    recorder.execute(args)
 
 def main(args):
     parser = optparse.OptionParser()
+    parser.add_option('', '--command', action="store",
+                      dest="command")
     parser.add_option('', '--record', action="store",
                       dest="record")
     parser.add_option('-v', '--verbose', action="store_true",
                       dest="verbose")
     options, args = parser.parse_args(args)
 
+    if options.record and options.command:
+        raise SystemExit(
+            "You can't specify a command and recording at the same time")
     if options.record:
         if options.verbose:
             set_log_level('recorder', 5)

Modified: kiwi/trunk/kiwi/ui/test/recorder.py
==============================================================================
--- kiwi/trunk/kiwi/ui/test/recorder.py	(original)
+++ kiwi/trunk/kiwi/ui/test/recorder.py	Tue Nov 21 14:21:07 2006
@@ -27,7 +27,8 @@
 This module provides an interface for creating, listening to
 and saving events.
 It uses the gobject introspection base class
-L{kiwi.ui.test.common.Base} to gather widgets, windows and other objects.
+L{kiwi.ui.test.common.WidgetIntrospecter} to gather widgets, windows and
+other objects.
 
 The user interfaces are saved in a format so they can easily be played
 back by simply executing the script through a standard python interpreter.
@@ -35,15 +36,24 @@
 
 import atexit
 import sys
+import time
 
 import gobject
 from gtk import gdk
 import gtk
 
 from kiwi.log import Logger
-from kiwi.ui.test.common import Base
+from kiwi.ui.test.common import WidgetIntrospecter
 from kiwi.ui.objectlist import ObjectList
 
+try:
+    from gobject import add_emission_hook
+except ImportError:
+    try:
+        from kiwi._kiwi import add_emission_hook
+    except ImportError:
+        add_emission_hook = None
+
 _events = []
 
 log = Logger('recorder')
@@ -152,9 +162,6 @@
     window manager.
     """
 
-    def serialize(self):
-        return 'delete_window("%s")' % self.name
-
 #
 # Signal Events
 #
@@ -340,7 +347,7 @@
 
 # register_event_type(KiwiComboBoxChangedEvent)
 
-class Recorder(Base):
+class Recorder(WidgetIntrospecter):
     """
     Recorder takes care of attaching events to widgets, when the appear,
     and creates the events when the user is interacting with some widgets.
@@ -349,18 +356,19 @@
     L{kiwi.ui.test.player.Player}.
     """
 
-    def __init__(self, filename, args):
+    def __init__(self, filename):
         """
         @param filename: name of the script
         @param args: command line used to run the script
         """
-        Base.__init__(self)
+        WidgetIntrospecter.__init__(self)
+        self.register_event_handler()
+        self.connect('window-removed', self.window_removed)
+
         self._filename = filename
-        self._args = args
         self._events = []
         self._listened_objects = []
         self._event_types = self._configure_event_types()
-        self._has_emission_hook = False
 
         # This is sort of a hack, but there are no other realiable ways
         # of actually having something executed after the application
@@ -370,10 +378,12 @@
         # Register a hook that is called before normal delete-events
         # because if it's connected using a normal callback it will not
         # be called if the application returns True in it's signal handler.
-        if hasattr(gobject, 'add_emission_hook'):
-            gobject.add_emission_hook(gtk.Window, 'delete-event',
-                                      self._emission_window__delete_event)
-            self._has_emission_hook = True
+        if add_emission_hook:
+            add_emission_hook(gtk.Window, 'delete-event',
+                              self._emission_window__delete_event)
+
+    def execute(self, args):
+        self._start_timestamp = time.time()
 
         # Run the script
         sys.argv = args
@@ -397,7 +407,7 @@
 
     def _add_event(self, event):
         log("Added event %s" % event.serialize())
-        self._events.append(event)
+        self._events.append((event, time.time()))
 
     def _listen_event(self, object, event_type):
         if not issubclass(event_type, SignalEvent):
@@ -419,15 +429,15 @@
                 pass
         event_type.connect(object, event_type.signal_name, on_signal)
 
-    def window_removed(self, window):
+    def window_removed(self, wi, window, name):
         # It'll already be trapped if we can use an emission hook
         # skip it here to avoid duplicates
-        if self._has_emission_hook:
+        if not add_emission_hook:
             return
         self._add_event(WindowDeleteEvent(window))
 
     def parse_one(self, toplevel, gobj):
-        Base.parse_one(self, toplevel, gobj)
+        WidgetIntrospecter.parse_one(self, toplevel, gobj)
 
         # mark the object as "listened" to ensure we'll always
         # receive unique objects
@@ -461,33 +471,42 @@
         finished executing.
         """
 
+        if not self._events:
+            return
+
         try:
             fd = open(self._filename, 'w')
         except IOError:
             raise SystemExit("Could not write: %s" % self._filename)
-        fd.write("from kiwi.ui.test.player import Player\n"
-                 "\n"
-                 "player = Player(%s)\n"
-                 "app = player.get_app()\n" % repr(self._args))
+        fd.write(">>> from kiwi.ui.test.runner import runner\n")
+        fd.write(">>> runner.start()\n")
 
         windows = {}
 
-        for event in self._events:
+        last = self._events[0][1]
+        fd.write('>>> runner.sleep(%2.1f)\n' % (last - self._start_timestamp,))
+
+        for event, timestamp in self._events:
             toplevel = event.toplevel_name
             if not toplevel in windows:
-                fd.write('\n'
-                         'player.wait_for_window("%s")\n' % toplevel)
+                fd.write('>>> %s = runner.waitopen("%s")\n' % (toplevel,
+                                                               toplevel))
                 windows[toplevel] = True
 
             if isinstance(event, WindowDeleteEvent):
-                fd.write("player.%s\n\n" % (event.serialize()))
+                fd.write(">>> %s.delete()\n" % (event.name,))
+                fd.write(">>> runner.waitclose('%s')\n" % (event.name,))
                 if not event.name in windows:
                     # Actually a bug
                     continue
                 del windows[event.name]
             else:
-                fd.write("app.%s.%s\n" % (toplevel,
-                                          event.serialize()))
+                fd.write(">>> %s.%s\n" % (toplevel, event.serialize()))
+
+            delta = timestamp - last
+            if delta > 0.05:
+                fd.write('>>> runner.sleep(%2.1f)\n' % (delta,))
+            last = timestamp
 
-        fd.write('player.finish()\n')
+        fd.write('>>> runner.quit()\n')
         fd.close()

Added: kiwi/trunk/kiwi/ui/test/runner.py
==============================================================================
--- (empty file)
+++ kiwi/trunk/kiwi/ui/test/runner.py	Tue Nov 21 14:21:07 2006
@@ -0,0 +1,207 @@
+#
+# Kiwi: a Framework and Enhanced Widgets for Python
+#
+# Copyright (C) 2006 Async Open Source
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
+# USA
+#
+# Author(s): Johan Dahlin <jdahlin at async.com.br>
+#
+
+"""
+Runner - executes recorded scripts
+"""
+
+import doctest
+import sys
+import time
+
+import gobject
+from gtk import gdk
+
+from kiwi.log import Logger
+from kiwi.ui.test.common import WidgetIntrospecter
+
+log = Logger('kiwi.ui.test.player')
+
+class NotReadyYet(Exception):
+    pass
+
+class MissingWidget(KeyError):
+    pass
+
+class MagicWindowWrapper(object):
+    def __init__(self, window, ns):
+        self.window = window
+        self.ns = ns
+
+    def delete(self):
+        self.window.emit('delete-event', gdk.Event(gdk.DELETE))
+
+    def __getattr__(self, attr):
+        if not attr in self.ns:
+            raise MissingWidget(attr)
+        return self.ns[attr]
+
+class Runner(object):
+    """
+    @ivar parser:
+    """
+    def __init__(self, filename):
+        self._filename = filename
+        self._pos = 0
+        self._windows = {}
+        self._ns = {}
+        self._source_id = -1
+
+        self.parser = doctest.DocTestParser()
+        self._stmts = self.parser.get_examples(open(filename).read())
+
+        wi = WidgetIntrospecter()
+        wi.register_event_handler()
+        wi.connect('window-added', self._on_wi__window_added)
+        wi.connect('window-removed', self._on_wi__window_removed)
+
+    # Callbacks
+
+    def _on_wi__window_added(self, wi, window, name, ns):
+        log.info('Window added: %s' % (name,))
+        self._windows[name] = MagicWindowWrapper(window, ns)
+
+        self._iterate()
+
+    def _on_wi__window_removed(self, wi, window, name):
+        log.info('Window removed: %s' % (name,))
+        del self._windows[name]
+
+        self._iterate()
+
+    # Private
+
+    def _iterate(self):
+        stmts = self._stmts
+        while True:
+            if self._pos == len(stmts):
+                self.quit()
+                break
+
+            ex =  stmts[self._pos]
+            self._pos += 1
+
+            log.info('will now execute %r' % (ex.source[:-1],))
+            try:
+                exec compile(ex.source, self._filename,
+                             'single', 0, 1) in self._ns
+            except NotReadyYet:
+                self._pos -= 1
+                break
+            except (SystemExit, KeyboardInterrupt):
+                raise SystemExit
+            except MissingWidget, e:
+                raise SystemExit(
+                    "ERROR: Could not find widget: %s" % str(e))
+            except Exception, e:
+                import traceback
+                traceback.print_exc()
+                raise SystemExit
+
+            log.info('Executed %r' % (ex.source[:-1],))
+            self._last = time.time()
+
+    # Public API
+
+    def quit(self):
+        print '* Executed successfully'
+        sys.exit(0)
+
+    def start(self):
+        self._last = time.time()
+
+    def sleep(self, duration):
+        """
+        @param duration:
+        """
+        # We don't want to block the interface here which means that
+        # we cannot use time.sleep.
+        # Instead we schedule another execute iteration in the future
+        # and raises NotReadyYet which stops the interpreter until
+        # iterate is called again.
+
+        def _iter():
+            # Turn ourselves off and allow future calls to wait() to
+            # queue new waits.
+            self._source_id = -1
+
+            # Iterate, which will call us again
+            self._iterate()
+
+            return False
+
+        if self._source_id != -1:
+            raise NotReadyYet
+
+        # The delta is the last time we executed a statement minus
+        delta = (self._last + duration) - time.time()
+        if delta > 0:
+            ms = int(delta * 1000)
+            self._source_id = gobject.timeout_add(ms, _iter)
+            raise NotReadyYet
+
+        # Okay, we've waited enough, let's go back to business
+
+    def waitopen(self, window_name):
+        """
+        @param window_name:
+        """
+        if not window_name in self._windows:
+            raise NotReadyYet(window_name)
+        return self._windows[window_name]
+
+    def waitclose(self, window_name):
+        """
+        @param window_name:
+        """
+        if window_name in self._windows:
+            raise NotReadyYet(window_name)
+
+runner = None
+
+def play_file(script, filename=None, args=None):
+    """
+    @param script:
+    @param filename:
+    @param args:
+    """
+
+    global runner
+
+    log.info('Running script %s' % script)
+    runner = Runner(script)
+
+    if filename is None:
+        fd = open(script)
+        data = fd.readline()[:-1] + fd.readline()[:-1]
+        pos = data.find('run:')
+        if pos != -1:
+            filename, rest = data[pos+5:].split(' ', 1)
+            if rest:
+                args = rest.split(' ')
+    else:
+        if args is None:
+            args = []
+
+    sys.argv = [filename] + args[:]
+    execfile(sys.argv[0], globals(), globals())

Modified: kiwi/trunk/setup.py
==============================================================================
--- kiwi/trunk/setup.py	(original)
+++ kiwi/trunk/setup.py	Tue Nov 21 14:21:07 2006
@@ -12,9 +12,30 @@
 simpler.
 """
 
+import commands
+from distutils.extension import Extension
+import sys
+
 from kiwi import kiwi_version
 from kiwi.dist import setup, listfiles, listpackages, get_site_packages_dir
 
+import  gtk
+
+ext_modules = []
+
+# Build a helper module for testing on gtk+ versions lower than 2.10.
+# Don't build it on windows due to easy availability compilers and
+# the lack of pkg-config.
+if gtk.pygtk_version < (2, 10) and sys.platform != 'win32':
+    pkgs = 'gdk-2.0 gtk+-2.0 pygtk-2.0'
+    cflags = commands.getoutput('pkg-config --cflags %s' % pkgs)
+    libs = commands.getoutput('pkg-config --libs %s' % pkgs)
+    include_dirs = [part.strip() for part in cflags.split('-I') if part]
+    libraries = [part.strip() for part in libs.split('-l') if part]
+    ext_modules.append(Extension('kiwi/_kiwi', ['kiwi/_kiwi.c'],
+                                 include_dirs=include_dirs,
+                                 libraries=libraries))
+
 setup(name="kiwi",
       version=".".join(map(str, kiwi_version)),
       description="A framework and a set of enhanced widgets based on PyGTK",
@@ -44,6 +65,7 @@
       scripts=['bin/kiwi-i18n',
                'bin/kiwi-ui-test'],
       packages=listpackages('kiwi'),
+      ext_modules=ext_modules,
       resources=dict(locale='$prefix/share/locale'),
       global_resources=dict(glade='$datadir/glade',
                             pixmap='$datadir/pixmaps'),

Modified: kiwi/trunk/tests/ui/diary.py
==============================================================================
--- kiwi/trunk/tests/ui/diary.py	(original)
+++ kiwi/trunk/tests/ui/diary.py	Tue Nov 21 14:21:07 2006
@@ -1,42 +1,29 @@
-from kiwi.ui.test.player import Player
+... -*- Mode: doctest -*-
+... run: examples/framework/diary/diary.py
 
-player = Player(['examples/framework/diary/diary.py'])
-app = player.get_app()
-
-player.wait_for_window("Diary")
-app.Diary.add.clicked()
-app.Diary.title.set_text("New title")
-app.Diary.title.set_text("")
-app.Diary.title.set_text("F")
-app.Diary.title.set_text("Fo")
-app.Diary.title.set_text("Foo")
-app.Diary.title.set_text("Foob")
-app.Diary.title.set_text("Fooba")
-app.Diary.title.set_text("Foobar")
-app.Diary.add.clicked()
-app.Diary.title.set_text("")
-app.Diary.title.set_text("New title")
-app.Diary.ObjectList.select_paths(['1'])
-app.Diary.title.set_text("")
-app.Diary.title.set_text("T")
-app.Diary.title.set_text("Te")
-app.Diary.title.set_text("Tes")
-app.Diary.title.set_text("Test")
-app.Diary.title.set_text("Testi")
-app.Diary.title.set_text("Testin")
-app.Diary.title.set_text("Testing")
-app.Diary.title.set_text("")
-app.Diary.title.set_text("Foobar")
-app.Diary.ObjectList.select_paths(['0'])
-app.Diary.remove.clicked()
-app.Diary.ObjectList.select_paths([])
-app.Diary.title.set_text("")
-app.Diary.title.set_text("Testing")
-app.Diary.ObjectList.select_paths(['0'])
-app.Diary.period.clicked()
-app.Diary.evening.clicked()
-app.Diary.remove.clicked()
-app.Diary.ObjectList.select_paths([])
-player.delete_window("Diary")
-
-player.finish()
+>>> from kiwi.ui.test.runner import runner
+>>> runner.start()
+>>> Diary = runner.waitopen("Diary")
+>>> Diary.add.clicked()
+>>> Diary.title.set_text("New title")
+>>> Diary.title.set_text("Foobar")
+>>> Diary.add.clicked()
+>>> Diary.title.set_text("")
+>>> Diary.title.set_text("New title")
+>>> Diary.ObjectList.select_paths(['1'])
+>>> Diary.title.set_text("Testing")
+>>> Diary.title.set_text("")
+>>> Diary.title.set_text("Foobar")
+>>> Diary.ObjectList.select_paths(['0'])
+>>> Diary.remove.clicked()
+>>> Diary.ObjectList.select_paths([])
+>>> Diary.title.set_text("")
+>>> Diary.title.set_text("Testing")
+>>> Diary.ObjectList.select_paths(['0'])
+>>> Diary.period.clicked()
+>>> Diary.evening.clicked()
+>>> Diary.remove.clicked()
+>>> Diary.ObjectList.select_paths([])
+>>> Diary.delete()
+>>> runner.waitclose("Diary")
+>>> runner.quit()

Modified: kiwi/trunk/tests/ui/diary2.py
==============================================================================
--- kiwi/trunk/tests/ui/diary2.py	(original)
+++ kiwi/trunk/tests/ui/diary2.py	Tue Nov 21 14:21:07 2006
@@ -1,83 +1,67 @@
-from kiwi.ui.test.player import Player
+... -*- Mode: doctest -*-
+... run: examples/framework/diary/diary2.py
 
-player = Player(['examples/framework/diary/diary2.py'])
-app = player.get_app()
+>>> from kiwi.ui.test.runner import runner
+>>> runner.start()
+>>> Diary = runner.waitopen("Diary")
+>>> Diary.add.clicked()
+>>> Diary.title.set_text("Untitled")
+>>> Diary.ObjectList.select_paths(['0'])
+>>> Diary.title.set_text("First")
+>>> Diary.add.clicked()
+>>> Diary.title.set_text("")
+>>> Diary.title.set_text("Untitled")
+>>> Diary.ObjectList.select_paths(['1'])
+>>> Diary.title.set_text("Second")
+>>> Diary.period.clicked()
+>>> Diary.afternoon.clicked()
+>>> Diary.add.clicked()
+>>> Diary.afternoon.clicked()
+>>> Diary.period.clicked()
+>>> Diary.title.set_text("")
+>>> Diary.title.set_text("Untitled")
+>>> Diary.ObjectList.select_paths(['2'])
+>>> Diary.title.set_text("Third")
+>>> Diary.period.clicked()
+>>> Diary.evening.clicked()
+>>> Diary.evening.clicked()
+>>> Diary.afternoon.clicked()
+>>> Diary.title.set_text("")
+>>> Diary.title.set_text("Second")
+>>> Diary.ObjectList.select_paths(['1'])
+>>> Diary.afternoon.clicked()
+>>> Diary.period.clicked()
+>>> Diary.title.set_text("")
+>>> Diary.title.set_text("First")
+>>> Diary.ObjectList.select_paths(['0'])
+>>> Diary.period.clicked()
+>>> Diary.afternoon.clicked()
+>>> Diary.title.set_text("")
+>>> Diary.title.set_text("Second")
+>>> Diary.ObjectList.select_paths(['1'])
+>>> Diary.afternoon.clicked()
+>>> Diary.evening.clicked()
+>>> Diary.title.set_text("")
+>>> Diary.title.set_text("Third")
+>>> Diary.ObjectList.select_paths(['2'])
+>>> Diary.remove.clicked()
+>>> Diary.ObjectList.select_paths([])
+>>> Diary.evening.clicked()
+>>> Diary.afternoon.clicked()
+>>> Diary.title.set_text("")
+>>> Diary.title.set_text("Second")
+>>> Diary.ObjectList.select_paths(['1'])
+>>> Diary.remove.clicked()
+>>> Diary.ObjectList.select_paths([])
+>>> Diary.afternoon.clicked()
+>>> Diary.period.clicked()
+>>> Diary.title.set_text("")
+>>> Diary.title.set_text("First")
+>>> Diary.ObjectList.select_paths(['0'])
+>>> Diary.remove.clicked()
+>>> Diary.ObjectList.select_paths([])
+>>> Diary.title.set_text("")
+>>> Diary.title.set_text("")
+>>> Diary.delete()
+>>> runner.quit()
 
-player.wait_for_window("Diary")
-app.Diary.add.clicked()
-app.Diary.title.set_text("Untitled")
-app.Diary.ObjectList.select_paths(['0'])
-app.Diary.title.set_text("")
-app.Diary.title.set_text("F")
-app.Diary.title.set_text("Fi")
-app.Diary.title.set_text("Fir")
-app.Diary.title.set_text("Firs")
-app.Diary.title.set_text("First")
-app.Diary.add.clicked()
-app.Diary.title.set_text("")
-app.Diary.title.set_text("Untitled")
-app.Diary.ObjectList.select_paths(['1'])
-app.Diary.title.set_text("")
-app.Diary.title.set_text("S")
-app.Diary.title.set_text("Se")
-app.Diary.title.set_text("Sec")
-app.Diary.title.set_text("Seco")
-app.Diary.title.set_text("Secon")
-app.Diary.title.set_text("Second")
-app.Diary.period.clicked()
-app.Diary.afternoon.clicked()
-app.Diary.add.clicked()
-app.Diary.afternoon.clicked()
-app.Diary.period.clicked()
-app.Diary.title.set_text("")
-app.Diary.title.set_text("Untitled")
-app.Diary.ObjectList.select_paths(['2'])
-app.Diary.title.set_text("")
-app.Diary.title.set_text("T")
-app.Diary.title.set_text("Th")
-app.Diary.title.set_text("Thi")
-app.Diary.title.set_text("Thir")
-app.Diary.title.set_text("Third")
-app.Diary.period.clicked()
-app.Diary.evening.clicked()
-app.Diary.evening.clicked()
-app.Diary.afternoon.clicked()
-app.Diary.title.set_text("")
-app.Diary.title.set_text("Second")
-app.Diary.ObjectList.select_paths(['1'])
-app.Diary.afternoon.clicked()
-app.Diary.period.clicked()
-app.Diary.title.set_text("")
-app.Diary.title.set_text("First")
-app.Diary.ObjectList.select_paths(['0'])
-app.Diary.period.clicked()
-app.Diary.afternoon.clicked()
-app.Diary.title.set_text("")
-app.Diary.title.set_text("Second")
-app.Diary.ObjectList.select_paths(['1'])
-app.Diary.afternoon.clicked()
-app.Diary.evening.clicked()
-app.Diary.title.set_text("")
-app.Diary.title.set_text("Third")
-app.Diary.ObjectList.select_paths(['2'])
-app.Diary.remove.clicked()
-app.Diary.ObjectList.select_paths([])
-app.Diary.evening.clicked()
-app.Diary.afternoon.clicked()
-app.Diary.title.set_text("")
-app.Diary.title.set_text("Second")
-app.Diary.ObjectList.select_paths(['1'])
-app.Diary.remove.clicked()
-app.Diary.ObjectList.select_paths([])
-app.Diary.afternoon.clicked()
-app.Diary.period.clicked()
-app.Diary.title.set_text("")
-app.Diary.title.set_text("First")
-app.Diary.ObjectList.select_paths(['0'])
-app.Diary.remove.clicked()
-app.Diary.ObjectList.select_paths([])
-app.Diary.title.set_text("")
-app.Diary.title.set_text("")
-player.delete_window("Diary")
-
-player.finish()

Modified: kiwi/trunk/tests/ui/personalinformation.py
==============================================================================
--- kiwi/trunk/tests/ui/personalinformation.py	(original)
+++ kiwi/trunk/tests/ui/personalinformation.py	Tue Nov 21 14:21:07 2006
@@ -1,115 +1,114 @@
-from kiwi.ui.test.player import Player
+... -*- Mode: doctest -*-
+... run: examples/validation/personalinformation.py
 
-player = Player(['examples/validation/personalinformation.py'])
-app = player.get_app()
-
-player.wait_for_window("Form")
-app.Form.name.set_text("")
-app.Form.name.set_text("J")
-app.Form.name.set_text("Jo")
-app.Form.name.set_text("Joh")
-app.Form.name.set_text("Joha")
-app.Form.name.set_text("Johan")
-app.Form.age.set_text("")
-app.Form.age.set_text("  ")
-app.Form.age.set_text(" ")
-app.Form.age.set_text("1 ")
-app.Form.age.set_text("1")
-app.Form.age.set_text("12")
-app.Form.age.set_text("1")
-app.Form.age.set_text("1 ")
-app.Form.age.set_text(" ")
-app.Form.age.set_text("  ")
-app.Form.age.set_text(" ")
-app.Form.age.set_text("9 ")
-app.Form.age.set_text("9")
-app.Form.age.set_text("99")
-app.Form.GtkToggleButton.clicked()
-app.Form.ProxyEntry.set_text("")
-app.Form.ProxyEntry.set_text("  /  /    ")
-app.Form.ProxyEntry.set_text(" /  /    ")
-app.Form.ProxyEntry.set_text("/  /    ")
-app.Form.ProxyEntry.set_text("  /    ")
-app.Form.ProxyEntry.set_text(" /    ")
-app.Form.ProxyEntry.set_text("/    ")
-app.Form.ProxyEntry.set_text("    ")
-app.Form.ProxyEntry.set_text("   ")
-app.Form.ProxyEntry.set_text("  ")
-app.Form.ProxyEntry.set_text(" ")
-app.Form.ProxyEntry.set_text("")
-app.Form.ProxyEntry.set_text("02/14/1969")
-app.Form.GtkToggleButton.clicked()
-app.Form.ProxyEntry.set_text("")
-app.Form.ProxyEntry.set_text("  /  /    ")
-app.Form.ProxyEntry.set_text(" /  /    ")
-app.Form.ProxyEntry.set_text("/  /    ")
-app.Form.ProxyEntry.set_text("  /    ")
-app.Form.ProxyEntry.set_text(" /    ")
-app.Form.ProxyEntry.set_text("/    ")
-app.Form.ProxyEntry.set_text("    ")
-app.Form.ProxyEntry.set_text("   ")
-app.Form.ProxyEntry.set_text("  ")
-app.Form.ProxyEntry.set_text(" ")
-app.Form.ProxyEntry.set_text("")
-app.Form.ProxyEntry.set_text("02/13/1969")
-app.Form.height.set_text("")
-app.Form.height.set_text("1")
-app.Form.height.set_text("12")
-app.Form.height.set_text("123")
-app.Form.height.set_text("1234")
-app.Form.height.set_text("12345")
-app.Form.weight.set_text("")
-app.Form.weight.set_text("87")
-app.Form.weight.set_text("")
-app.Form.weight.set_text("88")
-app.Form.weight.set_text("")
-app.Form.weight.set_text("89")
-app.Form.weight.set_text("")
-app.Form.weight.set_text("90")
-app.Form.weight.set_text("")
-app.Form.weight.set_text("91")
-app.Form.weight.set_text("")
-app.Form.weight.set_text("92")
-app.Form.weight.set_text("")
-app.Form.weight.set_text("93")
-app.Form.weight.set_text("")
-app.Form.weight.set_text("92")
-app.Form.weight.set_text("")
-app.Form.weight.set_text("91")
-app.Form.weight.set_text("")
-app.Form.weight.set_text("90")
-app.Form.weight.set_text("")
-app.Form.weight.set_text("89")
-app.Form.weight.set_text("")
-app.Form.weight.set_text("90")
-app.Form.height.set_text("")
-app.Form.height.set_text("1")
-app.Form.height.set_text("12")
-app.Form.age.set_text("")
-app.Form.age.set_text("  ")
-app.Form.age.set_text(" ")
-app.Form.age.set_text("1 ")
-app.Form.age.set_text("1")
-app.Form.age.set_text("12")
-app.Form.GtkToggleButton.clicked()
-app.Form.ProxyEntry.set_text("")
-app.Form.ProxyEntry.set_text("Brazilian")
-app.Form.GtkToggleButton.clicked()
-app.Form.ProxyEntry.set_text("")
-app.Form.ProxyEntry.set_text("Yankee")
-app.Form.ProxyEntry.set_text("")
-app.Form.ProxyEntry.set_text("Other")
-app.Form.ProxyEntry.set_text("")
-app.Form.ProxyEntry.set_text("Yankee")
-app.Form.ProxyEntry.set_text("")
-app.Form.ProxyEntry.set_text("Brazilian")
-app.Form.ProxyEntry.set_text("")
-app.Form.ProxyEntry.set_text("Yankee")
-app.Form.gender.select_item_by_label("Male")
-app.Form.gender.select_item_by_label("Female")
-app.Form.status_single.clicked()
-app.Form.status.clicked()
-app.Form.status.clicked()
-app.Form.status_single.clicked()
-app.Form.ok_btn.clicked()
-player.finish()
+>>> from kiwi.ui.test.runner import runner
+>>> Form = runner.waitopen("Form")
+>>> Form.name.set_text("")
+>>> Form.name.set_text("J")
+>>> Form.name.set_text("Jo")
+>>> Form.name.set_text("Joh")
+>>> Form.name.set_text("Joha")
+>>> Form.name.set_text("Johan")
+>>> Form.age.set_text("")
+>>> Form.age.set_text("  ")
+>>> Form.age.set_text(" ")
+>>> Form.age.set_text("1 ")
+>>> Form.age.set_text("1")
+>>> Form.age.set_text("12")
+>>> Form.age.set_text("1")
+>>> Form.age.set_text("1 ")
+>>> Form.age.set_text(" ")
+>>> Form.age.set_text("  ")
+>>> Form.age.set_text(" ")
+>>> Form.age.set_text("9 ")
+>>> Form.age.set_text("9")
+>>> Form.age.set_text("99")
+>>> Form.GtkToggleButton.clicked()
+>>> Form.ProxyEntry.set_text("")
+>>> Form.ProxyEntry.set_text("  /  /    ")
+>>> Form.ProxyEntry.set_text(" /  /    ")
+>>> Form.ProxyEntry.set_text("/  /    ")
+>>> Form.ProxyEntry.set_text("  /    ")
+>>> Form.ProxyEntry.set_text(" /    ")
+>>> Form.ProxyEntry.set_text("/    ")
+>>> Form.ProxyEntry.set_text("    ")
+>>> Form.ProxyEntry.set_text("   ")
+>>> Form.ProxyEntry.set_text("  ")
+>>> Form.ProxyEntry.set_text(" ")
+>>> Form.ProxyEntry.set_text("")
+>>> Form.ProxyEntry.set_text("02/14/1969")
+>>> Form.GtkToggleButton.clicked()
+>>> Form.ProxyEntry.set_text("")
+>>> Form.ProxyEntry.set_text("  /  /    ")
+>>> Form.ProxyEntry.set_text(" /  /    ")
+>>> Form.ProxyEntry.set_text("/  /    ")
+>>> Form.ProxyEntry.set_text("  /    ")
+>>> Form.ProxyEntry.set_text(" /    ")
+>>> Form.ProxyEntry.set_text("/    ")
+>>> Form.ProxyEntry.set_text("    ")
+>>> Form.ProxyEntry.set_text("   ")
+>>> Form.ProxyEntry.set_text("  ")
+>>> Form.ProxyEntry.set_text(" ")
+>>> Form.ProxyEntry.set_text("")
+>>> Form.ProxyEntry.set_text("02/13/1969")
+>>> Form.height.set_text("")
+>>> Form.height.set_text("1")
+>>> Form.height.set_text("12")
+>>> Form.height.set_text("123")
+>>> Form.height.set_text("1234")
+>>> Form.height.set_text("12345")
+>>> Form.weight.set_text("")
+>>> Form.weight.set_text("87")
+>>> Form.weight.set_text("")
+>>> Form.weight.set_text("88")
+>>> Form.weight.set_text("")
+>>> Form.weight.set_text("89")
+>>> Form.weight.set_text("")
+>>> Form.weight.set_text("90")
+>>> Form.weight.set_text("")
+>>> Form.weight.set_text("91")
+>>> Form.weight.set_text("")
+>>> Form.weight.set_text("92")
+>>> Form.weight.set_text("")
+>>> Form.weight.set_text("93")
+>>> Form.weight.set_text("")
+>>> Form.weight.set_text("92")
+>>> Form.weight.set_text("")
+>>> Form.weight.set_text("91")
+>>> Form.weight.set_text("")
+>>> Form.weight.set_text("90")
+>>> Form.weight.set_text("")
+>>> Form.weight.set_text("89")
+>>> Form.weight.set_text("")
+>>> Form.weight.set_text("90")
+>>> Form.height.set_text("")
+>>> Form.height.set_text("1")
+>>> Form.height.set_text("12")
+>>> Form.age.set_text("")
+>>> Form.age.set_text("  ")
+>>> Form.age.set_text(" ")
+>>> Form.age.set_text("1 ")
+>>> Form.age.set_text("1")
+>>> Form.age.set_text("12")
+>>> Form.GtkToggleButton.clicked()
+>>> Form.ProxyEntry.set_text("")
+>>> Form.ProxyEntry.set_text("Brazilian")
+>>> Form.GtkToggleButton.clicked()
+>>> Form.ProxyEntry.set_text("")
+>>> Form.ProxyEntry.set_text("Yankee")
+>>> Form.ProxyEntry.set_text("")
+>>> Form.ProxyEntry.set_text("Other")
+>>> Form.ProxyEntry.set_text("")
+>>> Form.ProxyEntry.set_text("Yankee")
+>>> Form.ProxyEntry.set_text("")
+>>> Form.ProxyEntry.set_text("Brazilian")
+>>> Form.ProxyEntry.set_text("")
+>>> Form.ProxyEntry.set_text("Yankee")
+>>> Form.gender.select_item_by_label("Male")
+>>> Form.gender.select_item_by_label("Female")
+>>> Form.status_single.clicked()
+>>> Form.status.clicked()
+>>> Form.status.clicked()
+>>> Form.status_single.clicked()
+>>> Form.ok_btn.clicked()
+>>> runner.quit()


More information about the POS-commit mailing list