Merge lp:~autopilot/autopilot/experimental into lp:autopilot

Proposed by Thomi Richards
Status: Merged
Approved by: Thomi Richards
Approved revision: 348
Merged at revision: 325
Proposed branch: lp:~autopilot/autopilot/experimental
Merge into: lp:autopilot
Diff against target: 3821 lines (+2175/-293)
61 files modified
autopilot/__init__.py (+4/-3)
autopilot/globals.py (+9/-2)
autopilot/ibus.py (+1/-1)
autopilot/input/_X11.py (+7/-2)
autopilot/input/_osk.py (+5/-0)
autopilot/input/_uinput.py (+5/-0)
autopilot/introspection/__init__.py (+14/-3)
autopilot/introspection/backends.py (+2/-2)
autopilot/introspection/constants.py (+1/-1)
autopilot/introspection/dbus.py (+95/-115)
autopilot/introspection/types.py (+619/-0)
autopilot/introspection/utilities.py (+6/-0)
autopilot/keybindings.py (+6/-2)
autopilot/matchers/__init__.py (+1/-1)
autopilot/process/_bamf.py (+10/-8)
autopilot/testcase.py (+5/-4)
autopilot/tests/functional/test_ap_apps.py (+31/-22)
autopilot/tests/functional/test_autopilot_functional.py (+6/-4)
autopilot/tests/functional/test_dbus_query.py (+15/-2)
autopilot/tests/functional/test_input_stack.py (+7/-2)
autopilot/tests/functional/test_introspection_features.py (+2/-1)
autopilot/tests/functional/test_open_window.py (+1/-1)
autopilot/tests/functional/test_process_emulator.py (+1/-1)
autopilot/tests/unit/test_command_line_args.py (+9/-2)
autopilot/tests/unit/test_custom_exceptions.py (+1/-1)
autopilot/tests/unit/test_introspection_features.py (+36/-5)
autopilot/tests/unit/test_matchers.py (+41/-30)
autopilot/tests/unit/test_pick_backend.py (+1/-1)
autopilot/tests/unit/test_platform.py (+7/-1)
autopilot/tests/unit/test_types.py (+579/-0)
autopilot/tests/unit/test_version_utility_fns.py (+2/-2)
autopilot/utilities.py (+6/-7)
autopilot/vis/bus_enumerator.py (+5/-5)
autopilot/vis/main_window.py (+20/-12)
autopilot/vis/objectproperties.py (+8/-2)
autopilot/vis/resources.py (+1/-1)
bin/autopilot (+21/-20)
debian/changelog (+7/-0)
debian/control (+90/-12)
debian/python-autopilot-tests.install (+3/-0)
debian/python-autopilot-tests.pyinstall (+0/-3)
debian/python-autopilot-vis.install (+1/-0)
debian/python-autopilot-vis.pyinstall (+0/-1)
debian/python-autopilot.install (+6/-0)
debian/python-autopilot.pyinstall (+0/-6)
debian/python3-autopilot-tests.install (+3/-0)
debian/python3-autopilot-trace.install (+1/-0)
debian/python3-autopilot-vis.install (+1/-0)
debian/python3-autopilot.docs (+2/-0)
debian/python3-autopilot.install (+9/-0)
debian/python3-autopilot.postinst (+24/-0)
debian/rules (+30/-2)
docs/_templates/indexcontent.html (+6/-0)
docs/api/introspection.rst (+3/-0)
docs/appendix/appendix.rst (+9/-0)
docs/appendix/protocol.rst (+366/-0)
docs/conf.py (+3/-1)
docs/contents.rst (+1/-0)
docs/porting/porting.rst (+18/-0)
docs/tutorial/advanced_autopilot.rst (+1/-1)
setup.py (+1/-1)
To merge this branch: bzr merge lp:~autopilot/autopilot/experimental
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Approve
Martin Pitt (community) Approve
Review via email: mp+185672@code.launchpad.net

Commit message

New Hotness for autopilot 1.4.

Description of the change

This branch contains the 'new hotness' for autopilot 1.4. Specifically:

 - better introspection type support.
 - python 3 support!

To post a comment you must log in.
lp:~autopilot/autopilot/experimental updated
347. By Thomi Richards

Merged trunk, resolved conflicts.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Martin Pitt (pitti) wrote :

Big Plus for documenting the wire protocol and porting instructions!

review: Approve
lp:~autopilot/autopilot/experimental updated
348. By Thomi Richards

PEP8 fixes.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Martin Pitt (pitti) wrote :

Strictly speaking this would need to grow "Breaks: libautopilot-gtk (<< 1.4), libautopilot-qt (<< 1.4)" to both p-ap and p3-ap, as these are only recommends and not depends:.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'autopilot/__init__.py'
--- autopilot/__init__.py 2013-07-22 04:42:45 +0000
+++ autopilot/__init__.py 2013-09-16 17:17:50 +0000
@@ -20,7 +20,7 @@
20from argparse import ArgumentParser, REMAINDER, Action20from argparse import ArgumentParser, REMAINDER, Action
21import subprocess21import subprocess
2222
23version = '1.3.1'23version = '1.4.0'
2424
2525
26class BackendException(RuntimeError):26class BackendException(RuntimeError):
@@ -30,7 +30,7 @@
30 def __init__(self, original_exception):30 def __init__(self, original_exception):
31 super(BackendException, self).__init__(31 super(BackendException, self).__init__(
32 "Error while initialising backend. Original exception was: " +32 "Error while initialising backend. Original exception was: " +
33 original_exception.message)33 str(original_exception))
34 self.original_exception = original_exception34 self.original_exception = original_exception
3535
3636
@@ -179,7 +179,8 @@
179 "${Version}",179 "${Version}",
180 "--show",180 "--show",
181 "python-autopilot",181 "python-autopilot",
182 ]182 ],
183 universal_newlines=True
183 ).strip()184 ).strip()
184 except subprocess.CalledProcessError:185 except subprocess.CalledProcessError:
185 return None186 return None
186187
=== modified file 'autopilot/globals.py'
--- autopilot/globals.py 2013-07-22 04:14:41 +0000
+++ autopilot/globals.py 2013-09-16 17:17:50 +0000
@@ -19,7 +19,14 @@
1919
2020
21from __future__ import absolute_import21from __future__ import absolute_import
22from StringIO import StringIO22
23try:
24 # Python 2
25 from StringIO import StringIO
26except ImportError:
27 # Python 3
28 from io import StringIO
29
23from autopilot.utilities import LogFormatter, CleanupRegistered30from autopilot.utilities import LogFormatter, CleanupRegistered
24from testtools.content import text_content31from testtools.content import text_content
25import subprocess32import subprocess
@@ -203,7 +210,7 @@
203 """210 """
204 if type(enable_recording) is not bool:211 if type(enable_recording) is not bool:
205 raise TypeError("enable_recording must be a boolean.")212 raise TypeError("enable_recording must be a boolean.")
206 if not isinstance(record_dir, basestring):213 if not isinstance(record_dir, str):
207 raise TypeError("record_dir must be a string.")214 raise TypeError("record_dir must be a string.")
208215
209 _video_logger.enable_recording(enable_recording)216 _video_logger.enable_recording(enable_recording)
210217
=== modified file 'autopilot/ibus.py'
--- autopilot/ibus.py 2013-07-30 00:16:11 +0000
+++ autopilot/ibus.py 2013-09-16 17:17:50 +0000
@@ -94,7 +94,7 @@
94 raise TypeError("engine_list must be a list of valid engine names.")94 raise TypeError("engine_list must be a list of valid engine names.")
95 available_engines = get_available_input_engines()95 available_engines = get_available_input_engines()
96 for engine in engine_list:96 for engine in engine_list:
97 if not isinstance(engine, basestring):97 if not isinstance(engine, str):
98 raise TypeError("Engines in engine_list must all be strings.")98 raise TypeError("Engines in engine_list must all be strings.")
99 if engine not in available_engines:99 if engine not in available_engines:
100 raise ValueError(100 raise ValueError(
101101
=== modified file 'autopilot/input/_X11.py'
--- autopilot/input/_X11.py 2013-09-04 03:44:07 +0000
+++ autopilot/input/_X11.py 2013-09-16 17:17:50 +0000
@@ -27,6 +27,7 @@
27from __future__ import absolute_import27from __future__ import absolute_import
2828
29import logging29import logging
30import sys
30from time import sleep31from time import sleep
3132
32from autopilot.display import is_point_on_any_screen, move_mouse_to_screen33from autopilot.display import is_point_on_any_screen, move_mouse_to_screen
@@ -45,6 +46,10 @@
45_DISPLAY = None46_DISPLAY = None
46logger = logging.getLogger(__name__)47logger = logging.getLogger(__name__)
4748
49# py2 compatible alias for py3
50if sys.version >= '3':
51 basestring = str
52
4853
49def get_display():54def get_display():
50 """Return the Xlib display object.55 """Return the Xlib display object.
@@ -333,8 +338,8 @@
333 sync,338 sync,
334 X.CurrentTime,339 X.CurrentTime,
335 X.NONE,340 X.NONE,
336 x=x,341 x=int(x),
337 y=y)342 y=int(y))
338 get_display().sync()343 get_display().sync()
339 sleep(time_between_events)344 sleep(time_between_events)
340345
341346
=== modified file 'autopilot/input/_osk.py'
--- autopilot/input/_osk.py 2013-08-23 00:23:40 +0000
+++ autopilot/input/_osk.py 2013-09-16 17:17:50 +0000
@@ -18,6 +18,7 @@
18#18#
1919
20import logging20import logging
21import sys
21from time import sleep22from time import sleep
22from contextlib import contextmanager23from contextlib import contextmanager
2324
@@ -31,6 +32,10 @@
3132
32logger = logging.getLogger(__name__)33logger = logging.getLogger(__name__)
3334
35# py2 compatible alias for py3
36if sys.version >= '3':
37 basestring = str
38
3439
35class Keyboard(KeyboardBase):40class Keyboard(KeyboardBase):
3641
3742
=== modified file 'autopilot/input/_uinput.py'
--- autopilot/input/_uinput.py 2013-07-24 08:26:10 +0000
+++ autopilot/input/_uinput.py 2013-09-16 17:17:50 +0000
@@ -29,6 +29,7 @@
29from time import sleep29from time import sleep
30from evdev import UInput, ecodes as e30from evdev import UInput, ecodes as e
31import os.path31import os.path
32import sys
3233
33logger = logging.getLogger(__name__)34logger = logging.getLogger(__name__)
3435
@@ -37,6 +38,10 @@
3738
38_PRESSED_KEYS = []39_PRESSED_KEYS = []
3940
41# py2 compatible alias for py3
42if sys.version >= '3':
43 basestring = str
44
4045
41def _get_devnode_path():46def _get_devnode_path():
42 """Provide a fallback uinput node for devices which don't support udev"""47 """Provide a fallback uinput node for devices which don't support udev"""
4348
=== modified file 'autopilot/introspection/__init__.py'
--- autopilot/introspection/__init__.py 2013-07-30 15:08:30 +0000
+++ autopilot/introspection/__init__.py 2013-09-16 17:17:50 +0000
@@ -33,6 +33,7 @@
33from time import sleep33from time import sleep
34from functools import partial34from functools import partial
35import os35import os
36import sys
3637
37from autopilot.introspection.backends import DBusAddress38from autopilot.introspection.backends import DBusAddress
38from autopilot.introspection.constants import (39from autopilot.introspection.constants import (
@@ -62,6 +63,10 @@
62# Keep track of known connections during search63# Keep track of known connections during search
63connection_list = []64connection_list = []
6465
66# py2 compatible alias for py3
67if sys.version >= '3':
68 basestring = str
69
6570
66class ProcessSearchError(RuntimeError):71class ProcessSearchError(RuntimeError):
67 pass72 pass
@@ -77,7 +82,10 @@
77 # any non-dynamically linked executables, which we may need to fix further82 # any non-dynamically linked executables, which we may need to fix further
78 # down the line.83 # down the line.
79 try:84 try:
80 ldd_output = subprocess.check_output(["ldd", app_path]).strip().lower()85 ldd_output = subprocess.check_output(
86 ["ldd", app_path],
87 universal_newlines=True
88 ).strip().lower()
81 except subprocess.CalledProcessError as e:89 except subprocess.CalledProcessError as e:
82 raise RuntimeError(e)90 raise RuntimeError(e)
83 if 'libqtcore' in ldd_output or 'libqt5core' in ldd_output:91 if 'libqtcore' in ldd_output or 'libqt5core' in ldd_output:
@@ -115,9 +123,11 @@
115 cwd = kwargs.pop('launch_dir', None)123 cwd = kwargs.pop('launch_dir', None)
116 capture_output = kwargs.pop('capture_output', True)124 capture_output = kwargs.pop('capture_output', True)
117 if kwargs:125 if kwargs:
126 arglist = [repr(k) for k in kwargs.keys()]
127 arglist.sort()
118 raise ValueError(128 raise ValueError(
119 "Unknown keyword arguments: %s." %129 "Unknown keyword arguments: %s." %
120 (', '.join(repr(k) for k in kwargs.keys())))130 (', '.join(arglist)))
121131
122 path, args = launcher.prepare_environment(application, list(arguments))132 path, args = launcher.prepare_environment(application, list(arguments))
123133
@@ -164,6 +174,7 @@
164 stderr=cap_mode,174 stderr=cap_mode,
165 close_fds=True,175 close_fds=True,
166 preexec_fn=os.setsid,176 preexec_fn=os.setsid,
177 universal_newlines=True,
167 **kwargs178 **kwargs
168 )179 )
169 return process180 return process
@@ -263,7 +274,7 @@
263 if application_name:274 if application_name:
264 app_name_check_fn = lambda i: get_classname_from_path(275 app_name_check_fn = lambda i: get_classname_from_path(
265 i.introspection_iface.GetState('/')[0][0]) == application_name276 i.introspection_iface.GetState('/')[0][0]) == application_name
266 dbus_addresses = filter(app_name_check_fn, dbus_addresses)277 dbus_addresses = list(filter(app_name_check_fn, dbus_addresses))
267278
268 if dbus_addresses is None or len(dbus_addresses) == 0:279 if dbus_addresses is None or len(dbus_addresses) == 0:
269 raise ProcessSearchError("Search criteria returned no results")280 raise ProcessSearchError("Search criteria returned no results")
270281
=== modified file 'autopilot/introspection/backends.py'
--- autopilot/introspection/backends.py 2013-07-30 17:31:50 +0000
+++ autopilot/introspection/backends.py 2013-09-16 17:17:50 +0000
@@ -91,9 +91,9 @@
9191
92 @property92 @property
93 def introspection_iface(self):93 def introspection_iface(self):
94 if not isinstance(self._addr_tuple.connection, basestring):94 if not isinstance(self._addr_tuple.connection, str):
95 raise TypeError("Service name must be a string.")95 raise TypeError("Service name must be a string.")
96 if not isinstance(self._addr_tuple.object_path, basestring):96 if not isinstance(self._addr_tuple.object_path, str):
97 raise TypeError("Object name must be a string")97 raise TypeError("Object name must be a string")
9898
99 if not self._check_pid_running():99 if not self._check_pid_running():
100100
=== modified file 'autopilot/introspection/constants.py'
--- autopilot/introspection/constants.py 2013-05-13 22:47:50 +0000
+++ autopilot/introspection/constants.py 2013-09-16 17:17:50 +0000
@@ -25,4 +25,4 @@
25AP_INTROSPECTION_IFACE = 'com.canonical.Autopilot.Introspection'25AP_INTROSPECTION_IFACE = 'com.canonical.Autopilot.Introspection'
26DBUS_INTROSPECTION_IFACE = 'org.freedesktop.DBus.Introspectable'26DBUS_INTROSPECTION_IFACE = 'org.freedesktop.DBus.Introspectable'
2727
28CURRENT_WIRE_PROTOCOL_VERSION = "1.3"28CURRENT_WIRE_PROTOCOL_VERSION = "1.4"
2929
=== modified file 'autopilot/introspection/dbus.py'
--- autopilot/introspection/dbus.py 2013-08-09 01:29:23 +0000
+++ autopilot/introspection/dbus.py 2013-09-16 17:17:50 +0000
@@ -30,10 +30,11 @@
30from contextlib import contextmanager30from contextlib import contextmanager
31import logging31import logging
32import re32import re
33from testtools.matchers import Equals33import sys
34from time import sleep
35from uuid import uuid434from uuid import uuid4
3635
36from autopilot.introspection.types import create_value_instance
37from autopilot.introspection.utilities import translate_state_keys
37from autopilot.utilities import Timer, get_debug_logger38from autopilot.utilities import Timer, get_debug_logger
3839
3940
@@ -41,8 +42,16 @@
41logger = logging.getLogger(__name__)42logger = logging.getLogger(__name__)
4243
4344
45# py2 compatible alias for py3
46if sys.version >= '3':
47 _PY3 = True
48 basestring = str
49else:
50 _PY3 = False
51
52
44class StateNotFoundError(RuntimeError):53class StateNotFoundError(RuntimeError):
45 """Raised when a piece of state information from unity is not found."""54 """Raised when a piece of state information is not found."""
4655
47 message = "State not found for class with name '{}' and id '{}'."56 message = "State not found for class with name '{}' and id '{}'."
4857
@@ -81,17 +90,11 @@
8190
82 """91 """
83 global _object_registry92 global _object_registry
84 for cls in _object_registry[proxy_object._id].itervalues():93 for cls in _object_registry[proxy_object._id].values():
85 if type(proxy_object) is not cls:94 if type(proxy_object) is not cls:
86 cls._Backend = None95 cls._Backend = None
8796
8897
89def translate_state_keys(state_dict):
90 """Translates the *state_dict* passed in so the keys are usable as python
91 attributes."""
92 return {k.replace('-', '_'): v for k, v in state_dict.iteritems()}
93
94
95def get_classname_from_path(object_path):98def get_classname_from_path(object_path):
96 return object_path.split("/")[-1]99 return object_path.split("/")[-1]
97100
@@ -100,7 +103,7 @@
100 """Return true if *instance* satisifies all the filters present in103 """Return true if *instance* satisifies all the filters present in
101 kwargs."""104 kwargs."""
102 with instance.no_automatic_refreshing():105 with instance.no_automatic_refreshing():
103 for attr, val in kwargs.iteritems():106 for attr, val in kwargs.items():
104 if not hasattr(instance, attr) or getattr(instance, attr) != val:107 if not hasattr(instance, attr) or getattr(instance, attr) != val:
105 # Either attribute is not present, or is present but with108 # Either attribute is not present, or is present but with
106 # the wrong value - don't add this instance to the results109 # the wrong value - don't add this instance to the results
@@ -108,8 +111,14 @@
108 return False111 return False
109 return True112 return True
110113
111114DBusIntrospectionObjectBase = IntrospectableObjectMetaclass(
112class DBusIntrospectionObject(object):115 'DBusIntrospectionObjectBase',
116 (object,),
117 {}
118)
119
120
121class DBusIntrospectionObject(DBusIntrospectionObjectBase):
113 """A class that supports transparent data retrieval from the application122 """A class that supports transparent data retrieval from the application
114 under test.123 under test.
115124
@@ -120,8 +129,6 @@
120129
121 """130 """
122131
123 __metaclass__ = IntrospectableObjectMetaclass
124
125 _Backend = None132 _Backend = None
126133
127 def __init__(self, state_dict, path):134 def __init__(self, state_dict, path):
@@ -139,91 +146,20 @@
139146
140 """147 """
141 self.__state = {}148 self.__state = {}
142 for key, value in translate_state_keys(state_dict).iteritems():149 for key, value in translate_state_keys(state_dict).items():
143 # don't store id in state dictionary -make it a proper instance150 # don't store id in state dictionary -make it a proper instance
144 # attribute151 # attribute
145 if key == 'id':152 if key == 'id':
146 self.id = value153 self.id = int(value[1])
147 self.__state[key] = self._make_attribute(key, value)154 try:
148155 self.__state[key] = create_value_instance(value, self, key)
149 def _make_attribute(self, name, value):156 except ValueError as e:
150 """Make an attribute for *value*, patched with the wait_for157 logger.warning(
151 function."""158 "While constructing attribute '%s.%s': %s",
152159 self.__class__.__name__,
153 def wait_for(self, expected_value, timeout=10):160 key,
154 """Wait up to 10 seconds for our value to change to161 str(e)
155 *expected_value*.162 )
156
157 *expected_value* can be a testtools.matcher. Matcher subclass (like
158 LessThan, for example), or an ordinary value.
159
160 This works by refreshing the value using repeated dbus calls.
161
162 :raises: **RuntimeError** if the attribute was not equal to the
163 expected value after 10 seconds.
164
165 """
166 # It's guaranteed that our value is up to date, since __getattr__
167 # calls refresh_state. This if statement stops us waiting if the
168 # value is already what we expect:
169 if self == expected_value:
170 return
171
172 def make_unicode(value):
173 if isinstance(value, str):
174 return unicode(value.decode('utf8'))
175 return value
176
177 if hasattr(expected_value, 'expected'):
178 expected_value.expected = make_unicode(expected_value.expected)
179
180 # unfortunately not all testtools matchers derive from the Matcher
181 # class, so we can't use issubclass, isinstance for this:
182 match_fun = getattr(expected_value, 'match', None)
183 is_matcher = match_fun and callable(match_fun)
184 if not is_matcher:
185 expected_value = Equals(expected_value)
186
187 time_left = timeout
188 while True:
189 _, new_state = self.parent.get_new_state()
190 new_state = translate_state_keys(new_state)
191 new_value = make_unicode(new_state[self.name])
192 # Support for testtools.matcher classes:
193 mismatch = expected_value.match(new_value)
194 if mismatch:
195 failure_msg = mismatch.describe()
196 else:
197 self.parent._set_properties(new_state)
198 return
199
200 if time_left >= 1:
201 sleep(1)
202 time_left -= 1
203 else:
204 sleep(time_left)
205 break
206
207 raise AssertionError(
208 "After %.1f seconds test on %s.%s failed: %s" % (
209 timeout, self.parent.__class__.__name__, self.name,
210 failure_msg))
211
212 # This looks like magic, but it's really not. We're creating a new type
213 # on the fly that derives from the type of 'value' with a couple of
214 # extra attributes: wait_for is the wait_for method above. 'parent' and
215 # 'name' are needed by the wait_for method.
216 #
217 # We can't use traditional meta-classes here, since the type we're
218 # deriving from is only known at call time, not at parse time (we could
219 # override __call__ in the meta class, but that doesn't buy us anything
220 # extra).
221 #
222 # A better way to do this would be with functools.partial, which I
223 # tried initially, but doesn't work well with bound methods.
224 t = type(value)
225 attrs = {'wait_for': wait_for, 'parent': self, 'name': name}
226 return type(t.__name__, (t,), attrs)(value)
227163
228 def get_children_by_type(self, desired_type, **kwargs):164 def get_children_by_type(self, desired_type, **kwargs):
229 """Get a list of children of the specified type.165 """Get a list of children of the specified type.
@@ -302,6 +238,19 @@
302 children = [self.make_introspection_object(i) for i in state_dicts]238 children = [self.make_introspection_object(i) for i in state_dicts]
303 return children239 return children
304240
241 def get_parent(self):
242 """Returns the parent of this object.
243
244 If this object has no parent (i.e.- it is the root of the introspection
245 tree). Then it returns itself.
246
247 """
248 query = self.get_class_query_string() + "/.."
249 parent_state_dicts = self.get_state_by_path(query)
250
251 parent = self.make_introspection_object(parent_state_dicts[0])
252 return parent
253
305 def select_single(self, type_name='*', **kwargs):254 def select_single(self, type_name='*', **kwargs):
306 """Get a single node from the introspection tree, with type equal to255 """Get a single node from the introspection tree, with type equal to
307 *type_name* and (optionally) matching the keyword filters present in256 *type_name* and (optionally) matching the keyword filters present in
@@ -390,8 +339,9 @@
390 "Selecting objects of %s with attributes: %r",339 "Selecting objects of %s with attributes: %r",
391 'any type' if type_name == '*' else 'type ' + type_name, kwargs)340 'any type' if type_name == '*' else 'type ' + type_name, kwargs)
392341
393 first_param = ''342 server_side_filters = []
394 for k, v in kwargs.iteritems():343 client_side_filters = {}
344 for k, v in kwargs.items():
395 # LP Bug 1209029: The XPathSelect protocol does not allow all valid345 # LP Bug 1209029: The XPathSelect protocol does not allow all valid
396 # node names or values. We need to decide here whether the filter346 # node names or values. We need to decide here whether the filter
397 # parameters are going to work on the backend or not. If not, we347 # parameters are going to work on the backend or not. If not, we
@@ -399,25 +349,33 @@
399 # _is_valid_server_side_filter_param function (below) for the349 # _is_valid_server_side_filter_param function (below) for the
400 # specific requirements.350 # specific requirements.
401 if _is_valid_server_side_filter_param(k, v):351 if _is_valid_server_side_filter_param(k, v):
402 first_param = '[{}={}]'.format(k, v)352 server_side_filters.append(
403 kwargs.pop(k)353 _get_filter_string_for_key_value_pair(k, v)
404 break354 )
405 query_path = "%s//%s%s" % (self.get_class_query_string(),355 else:
406 type_name,356 client_side_filters[k] = v
407 first_param)357 filter_str = '[{}]'.format(','.join(server_side_filters)) \
358 if server_side_filters else ""
359 query_path = "%s//%s%s" % (
360 self.get_class_query_string(),
361 type_name,
362 filter_str
363 )
408364
409 state_dicts = self.get_state_by_path(query_path)365 state_dicts = self.get_state_by_path(query_path)
410 instances = [self.make_introspection_object(i) for i in state_dicts]366 instances = [self.make_introspection_object(i) for i in state_dicts]
411 return filter(lambda i: object_passes_filters(i, **kwargs), instances)367 return [i for i in instances
368 if object_passes_filters(i, **client_side_filters)]
412369
413 def refresh_state(self):370 def refresh_state(self):
414 """Refreshes the object's state from unity.371 """Refreshes the object's state.
415372
416 You should probably never have to call this directly. Autopilot373 You should probably never have to call this directly. Autopilot
417 automatically retrieves new state every time this object's attributes374 automatically retrieves new state every time this object's attributes
418 are read.375 are read.
419376
420 :raises: **StateNotFound** if the object in unity has been destroyed.377 :raises: **StateNotFound** if the object in the application under test
378 has been destroyed.
421379
422 """380 """
423 _, new_state = self.get_new_state()381 _, new_state = self.get_new_state()
@@ -563,9 +521,32 @@
563521
564 """522 """
565 return (523 return (
566 isinstance(value, str) and524 type(value) in (str, int, bool) and
567 re.match(r'^[a-zA-Z0-9_\-]+( [a-zA-Z0-9_\-])*$', key) is not None and525 re.match(r'^[a-zA-Z0-9_\-]+( [a-zA-Z0-9_\-])*$', key) is not None and
568 re.match(r'^[a-zA-Z0-9_\-]+( [a-zA-Z0-9_\-])*$', value) is not None)526 (type(value) != int or -2**31 <= value <= 2**31 - 1))
527
528
529def _get_filter_string_for_key_value_pair(key, value):
530 """Return a string representing the filter query for this key/value pair.
531
532 The value must be suitable for server-side filtering. Raises ValueError if
533 this is not the case.
534
535 """
536 if isinstance(value, str):
537 if _PY3:
538 escaped_value = value.encode("unicode_escape")\
539 .decode('ASCII')\
540 .replace("'", "\\'")
541 else:
542 escaped_value = value.encode("string_escape")
543 # note: string_escape codec escapes "'" but not '"'...
544 escaped_value = escaped_value.replace('"', r'\"')
545 return '{}="{}"'.format(key, escaped_value)
546 elif isinstance(value, int) or isinstance(value, bool):
547 return "{}={}".format(key, repr(value))
548 else:
549 raise ValueError("Unsupported value type: {}".format(type(value)))
569550
570551
571class _CustomEmulatorMeta(IntrospectableObjectMetaclass):552class _CustomEmulatorMeta(IntrospectableObjectMetaclass):
@@ -583,9 +564,10 @@
583 d['_id'] = uuid4()564 d['_id'] = uuid4()
584 return super(_CustomEmulatorMeta, cls).__new__(cls, name, bases, d)565 return super(_CustomEmulatorMeta, cls).__new__(cls, name, bases, d)
585566
586567CustomEmulatorBase = _CustomEmulatorMeta('CustomEmulatorBase',
587class CustomEmulatorBase(DBusIntrospectionObject):568 (DBusIntrospectionObject, ),
588569 {})
570CustomEmulatorBase.__doc__ = \
589 """This class must be used as a base class for any custom emulators defined571 """This class must be used as a base class for any custom emulators defined
590 within a test case.572 within a test case.
591573
@@ -593,5 +575,3 @@
593 Tutorial Section :ref:`custom_emulators`575 Tutorial Section :ref:`custom_emulators`
594 Information on how to write custom emulators.576 Information on how to write custom emulators.
595 """577 """
596
597 __metaclass__ = _CustomEmulatorMeta
598578
=== added file 'autopilot/introspection/types.py'
--- autopilot/introspection/types.py 1970-01-01 00:00:00 +0000
+++ autopilot/introspection/types.py 2013-09-16 17:17:50 +0000
@@ -0,0 +1,619 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#
3# Autopilot Functional Test Tool
4# Copyright (C) 2013 Canonical
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#
19
20
21"""
22Autopilot proxy type support.
23=============================
24
25This module defines the classes that are used for all attributes on proxy
26objects. All proxy objects contain attributes that transparently mirror the
27values present in the application under test. Autopilot takes care of keeping
28these values up to date.
29
30Object attributes fall into two categories. Attributes that are a single
31string, boolean, or integer property are sent directly across DBus. These are
32called "plain" types, and are stored in autopilot as instnaces of the
33:class:`PlainType` class. Attributes that are more complex (a rectangle, for
34example) are called "complex" types, and are split into several component
35values, sent across dbus, and are then reconstituted in autopilot into useful
36objects.
37
38"""
39
40from __future__ import absolute_import
41
42from datetime import datetime, time
43import dbus
44import logging
45from testtools.matchers import Equals
46from time import sleep
47
48from autopilot.introspection.utilities import translate_state_keys
49
50
51logger = logging.getLogger(__name__)
52
53
54class ValueType(object):
55
56 """Store constants for different special types that autopilot understands.
57
58 DO NOT add items here unless you have documented them correctly in
59 docs/appendix/protocol.rst.
60
61 """
62 PLAIN = 0
63 RECTANGLE = 1
64 POINT = 2
65 SIZE = 3
66 COLOR = 4
67 DATETIME = 5
68 TIME = 6
69 UNKNOWN = -1
70
71
72def create_value_instance(value, parent, name):
73 """Create an object that exposes the interesing part of the value
74 specified, given the value_type_id.
75
76 :param parent: The object this attribute belongs to.
77 :param name: The name of this attribute.
78 :param value: The value array from DBus.
79
80 """
81 type_dict = {
82 ValueType.PLAIN: _make_plain_type,
83 ValueType.RECTANGLE: Rectangle,
84 ValueType.POINT: Point,
85 ValueType.SIZE: Size,
86 ValueType.DATETIME: DateTime,
87 ValueType.TIME: Time,
88 ValueType.UNKNOWN: _make_plain_type,
89 }
90 type_id = value[0]
91 value = value[1:]
92
93 if type_id not in type_dict:
94 logger.warning("Unknown type id %d", type_id)
95 type_id = ValueType.UNKNOWN
96
97 type_class = type_dict.get(type_id, None)
98 if type_id == ValueType.UNKNOWN:
99 value = [dbus.Array(value)]
100 if len(value) == 0:
101 raise ValueError("Cannot create attribute, no data supplied")
102 return type_class(*value, parent=parent, name=name)
103
104
105class TypeBase(object):
106
107 def wait_for(self, expected_value, timeout=10):
108 """Wait up to 10 seconds for our value to change to
109 *expected_value*.
110
111 *expected_value* can be a testtools.matcher. Matcher subclass (like
112 LessThan, for example), or an ordinary value.
113
114 This works by refreshing the value using repeated dbus calls.
115
116 :raises AssertionError: if the attribute was not equal to the
117 expected value after 10 seconds.
118
119 :raises RuntimeError: if the attribute you called this on was not
120 constructed as part of an object.
121
122 """
123 # It's guaranteed that our value is up to date, since __getattr__
124 # calls refresh_state. This if statement stops us waiting if the
125 # value is already what we expect:
126 if self == expected_value:
127 return
128
129 if self.name is None or self.parent is None:
130 raise RuntimeError(
131 "This variable was not constructed as part of "
132 "an object. The wait_for method cannot be used."
133 )
134
135 def make_unicode(value):
136 if isinstance(value, bytes):
137 return value.decode('utf8')
138 return value
139
140 if hasattr(expected_value, 'expected'):
141 expected_value.expected = make_unicode(expected_value.expected)
142
143 # unfortunately not all testtools matchers derive from the Matcher
144 # class, so we can't use issubclass, isinstance for this:
145 match_fun = getattr(expected_value, 'match', None)
146 is_matcher = match_fun and callable(match_fun)
147 if not is_matcher:
148 expected_value = Equals(expected_value)
149
150 time_left = timeout
151 while True:
152 # TODO: These next three lines are duplicated from the parent...
153 # can we just have this code once somewhere?
154 _, new_state = self.parent.get_new_state()
155 new_state = translate_state_keys(new_state)
156 new_value = make_unicode(new_state[self.name][1]) # [1] is the val
157 # Support for testtools.matcher classes:
158 mismatch = expected_value.match(new_value)
159 if mismatch:
160 failure_msg = mismatch.describe()
161 else:
162 self.parent._set_properties(new_state)
163 return
164
165 if time_left >= 1:
166 sleep(1)
167 time_left -= 1
168 else:
169 sleep(time_left)
170 break
171
172 raise AssertionError(
173 "After %.1f seconds test on %s.%s failed: %s" % (
174 timeout, self.parent.__class__.__name__, self.name,
175 failure_msg))
176
177
178class PlainType(TypeBase):
179
180 """Plain type support in autopilot proxy objects.
181
182 Instances of this class will be used for all plain attrubites. The word
183 "plain" in this context means anything that's marshalled as a string,
184 boolean or integer type across dbus.
185
186 Instances of these classes can be used just like the underlying type. For
187 example, given an object property called 'length' that is marshalled over
188 dbus as an integer value, the following will be true::
189
190 >>> isinstance(object.length, PlainType)
191 True
192 >>> isinstance(object.length, int)
193 True
194 >>> print(object.length)
195 123
196 >>> print(object.length + 32)
197 155
198
199 However, a special case exists for boolean values: because you cannot
200 subclass from the 'bool' type, the following check will fail (
201 ``object.visible`` is a boolean property)::
202
203 >>> isinstance(object.visible, bool)
204 False
205
206 However boolean values will behave exactly as you expect them to.
207
208 """
209
210 def __new__(cls, value, parent=None, name=None):
211 return _make_plain_type(value, parent=parent, name=name)
212
213
214def _make_plain_type(value, parent=None, name=None):
215 new_type_name = type(value).__name__
216 new_type_bases = (type(value), PlainType)
217 new_type_dict = dict(parent=parent, name=name)
218 new_type = type(new_type_name, new_type_bases, new_type_dict)
219 return new_type(value)
220
221
222def _array_packed_type(num_args):
223 """Return a base class that accepts 'num_args' and is packed into a dbus
224 Array type.
225
226 """
227 class _ArrayPackedType(dbus.Array, TypeBase):
228
229 def __init__(self, *args, **kwargs):
230 if len(args) != self._required_arg_count:
231 raise ValueError(
232 "%s must be constructed with %d arguments, not %d"
233 % (
234 self.__class__.__name__,
235 self._required_arg_count,
236 len(args)
237 )
238 )
239 super(_ArrayPackedType, self).__init__(args)
240 # TODO: pop instead of get, and raise on unknown kwarg
241 self.parent = kwargs.get("parent", None)
242 self.name = kwargs.get("name", None)
243 return type(
244 "_ArrayPackedType_{}".format(num_args),
245 (_ArrayPackedType,),
246 dict(_required_arg_count=num_args)
247 )
248
249
250class Rectangle(_array_packed_type(4)):
251
252 """The RectangleType class represents a rectangle in cartesian space.
253
254 To construct a rectangle, pass the x, y, width and height parameters in to
255 the class constructor::
256
257 my_rect = Rectangle(12,13,100,150)
258
259 These attributes can be accessed either using named attributes, or via
260 sequence indexes::
261
262 >>> my_rect.x == my_rect[0] == 12
263 True
264 >>> my_rect.y == my_rect[1] == 13
265 True
266 >>> my_rect.w == my_rect[2] == 100
267 True
268 >>> my_rect.h == my_rect[3] == 150
269 True
270
271 You may also access the width and height values using the ``width`` and
272 ``height`` properties::
273
274 >>> my_rect.width == my_rect.w
275 True
276 >>> my_rect.height == my_rect.h
277 True
278
279 Rectangles can be compared using ``==`` and ``!=``, either to another
280 Rectangle instance, or to any mutable sequence type::
281
282 >>> my_rect == [12, 13, 100, 150]
283 True
284 >>> my_rect != Rectangle(1,2,3,4)
285 True
286
287 """
288
289 @property
290 def x(self):
291 return self[0]
292
293 @property
294 def y(self):
295 return self[1]
296
297 @property
298 def w(self):
299 return self[2]
300
301 @property
302 def width(self):
303 return self[2]
304
305 @property
306 def h(self):
307 return self[3]
308
309 @property
310 def height(self):
311 return self[3]
312
313
314class Point(_array_packed_type(2)):
315
316 """The Point class represents a 2D point in cartesian space.
317
318 To construct a Point, pass in the x, y parameters to the class
319 constructor::
320
321 >>> my_point = Point(50,100)
322
323 These attributes can be accessed either using named attributes, or via
324 sequence indexes::
325
326 >>> my_point.x == my_point[0] == 50
327 True
328 >>> my_point.y == my_point[1] == 100
329 True
330
331 Point instances can be compared using ``==`` and ``!=``, either to another
332 Point instance, or to any mutable sequence type with the correct number of
333 items::
334
335 >>> my_point == [50, 100]
336 True
337 >>> my_point != Point(5, 10)
338 True
339
340 """
341
342 @property
343 def x(self):
344 return self[0]
345
346 @property
347 def y(self):
348 return self[1]
349
350
351class Size(_array_packed_type(2)):
352
353 """The Size class represents a 2D size in cartesian space.
354
355 To construct a Size, pass in the width, height parameters to the class
356 constructor::
357
358 >>> my_size = Size(50,100)
359
360 These attributes can be accessed either using named attributes, or via
361 sequence indexes::
362
363 >>> my_size.width == my_size.w == my_size[0] == 50
364 True
365 >>> my_size.height == my_size.h == my_size[1] == 100
366 True
367
368 Size instances can be compared using ``==`` and ``!=``, either to another
369 Size instance, or to any mutable sequence type with the correct number of
370 items::
371
372 >>> my_size == [50, 100]
373 True
374 >>> my_size != Size(5, 10)
375 True
376
377 """
378
379 @property
380 def w(self):
381 return self[0]
382
383 @property
384 def width(self):
385 return self[0]
386
387 @property
388 def h(self):
389 return self[1]
390
391 @property
392 def height(self):
393 return self[1]
394
395
396class Color(_array_packed_type(4)):
397
398 """The Color class represents an RGBA Color.
399
400 To construct a Color, pass in the red, green, blue and alpha parameters to
401 the class constructor::
402
403 >>> my_color = Color(50, 100, 200, 255)
404
405 These attributes can be accessed either using named attributes, or via
406 sequence indexes::
407
408 >>> my_color.red == my_color[0] == 50
409 True
410 >>> my_color.green == my_color[1] == 100
411 True
412 >>> my_color.blue == my_color[2] == 200
413 True
414 >>> my_color.alpha == my_color[3] == 255
415 True
416
417 Color instances can be compared using ``==`` and ``!=``, either to another
418 Color instance, or to any mutable sequence type with the correct number of
419 items::
420
421 >>> my_color == [50, 100, 200, 255]
422 True
423 >>> my_color != Color(5, 10, 0, 0)
424 True
425
426 """
427
428 @property
429 def red(self):
430 return self[0]
431
432 @property
433 def green(self):
434 return self[1]
435
436 @property
437 def blue(self):
438 return self[2]
439
440 @property
441 def alpha(self):
442 return self[3]
443
444
445class DateTime(_array_packed_type(1)):
446
447 """The DateTime class represents a date and time in the UTC timezone.
448
449 DateTime is constructed by passing a unix timestamp in to the constructor.
450 Timestamps are expressed as the number of seconds since 1970-01-01T00:00:00
451 in the UTC timezone::
452
453 >>> my_dt = DateTime(1377209927)
454
455 This timestamp can always be accessed either using index access or via a
456 named property::
457
458 >>> my_dt[0] == my_dt.timestamp == 1377209927
459 True
460
461 DateTime objects also expose the usual named properties you would expect on
462 a date/time object::
463
464 >>> my_dt.year
465 2013
466 >>> my_dt.month
467 8
468 >>> my_dt.day
469 22
470 >>> my_dt.hour
471 22
472 >>> my_dt.minute
473 18
474 >>> my_dt.second
475 47
476
477 Two DateTime objects can be compared for equality::
478
479 >>> my_dt == DateTime(1377209927)
480 True
481
482 You can also compare a DateTime with any mutable sequence type containing
483 the timestamp (although this probably isn't very useful for test authors)::
484
485 >>> my_dt == [1377209927]
486 True
487
488 Finally, you can also compare a DateTime instance with a python datetime
489 instance::
490
491 >>> my_datetime = datetime.datetime.fromutctimestamp(1377209927)
492 True
493
494 DateTime instances can be converted to datetime instances:
495
496 >>> isinstance(my_dt.datetime, datetime.datetime)
497 True
498
499 """
500 def __init__(self, *args, **kwargs):
501 super(DateTime, self).__init__(*args, **kwargs)
502 self._cached_dt = datetime.utcfromtimestamp(self[0])
503
504 @property
505 def year(self):
506 return self._cached_dt.year
507
508 @property
509 def month(self):
510 return self._cached_dt.month
511
512 @property
513 def day(self):
514 return self._cached_dt.day
515
516 @property
517 def hour(self):
518 return self._cached_dt.hour
519
520 @property
521 def minute(self):
522 return self._cached_dt.minute
523
524 @property
525 def second(self):
526 return self._cached_dt.second
527
528 @property
529 def timestamp(self):
530 return self[0]
531
532 @property
533 def datetime(self):
534 return self._cached_dt
535
536 def __eq__(self, other):
537 if isinstance(other, datetime):
538 return other == self._cached_dt
539 return super(DateTime, self).__eq__(other)
540
541
542class Time(_array_packed_type(4)):
543
544 """The Time class represents a time, without a date component.
545
546 You can construct a Time instnace by passing the hours, minutes, seconds,
547 and milliseconds to the class constructor::
548
549 >>> my_time = Time(12, 34, 01, 23)
550
551 The values passed in must be valid for their positions (ie..- 0-23 for
552 hours, 0-59 for minutes and seconds, and 0-999 for milliseconds). Passing
553 invalid values will cause a ValueError to be raised.
554
555 The hours, minutes, seconds, and milliseconds can be accessed using either
556 index access or named properties::
557
558 >>> my_time.hours == my_time[0] == 12
559 True
560 >>> my_time.minutes == my_time[1] == 34
561 True
562 >>> my_time.seconds == my_time[2] == 01
563 True
564 >>> my_time.milliseconds == my_time[3] == 23
565 True
566
567 Time instances can be compared to other time instances, any mutable
568 sequence containing four integers, or datetime.time instances::
569
570 >>> my_time == Time(12, 34, 01, 23)
571 True
572 >>> my_time == Time(1,2,3,4)
573 False
574
575 >>> my_time == [12, 34, 01, 23]
576 True
577
578 >>> my_time == datetime.time(12, 34, 01, 23000)
579 True
580
581 Note that the Time class stores milliseconds, while the ``datettime.time``
582 class stores microseconds.
583
584 Finally, you can get a ``datetime.time`` instance from a Time instance::
585
586 >>> isinstance(my_time.time, datetime.time)
587 True
588
589 """
590
591 def __init__(self, *args, **kwargs):
592 super(Time, self).__init__(*args, **kwargs)
593 # datetime.time uses microseconds, instead of mulliseconds:
594 self._cached_time = time(self[0], self[1], self[2], self[3] * 1000)
595
596 @property
597 def hour(self):
598 return self._cached_time.hour
599
600 @property
601 def minute(self):
602 return self._cached_time.minute
603
604 @property
605 def second(self):
606 return self._cached_time.second
607
608 @property
609 def millisecond(self):
610 return self._cached_time.microsecond / 1000
611
612 @property
613 def time(self):
614 return self._cached_time
615
616 def __eq__(self, other):
617 if isinstance(other, time):
618 return other == self._cached_time
619 return super(DateTime, self).__eq__(other)
0620
=== modified file 'autopilot/introspection/utilities.py'
--- autopilot/introspection/utilities.py 2013-07-30 15:08:30 +0000
+++ autopilot/introspection/utilities.py 2013-09-16 17:17:50 +0000
@@ -39,3 +39,9 @@
39 bus_obj = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')39 bus_obj = bus.get_object('org.freedesktop.DBus', '/org/freedesktop/DBus')
40 bus_iface = Interface(bus_obj, 'org.freedesktop.DBus')40 bus_iface = Interface(bus_obj, 'org.freedesktop.DBus')
41 return bus_iface.GetConnectionUnixProcessID(connection_name)41 return bus_iface.GetConnectionUnixProcessID(connection_name)
42
43
44def translate_state_keys(state_dict):
45 """Translates the *state_dict* passed in so the keys are usable as python
46 attributes."""
47 return {k.replace('-', '_'): v for k, v in state_dict.items()}
4248
=== modified file 'autopilot/keybindings.py'
--- autopilot/keybindings.py 2013-09-16 14:37:29 +0000
+++ autopilot/keybindings.py 2013-09-16 17:17:50 +0000
@@ -36,8 +36,8 @@
36from __future__ import absolute_import36from __future__ import absolute_import
3737
38import logging38import logging
39from types import NoneType
40import re39import re
40import sys
4141
42from autopilot.input import Keyboard42from autopilot.input import Keyboard
43from autopilot.utilities import Silence43from autopilot.utilities import Silence
@@ -45,6 +45,10 @@
45logger = logging.getLogger(__name__)45logger = logging.getLogger(__name__)
4646
4747
48# py2 compatible alias for py3
49if sys.version >= '3':
50 basestring = str
51
48#52#
49# Fill this dictionary with keybindings we want to store.53# Fill this dictionary with keybindings we want to store.
50#54#
@@ -262,7 +266,7 @@
262 keyboard emulator.266 keyboard emulator.
263267
264 """268 """
265 if type(delay) not in (float, NoneType):269 if delay is not None and type(delay) != float:
266 raise TypeError(270 raise TypeError(
267 "delay parameter must be a float if it is defined.")271 "delay parameter must be a float if it is defined.")
268 if delay:272 if delay:
269273
=== modified file 'autopilot/matchers/__init__.py'
--- autopilot/matchers/__init__.py 2013-08-06 20:15:11 +0000
+++ autopilot/matchers/__init__.py 2013-09-16 17:17:50 +0000
@@ -108,7 +108,7 @@
108 try:108 try:
109 wait_fun(self.matcher, self.timeout)109 wait_fun(self.matcher, self.timeout)
110 except AssertionError as e:110 except AssertionError as e:
111 return Mismatch(unicode(e))111 return Mismatch(str(e))
112 return None112 return None
113113
114 def __str__(self):114 def __str__(self):
115115
=== modified file 'autopilot/process/_bamf.py'
--- autopilot/process/_bamf.py 2013-08-12 16:11:03 +0000
+++ autopilot/process/_bamf.py 2013-09-16 17:17:50 +0000
@@ -168,11 +168,11 @@
168 [new_windows.extend(a.get_windows()) for a in apps]168 [new_windows.extend(a.get_windows()) for a in apps]
169 filter_fn = lambda w: w.x_id not in [169 filter_fn = lambda w: w.x_id not in [
170 c.x_id for c in existing_windows]170 c.x_id for c in existing_windows]
171 new_wins = filter(filter_fn, new_windows)171 new_wins = list(filter(filter_fn, new_windows))
172 if new_wins:172 if new_wins:
173 assert len(new_wins) == 1173 assert len(new_wins) == 1
174 return new_wins[0]174 return new_wins[0]
175 except DBusException:175 except dbus.DBusException:
176 pass176 pass
177 sleep(1)177 sleep(1)
178 return None178 return None
@@ -223,7 +223,7 @@
223 apps = [Application(p) for p in223 apps = [Application(p) for p in
224 self.matcher_interface.RunningApplications()]224 self.matcher_interface.RunningApplications()]
225 if user_visible_only:225 if user_visible_only:
226 return filter(_filter_user_visible, apps)226 return list(filter(_filter_user_visible, apps))
227 return apps227 return apps
228228
229 def get_running_applications_by_desktop_file(self, desktop_file):229 def get_running_applications_by_desktop_file(self, desktop_file):
@@ -255,7 +255,7 @@
255 windows = [Window(w) for w in255 windows = [Window(w) for w in
256 self.matcher_interface.WindowStackForMonitor(-1)]256 self.matcher_interface.WindowStackForMonitor(-1)]
257 if user_visible_only:257 if user_visible_only:
258 windows = filter(_filter_user_visible, windows)258 windows = list(filter(_filter_user_visible, windows))
259 # Now sort on stacking order.259 # Now sort on stacking order.
260 # We explicitly convert to a list from an iterator since tests260 # We explicitly convert to a list from an iterator since tests
261 # frequently try and use len() on return values from these methods.261 # frequently try and use len() on return values from these methods.
@@ -349,8 +349,8 @@
349 self._app_proxy, 'org.ayatana.bamf.view')349 self._app_proxy, 'org.ayatana.bamf.view')
350 self._app_iface = dbus.Interface(350 self._app_iface = dbus.Interface(
351 self._app_proxy, 'org.ayatana.bamf.application')351 self._app_proxy, 'org.ayatana.bamf.application')
352 except dbus.DBusException, e:352 except dbus.DBusException as e:
353 e.message += 'bamf_app_path=%r' % (bamf_app_path)353 e.args += ('bamf_app_path=%r' % (bamf_app_path),)
354 raise354 raise
355355
356 @property356 @property
@@ -413,6 +413,8 @@
413 return "<Application '%s'>" % (self.name)413 return "<Application '%s'>" % (self.name)
414414
415 def __eq__(self, other):415 def __eq__(self, other):
416 if other is None:
417 return False
416 return self.desktop_file == other.desktop_file418 return self.desktop_file == other.desktop_file
417419
418420
@@ -611,5 +613,5 @@
611 """Return a list of strings representing the current window state."""613 """Return a list of strings representing the current window state."""
612614
613 get_display().sync()615 get_display().sync()
614 return map(get_display().get_atom_name, self._getProperty(616 return [get_display().get_atom_name(p)
615 '_NET_WM_STATE'))617 for p in self._getProperty('_NET_WM_STATE')]
616618
=== modified file 'autopilot/testcase.py'
--- autopilot/testcase.py 2013-07-29 10:29:37 +0000
+++ autopilot/testcase.py 2013-09-16 17:17:50 +0000
@@ -251,7 +251,8 @@
251 data is retrievable via this object.251 data is retrievable via this object.
252252
253 """253 """
254 app_path = subprocess.check_output(['which', application]).strip()254 app_path = subprocess.check_output(['which', application],
255 universal_newlines=True).strip()
255 # Get a launcher, tests can override this if they need:256 # Get a launcher, tests can override this if they need:
256 launcher_hint = kwargs.pop('app_type', '')257 launcher_hint = kwargs.pop('app_type', '')
257 launcher = None258 launcher = None
@@ -294,8 +295,8 @@
294 new_apps = []295 new_apps = []
295 for i in range(10):296 for i in range(10):
296 current_apps = self.process_manager.get_running_applications()297 current_apps = self.process_manager.get_running_applications()
297 new_apps = filter(298 new_apps = list(filter(
298 lambda i: i not in self._app_snapshot, current_apps)299 lambda i: i not in self._app_snapshot, current_apps))
299 if not new_apps:300 if not new_apps:
300 self._app_snapshot = None301 self._app_snapshot = None
301 return302 return
@@ -399,7 +400,7 @@
399 if not kwargs:400 if not kwargs:
400 raise ValueError("At least one keyword argument must be present.")401 raise ValueError("At least one keyword argument must be present.")
401402
402 for prop_name, desired_value in kwargs.iteritems():403 for prop_name, desired_value in kwargs.items():
403 none_val = object()404 none_val = object()
404 attr = getattr(obj, prop_name, none_val)405 attr = getattr(obj, prop_name, none_val)
405 if attr == none_val:406 if attr == none_val:
406407
=== modified file 'autopilot/tests/functional/test_ap_apps.py'
--- autopilot/tests/functional/test_ap_apps.py 2013-07-30 15:08:30 +0000
+++ autopilot/tests/functional/test_ap_apps.py 2013-09-16 17:17:50 +0000
@@ -23,9 +23,10 @@
23import stat23import stat
24import subprocess24import subprocess
25import logging25import logging
26import sys
26from mock import patch27from mock import patch
27from tempfile import mktemp28from tempfile import mktemp
28from testtools.matchers import raises, LessThan, Equals29from testtools.matchers import raises, LessThan
29from textwrap import dedent30from textwrap import dedent
30from time import sleep31from time import sleep
3132
@@ -37,6 +38,10 @@
37)38)
3839
3940
41# backwards compatible alias for Python 3
42if sys.version > '3':
43 xrange = range
44
40logger = logging.getLogger(__name__)45logger = logging.getLogger(__name__)
4146
4247
@@ -90,14 +95,14 @@
9095
91 """96 """
92 path = self.write_script(dedent("""\97 path = self.write_script(dedent("""\
93 #!/usr/bin/python98 #!%s
9499
95 from time import sleep100 from time import sleep
96101
97 while True:102 while True:
98 print "Still running"103 print("Still running")
99 sleep(1)104 sleep(1)
100 """))105 """ % sys.executable))
101106
102 expected_error = "Search criteria returned no results"107 expected_error = "Search criteria returned no results"
103 self.assertThat(108 self.assertThat(
@@ -123,14 +128,14 @@
123128
124 """129 """
125 path = self.write_script(dedent("""\130 path = self.write_script(dedent("""\
126 #!/usr/bin/python131 #!%s
127132
128 from time import sleep133 from time import sleep
129 import sys134 import sys
130135
131 sleep(5)136 sleep(5)
132 sys.exit(1)137 sys.exit(1)
133 """))138 """ % sys.executable))
134139
135 expected_error = "Process exited with exit code: 1"140 expected_error = "Process exited with exit code: 1"
136 self.assertThat(141 self.assertThat(
@@ -148,14 +153,14 @@
148153
149 """154 """
150 path = self.write_script(dedent("""\155 path = self.write_script(dedent("""\
151 #!/usr/bin/python156 #!%s
152157
153 from time import sleep158 from time import sleep
154 import sys159 import sys
155160
156 sleep(1)161 sleep(1)
157 sys.exit(1)162 sys.exit(1)
158 """))163 """ % sys.executable))
159 start = datetime.datetime.now()164 start = datetime.datetime.now()
160165
161 try:166 try:
@@ -177,7 +182,7 @@
177182
178 """183 """
179 path = self.write_script(dedent("""\184 path = self.write_script(dedent("""\
180 #!/usr/bin/python185 #!%s
181 from PyQt4.QtGui import QMainWindow, QApplication186 from PyQt4.QtGui import QMainWindow, QApplication
182 from PyQt4.QtCore import QTimer187 from PyQt4.QtCore import QTimer
183188
@@ -188,7 +193,7 @@
188 win.show()193 win.show()
189 QTimer.singleShot(8000, app.exit)194 QTimer.singleShot(8000, app.exit)
190 app.exec_()195 app.exec_()
191 """))196 """ % sys.executable))
192 app_proxy = self.launch_test_application(path, app_type='qt')197 app_proxy = self.launch_test_application(path, app_type='qt')
193 self.assertTrue(app_proxy is not None)198 self.assertTrue(app_proxy is not None)
194199
@@ -214,7 +219,8 @@
214 # We cannot use 'which', as qtchooser installs wrappers - we need to219 # We cannot use 'which', as qtchooser installs wrappers - we need to
215 # check in the actual library paths220 # check in the actual library paths
216 env = subprocess.check_output(221 env = subprocess.check_output(
217 ['qtchooser', '-qt=' + version, '-print-env']).split('\n')222 ['qtchooser', '-qt=' + version, '-print-env'],
223 universal_newlines=True).split('\n')
218 for i in env:224 for i in env:
219 if i.find('QTTOOLDIR') >= 0:225 if i.find('QTTOOLDIR') >= 0:
220 path = i.lstrip("QTTOOLDIR=").strip('"') + "/" + name226 path = i.lstrip("QTTOOLDIR=").strip('"') + "/" + name
@@ -226,7 +232,8 @@
226 def _find_qt_binary_old(self, version, name):232 def _find_qt_binary_old(self, version, name):
227 # Check for the existence of the binary the old way233 # Check for the existence of the binary the old way
228 try:234 try:
229 path = subprocess.check_output(['which', 'qmlviewer']).strip()235 path = subprocess.check_output(['which', 'qmlviewer'],
236 universal_newlines=True).strip()
230 except subprocess.CalledProcessError:237 except subprocess.CalledProcessError:
231 path = None238 path = None
232 return path239 return path
@@ -236,7 +243,9 @@
236243
237 try:244 try:
238 qtversions = subprocess.check_output(245 qtversions = subprocess.check_output(
239 ['qtchooser', '-list-versions']).split('\n')246 ['qtchooser', '-list-versions'],
247 universal_newlines=True
248 ).split('\n')
240 check_func = self._find_qt_binary_chooser249 check_func = self._find_qt_binary_chooser
241 except OSError:250 except OSError:
242 # This means no qtchooser is installed, so let's check for251 # This means no qtchooser is installed, so let's check for
@@ -268,7 +277,7 @@
268277
269 def test_can_launch_qt_script(self):278 def test_can_launch_qt_script(self):
270 path = self.write_script(dedent("""\279 path = self.write_script(dedent("""\
271 #!/usr/bin/python280 #!%s
272 from PyQt4.QtGui import QMainWindow, QApplication281 from PyQt4.QtGui import QMainWindow, QApplication
273 from sys import argv282 from sys import argv
274283
@@ -276,13 +285,13 @@
276 win = QMainWindow()285 win = QMainWindow()
277 win.show()286 win.show()
278 app.exec_()287 app.exec_()
279 """))288 """ % sys.executable))
280 app_proxy = self.launch_test_application(path, app_type='qt')289 app_proxy = self.launch_test_application(path, app_type='qt')
281 self.assertTrue(app_proxy is not None)290 self.assertTrue(app_proxy is not None)
282291
283 def test_can_launch_wrapper_script(self):292 def test_can_launch_wrapper_script(self):
284 path = self.write_script(dedent("""\293 path = self.write_script(dedent("""\
285 #!/usr/bin/python294 #!%s
286 from PyQt4.QtGui import QMainWindow, QApplication295 from PyQt4.QtGui import QMainWindow, QApplication
287 from sys import argv296 from sys import argv
288297
@@ -290,7 +299,7 @@
290 win = QMainWindow()299 win = QMainWindow()
291 win.show()300 win.show()
292 app.exec_()301 app.exec_()
293 """))302 """ % sys.executable))
294 wrapper_path = self.write_script(dedent("""\303 wrapper_path = self.write_script(dedent("""\
295 #!/bin/sh304 #!/bin/sh
296305
@@ -309,7 +318,7 @@
309318
310 try:319 try:
311 self.app_path = subprocess.check_output(320 self.app_path = subprocess.check_output(
312 ['which', 'gnome-mahjongg']).strip()321 ['which', 'gnome-mahjongg'], universal_newlines=True).strip()
313 except subprocess.CalledProcessError:322 except subprocess.CalledProcessError:
314 self.skip("gnome-mahjongg not found.")323 self.skip("gnome-mahjongg not found.")
315324
@@ -319,27 +328,27 @@
319328
320 def test_can_launch_gtk_script(self):329 def test_can_launch_gtk_script(self):
321 path = self.write_script(dedent("""\330 path = self.write_script(dedent("""\
322 #!/usr/bin/python331 #!%s
323 from gi.repository import Gtk332 from gi.repository import Gtk
324333
325 win = Gtk.Window()334 win = Gtk.Window()
326 win.connect("delete-event", Gtk.main_quit)335 win.connect("delete-event", Gtk.main_quit)
327 win.show_all()336 win.show_all()
328 Gtk.main()337 Gtk.main()
329 """))338 """ % sys.executable))
330 app_proxy = self.launch_test_application(path, app_type='gtk')339 app_proxy = self.launch_test_application(path, app_type='gtk')
331 self.assertTrue(app_proxy is not None)340 self.assertTrue(app_proxy is not None)
332341
333 def test_can_launch_wrapper_script(self):342 def test_can_launch_wrapper_script(self):
334 path = self.write_script(dedent("""\343 path = self.write_script(dedent("""\
335 #!/usr/bin/python344 #!%s
336 from gi.repository import Gtk345 from gi.repository import Gtk
337346
338 win = Gtk.Window()347 win = Gtk.Window()
339 win.connect("delete-event", Gtk.main_quit)348 win.connect("delete-event", Gtk.main_quit)
340 win.show_all()349 win.show_all()
341 Gtk.main()350 Gtk.main()
342 """))351 """ % sys.executable))
343 wrapper_path = self.write_script(dedent("""\352 wrapper_path = self.write_script(dedent("""\
344 #!/bin/sh353 #!/bin/sh
345354
346355
=== modified file 'autopilot/tests/functional/test_autopilot_functional.py'
--- autopilot/tests/functional/test_autopilot_functional.py 2013-08-29 06:46:41 +0000
+++ autopilot/tests/functional/test_autopilot_functional.py 2013-09-16 17:17:50 +0000
@@ -99,7 +99,8 @@
99 environment_patch['PYTHONPATH'] = ap_base_path99 environment_patch['PYTHONPATH'] = ap_base_path
100 bin_path = os.path.join(ap_base_path, 'bin', 'autopilot')100 bin_path = os.path.join(ap_base_path, 'bin', 'autopilot')
101 if not os.path.exists(bin_path):101 if not os.path.exists(bin_path):
102 bin_path = subprocess.check_output(['which', 'autopilot']).strip()102 bin_path = subprocess.check_output(['which', 'autopilot'],
103 universal_newlines=True).strip()
103 logger.info(104 logger.info(
104 "Not running from source, setting bin_path to %s", bin_path)105 "Not running from source, setting bin_path to %s", bin_path)
105106
@@ -119,6 +120,7 @@
119 env=environ,120 env=environ,
120 stdout=subprocess.PIPE,121 stdout=subprocess.PIPE,
121 stderr=subprocess.PIPE,122 stderr=subprocess.PIPE,
123 universal_newlines=True,
122 )124 )
123125
124 stdout, stderr = process.communicate()126 stdout, stderr = process.communicate()
@@ -173,7 +175,7 @@
173175
174 if type(tests) is not list:176 if type(tests) is not list:
175 raise TypeError("tests must be a list, not %r" % type(tests))177 raise TypeError("tests must be a list, not %r" % type(tests))
176 if not isinstance(output, basestring):178 if not isinstance(output, str):
177 raise TypeError("output must be a string, not %r" % type(output))179 raise TypeError("output must be a string, not %r" % type(output))
178180
179 test_names = ''.join([' %s\n' % t for t in sorted(tests)])181 test_names = ''.join([' %s\n' % t for t in sorted(tests)])
@@ -738,7 +740,7 @@
738740
739 self.assertThat(code, Equals(1))741 self.assertThat(code, Equals(1))
740 self.assertTrue(os.path.exists(output_file_path))742 self.assertTrue(os.path.exists(output_file_path))
741 log_contents = unicode(open(output_file_path, encoding='utf-8').read())743 log_contents = open(output_file_path, encoding='utf-8').read()
742 self.assertThat(744 self.assertThat(
743 log_contents,745 log_contents,
744 Contains(u'\xa1pl\u0279oM \u01ddpo\u0254\u0131u\u2229 oll\u01ddH'))746 Contains(u'\xa1pl\u0279oM \u01ddpo\u0254\u0131u\u2229 oll\u01ddH'))
@@ -773,7 +775,7 @@
773775
774 self.assertThat(code, Equals(1))776 self.assertThat(code, Equals(1))
775 self.assertTrue(os.path.exists(output_file_path))777 self.assertTrue(os.path.exists(output_file_path))
776 log_contents = unicode(open(output_file_path, encoding='utf-8').read())778 log_contents = open(output_file_path, encoding='utf-8').read()
777 self.assertThat(779 self.assertThat(
778 log_contents,780 log_contents,
779 Contains(u'\xa1pl\u0279oM \u01ddpo\u0254\u0131u\u2229 oll\u01ddH'))781 Contains(u'\xa1pl\u0279oM \u01ddpo\u0254\u0131u\u2229 oll\u01ddH'))
780782
=== modified file 'autopilot/tests/functional/test_dbus_query.py'
--- autopilot/tests/functional/test_dbus_query.py 2013-07-29 10:29:37 +0000
+++ autopilot/tests/functional/test_dbus_query.py 2013-09-16 17:17:50 +0000
@@ -73,6 +73,18 @@
73 main_window = app.select_single('QMainWindow')73 main_window = app.select_single('QMainWindow')
74 self.assertThat(main_window, NotEquals(None))74 self.assertThat(main_window, NotEquals(None))
7575
76 def test_can_select_parent_of_root(self):
77 """Must be able to select the parent of the root object."""
78 root = self.start_fully_featured_app()
79 root_parent = root.get_parent()
80 self.assertThat(root.id, Equals(root_parent.id))
81
82 def test_can_select_parent_of_normal_node(self):
83 root = self.start_fully_featured_app()
84 main_window = root.select_single('QMainWindow')
85 window_parent = main_window.get_parent()
86 self.assertThat(window_parent.id, Equals(root.id))
87
76 def test_single_select_on_object(self):88 def test_single_select_on_object(self):
77 """Must be able to select a single unique child of an object."""89 """Must be able to select a single unique child of an object."""
78 app = self.start_fully_featured_app()90 app = self.start_fully_featured_app()
@@ -156,7 +168,7 @@
156 def test_select_many_only_using_parameters(self):168 def test_select_many_only_using_parameters(self):
157 app = self.start_fully_featured_app()169 app = self.start_fully_featured_app()
158 many_help_menus = app.select_many(title='Help')170 many_help_menus = app.select_many(title='Help')
159 self.assertThat(len(many_help_menus), Equals(2))171 self.assertThat(len(many_help_menus), Equals(1))
160172
161 def test_select_many_with_no_parameter_matches_returns_empty_list(self):173 def test_select_many_with_no_parameter_matches_returns_empty_list(self):
162 app = self.start_fully_featured_app()174 app = self.start_fully_featured_app()
@@ -172,7 +184,8 @@
172 super(DbusCustomBusTests, self).setUp()184 super(DbusCustomBusTests, self).setUp()
173185
174 def _enable_custom_dbus_bus(self):186 def _enable_custom_dbus_bus(self):
175 p = subprocess.Popen(['dbus-launch'], stdout=subprocess.PIPE)187 p = subprocess.Popen(['dbus-launch'], stdout=subprocess.PIPE,
188 universal_newlines=True)
176 output = p.communicate()189 output = p.communicate()
177 results = output[0].split("\n")190 results = output[0].split("\n")
178 dbus_pid = int(results[1].split("=")[1])191 dbus_pid = int(results[1].split("=")[1])
179192
=== modified file 'autopilot/tests/functional/test_input_stack.py'
--- autopilot/tests/functional/test_input_stack.py 2013-09-04 03:44:07 +0000
+++ autopilot/tests/functional/test_input_stack.py 2013-09-16 17:17:50 +0000
@@ -27,11 +27,13 @@
27from unittest import SkipTest27from unittest import SkipTest
28from mock import patch28from mock import patch
2929
30from autopilot.display import Display
30from autopilot import platform31from autopilot import platform
31from autopilot.testcase import AutopilotTestCase, multiply_scenarios32from autopilot.testcase import AutopilotTestCase, multiply_scenarios
32from autopilot.input import Keyboard, Mouse, Pointer, Touch33from autopilot.input import Keyboard, Mouse, Pointer, Touch
33from autopilot.input._common import get_center_point34from autopilot.input._common import get_center_point
34from autopilot.matchers import Eventually35from autopilot.matchers import Eventually
36from autopilot.testcase import AutopilotTestCase, multiply_scenarios
35from autopilot.utilities import on_test_started37from autopilot.utilities import on_test_started
3638
3739
@@ -274,9 +276,12 @@
274 LP bug #1195499.276 LP bug #1195499.
275277
276 """278 """
279 screen_geometry = Display.create().get_screen_geometry(0)
277 device = Mouse.create()280 device = Mouse.create()
278 device.move(10, 10.6)281 target_x = screen_geometry[0] + 10
279 self.assertEqual(device.position(), (10, 10))282 target_y = screen_geometry[1] + 10.6
283 device.move(target_x, target_y)
284 self.assertEqual(device.position(), (target_x, int(target_y)))
280285
281 @patch('autopilot.platform.model', new=lambda *args: "Not Desktop", )286 @patch('autopilot.platform.model', new=lambda *args: "Not Desktop", )
282 def test_mouse_creation_on_device_raises_useful_error(self):287 def test_mouse_creation_on_device_raises_useful_error(self):
283288
=== modified file 'autopilot/tests/functional/test_introspection_features.py'
--- autopilot/tests/functional/test_introspection_features.py 2013-07-14 09:22:48 +0000
+++ autopilot/tests/functional/test_introspection_features.py 2013-09-16 17:17:50 +0000
@@ -104,7 +104,8 @@
104104
105 def launch_test_qml(self):105 def launch_test_qml(self):
106 arch = subprocess.check_output(106 arch = subprocess.check_output(
107 ["dpkg-architecture", "-qDEB_HOST_MULTIARCH"]).strip()107 ["dpkg-architecture", "-qDEB_HOST_MULTIARCH"],
108 universal_newlines=True).strip()
108 qml_path = tempfile.mktemp(suffix='.qml')109 qml_path = tempfile.mktemp(suffix='.qml')
109 open(qml_path, 'w').write(self.test_qml)110 open(qml_path, 'w').write(self.test_qml)
110 self.addCleanup(os.remove, qml_path)111 self.addCleanup(os.remove, qml_path)
111112
=== modified file 'autopilot/tests/functional/test_open_window.py'
--- autopilot/tests/functional/test_open_window.py 2013-07-14 09:17:37 +0000
+++ autopilot/tests/functional/test_open_window.py 2013-09-16 17:17:50 +0000
@@ -31,7 +31,7 @@
31class OpenWindowTests(AutopilotTestCase):31class OpenWindowTests(AutopilotTestCase):
3232
33 scenarios = [33 scenarios = [
34 (k, {'app_name': k}) for k in ProcessManager.KNOWN_APPS.iterkeys()]34 (k, {'app_name': k}) for k in ProcessManager.KNOWN_APPS.keys()]
3535
36 def test_open_window(self):36 def test_open_window(self):
37 """self.start_app_window must open a new window of the given app."""37 """self.start_app_window must open a new window of the given app."""
3838
=== modified file 'autopilot/tests/functional/test_process_emulator.py'
--- autopilot/tests/functional/test_process_emulator.py 2013-07-24 04:45:13 +0000
+++ autopilot/tests/functional/test_process_emulator.py 2013-09-16 17:17:50 +0000
@@ -96,7 +96,7 @@
96 try:96 try:
97 process_manager = ProcessManager.create(preferred_backend="BAMF")97 process_manager = ProcessManager.create(preferred_backend="BAMF")
98 except BackendException as e:98 except BackendException as e:
99 self.skip("Test is only for BAMF backend ({}).".format(e.message))99 self.skip("Test is only for BAMF backend ({}).".format(str(e)))
100100
101 process_manager.start_app('Calculator')101 process_manager.start_app('Calculator')
102102
103103
=== modified file 'autopilot/tests/unit/test_command_line_args.py'
--- autopilot/tests/unit/test_command_line_args.py 2013-07-22 03:40:32 +0000
+++ autopilot/tests/unit/test_command_line_args.py 2013-09-16 17:17:50 +0000
@@ -23,7 +23,14 @@
2323
2424
25from mock import patch25from mock import patch
26from StringIO import StringIO26
27try:
28 # Python 2
29 from StringIO import StringIO
30except ImportError:
31 # Python 3
32 from io import StringIO
33
27from testtools import TestCase34from testtools import TestCase
28from testtools.matchers import Equals35from testtools.matchers import Equals
29from unittest import expectedFailure36from unittest import expectedFailure
@@ -34,7 +41,7 @@
34class CommandLineArgsTests(TestCase):41class CommandLineArgsTests(TestCase):
3542
36 def parse_args(self, args):43 def parse_args(self, args):
37 if isinstance(args, basestring):44 if isinstance(args, str):
38 args = args.split()45 args = args.split()
39 try:46 try:
40 return parse_arguments(args)47 return parse_arguments(args)
4148
=== modified file 'autopilot/tests/unit/test_custom_exceptions.py'
--- autopilot/tests/unit/test_custom_exceptions.py 2013-07-22 03:39:30 +0000
+++ autopilot/tests/unit/test_custom_exceptions.py 2013-09-16 17:17:50 +0000
@@ -30,7 +30,7 @@
30 """BackendException must be able to wrap another exception instance."""30 """BackendException must be able to wrap another exception instance."""
31 err = BackendException(RuntimeError("Hello World"))31 err = BackendException(RuntimeError("Hello World"))
32 self.assertThat(err.original_exception, IsInstance(RuntimeError))32 self.assertThat(err.original_exception, IsInstance(RuntimeError))
33 self.assertThat(err.original_exception.message, Equals("Hello World"))33 self.assertThat(str(err.original_exception), Equals("Hello World"))
3434
35 def test_dunder_str(self):35 def test_dunder_str(self):
36 err = BackendException(RuntimeError("Hello World"))36 err = BackendException(RuntimeError("Hello World"))
3737
=== modified file 'autopilot/tests/unit/test_introspection_features.py'
--- autopilot/tests/unit/test_introspection_features.py 2013-08-08 20:33:14 +0000
+++ autopilot/tests/unit/test_introspection_features.py 2013-09-16 17:17:50 +0000
@@ -24,8 +24,9 @@
2424
2525
26from autopilot.introspection.dbus import (26from autopilot.introspection.dbus import (
27 _get_filter_string_for_key_value_pair,
28 _is_valid_server_side_filter_param,
27 CustomEmulatorBase,29 CustomEmulatorBase,
28 _is_valid_server_side_filter_param,
29)30)
3031
3132
@@ -69,10 +70,19 @@
69 scenarios = [70 scenarios = [
70 ('should work', dict(key='keyname', value='value', result=True)),71 ('should work', dict(key='keyname', value='value', result=True)),
71 ('invalid key', dict(key='k e', value='value', result=False)),72 ('invalid key', dict(key='k e', value='value', result=False)),
72 ('invalid value', dict(key='key', value='v e', result=False)),73 ('string value', dict(key='key', value='v e', result=True)),
73 ('invalid value2', dict(key='key', value='v?e', result=False)),74 ('string value2', dict(key='key', value='v?e', result=True)),
74 ('invalid value3', dict(key='key', value='1/2', result=False)),75 ('string value3', dict(key='key', value='1/2."!@#*&^%', result=True)),
75 ('invalid value type', dict(key='key', value=False, result=False)),76 ('bool value', dict(key='key', value=False, result=True)),
77 ('int value', dict(key='key', value=123, result=True)),
78 ('int value2', dict(key='key', value=-123, result=True)),
79 ('float value', dict(key='key', value=1.0, result=False)),
80 ('dict value', dict(key='key', value={}, result=False)),
81 ('obj value', dict(key='key', value=TestCase, result=False)),
82 ('int overflow 1', dict(key='key', value=-2147483648, result=True)),
83 ('int overflow 2', dict(key='key', value=-2147483649, result=False)),
84 ('int overflow 3', dict(key='key', value=2147483647, result=True)),
85 ('int overflow 4', dict(key='key', value=2147483648, result=False)),
76 ]86 ]
7787
78 def test_valid_server_side_param(self):88 def test_valid_server_side_param(self):
@@ -80,3 +90,24 @@
80 _is_valid_server_side_filter_param(self.key, self.value),90 _is_valid_server_side_filter_param(self.key, self.value),
81 Equals(self.result)91 Equals(self.result)
82 )92 )
93
94
95class ServerSideParameterFilterStringTests(TestWithScenarios, TestCase):
96
97 scenarios = [
98 ('bool true', dict(k='visible', v=True, r="visible=True")),
99 ('bool false', dict(k='visible', v=False, r="visible=False")),
100 ('int +ve', dict(k='size', v=123, r="size=123")),
101 ('int -ve', dict(k='prio', v=-12, r="prio=-12")),
102 ('simple string', dict(k='Name', v="btn1", r="Name=\"btn1\"")),
103 ('string space', dict(k='Name', v="a b c ", r="Name=\"a b c \"")),
104 ('str escapes', dict(
105 k='a',
106 v="\a\b\f\n\r\t\v\\",
107 r=r'a="\x07\x08\x0c\n\r\t\x0b\\"')),
108 ('escape quotes', dict(k='b', v="'", r='b="\\' + "'" + '"')),
109 ]
110
111 def test_query_string(self):
112 s = _get_filter_string_for_key_value_pair(self.k, self.v)
113 self.assertThat(s, Equals(self.r))
83114
=== modified file 'autopilot/tests/unit/test_matchers.py'
--- autopilot/tests/unit/test_matchers.py 2013-08-07 03:18:32 +0000
+++ autopilot/tests/unit/test_matchers.py 2013-09-16 17:17:50 +0000
@@ -20,9 +20,12 @@
2020
21from __future__ import absolute_import21from __future__ import absolute_import
2222
23import sys
24
23from autopilot.introspection.dbus import DBusIntrospectionObject25from autopilot.introspection.dbus import DBusIntrospectionObject
24from autopilot.matchers import Eventually26from autopilot.matchers import Eventually
2527
28from contextlib import contextmanager
26import dbus29import dbus
27from testscenarios import TestWithScenarios30from testscenarios import TestWithScenarios
28from testtools import TestCase31from testtools import TestCase
@@ -30,7 +33,6 @@
30 Contains,33 Contains,
31 Equals,34 Equals,
32 IsInstance,35 IsInstance,
33 LessThan,
34 MatchesException,36 MatchesException,
35 Mismatch,37 Mismatch,
36 Raises,38 Raises,
@@ -38,6 +40,23 @@
38from time import time40from time import time
3941
4042
43if sys.version >= '3':
44 unicode = str
45
46
47@contextmanager
48def expected_runtime(tmin, tmax):
49 start = time()
50 try:
51 yield
52 finally:
53 elapsed_time = abs(time() - start)
54 if not tmin < elapsed_time < tmax:
55 raise AssertionError(
56 "Runtime of %f is not between %f and %f"
57 % (elapsed_time, tmin, tmax))
58
59
41def make_fake_attribute_with_result(result, attribute_type='wait_for'):60def make_fake_attribute_with_result(result, attribute_type='wait_for'):
42 """Make a fake attribute with the given result.61 """Make a fake attribute with the given result.
4362
@@ -58,13 +77,15 @@
58 return lambda: result77 return lambda: result
59 elif attribute_type == 'wait_for':78 elif attribute_type == 'wait_for':
60 if isinstance(result, unicode):79 if isinstance(result, unicode):
61 obj = FakeObject(dict(id=123, attr=dbus.String(result)))80 obj = FakeObject(dict(id=[0, 123], attr=[0, dbus.String(result)]))
62 return obj.attr81 return obj.attr
63 elif isinstance(result, str):82 elif isinstance(result, bytes):
64 obj = FakeObject(dict(id=123, attr=dbus.UTF8String(result)))83 obj = FakeObject(
84 dict(id=[0, 123], attr=[0, dbus.UTF8String(result)])
85 )
65 return obj.attr86 return obj.attr
66 else:87 else:
67 obj = FakeObject(dict(id=123, attr=dbus.Boolean(result)))88 obj = FakeObject(dict(id=[0, 123], attr=[0, dbus.Boolean(result)]))
68 return obj.attr89 return obj.attr
6990
7091
@@ -84,37 +105,31 @@
84 ('wait_for', dict(attribute_type='wait_for')),105 ('wait_for', dict(attribute_type='wait_for')),
85 ]106 ]
86107
87 def test_eventually_matcher_returns_Mismatch(self):108 def test_eventually_matcher_returns_mismatch(self):
88 """Eventually matcher must return a Mismatch."""109 """Eventually matcher must return a Mismatch."""
89 attr = make_fake_attribute_with_result(False, self.attribute_type)110 attr = make_fake_attribute_with_result(False, self.attribute_type)
90 e = Eventually(Equals(True)).match(lambda: attr)111 e = Eventually(Equals(True)).match(attr)
91112
92 self.assertThat(e, IsInstance(Mismatch))113 self.assertThat(e, IsInstance(Mismatch))
93114
94 def test_eventually_default_timeout(self):115 def test_eventually_default_timeout(self):
95 """Eventually matcher must default to 10 second timeout."""116 """Eventually matcher must default to 10 second timeout."""
96 attr = make_fake_attribute_with_result(False, self.attribute_type)117 attr = make_fake_attribute_with_result(False, self.attribute_type)
97 start = time()118 with expected_runtime(9.5, 11.0):
98 Eventually(Equals(True)).match(attr)119 Eventually(Equals(True)).match(attr)
99 # max error of 1 second seems reasonable:
100 self.assertThat(abs(time() - start - 10.0), LessThan(1))
101120
102 def test_eventually_passes_immeadiately(self):121 def test_eventually_passes_immeadiately(self):
103 """Eventually matcher must not wait if the assertion passes122 """Eventually matcher must not wait if the assertion passes
104 initially."""123 initially."""
105 start = time()
106 attr = make_fake_attribute_with_result(True, self.attribute_type)124 attr = make_fake_attribute_with_result(True, self.attribute_type)
107 Eventually(Equals(True)).match(attr)125 with expected_runtime(0.0, 1.0):
108 # max error of 1 second seems reasonable:126 Eventually(Equals(True)).match(attr)
109 self.assertThat(abs(time() - start), LessThan(1))
110127
111 def test_eventually_matcher_allows_non_default_timeout(self):128 def test_eventually_matcher_allows_non_default_timeout(self):
112 """Eventually matcher must allow a non-default timeout value."""129 """Eventually matcher must allow a non-default timeout value."""
113 start = time()
114 attr = make_fake_attribute_with_result(False, self.attribute_type)130 attr = make_fake_attribute_with_result(False, self.attribute_type)
115 Eventually(Equals(True), timeout=5).match(attr)131 with expected_runtime(4.5, 6.0):
116 # max error of 1 second seems reasonable:132 Eventually(Equals(True), timeout=5).match(attr)
117 self.assertThat(abs(time() - start - 5.0), LessThan(1))
118133
119 def test_mismatch_message_has_correct_timeout_value(self):134 def test_mismatch_message_has_correct_timeout_value(self):
120 """The mismatch value must have the correct timeout value in it."""135 """The mismatch value must have the correct timeout value in it."""
@@ -134,20 +149,16 @@
134149
135 def test_match_with_expected_value_unicode(self):150 def test_match_with_expected_value_unicode(self):
136 """The expected unicode value matches new value string."""151 """The expected unicode value matches new value string."""
137 start = time()
138 attr = make_fake_attribute_with_result(152 attr = make_fake_attribute_with_result(
139 unicode(u'\u963f\u5e03\u4ece'), 'wait_for')153 u'\u963f\u5e03\u4ece', 'wait_for')
140 Eventually(Equals(str("阿布从"))).match(attr)154 with expected_runtime(0.0, 1.0):
141 #this should not take more than 1 second155 Eventually(Equals("阿布从")).match(attr)
142 self.assertThat(abs(time() - start), LessThan(1))
143156
144 def test_match_with_new_value_unicode(self):157 def test_match_with_new_value_unicode(self):
145 """new value with unicode must match expected value string."""158 """new value with unicode must match expected value string."""
146 start = time()
147 attr = make_fake_attribute_with_result(str("阿布从"), 'wait_for')159 attr = make_fake_attribute_with_result(str("阿布从"), 'wait_for')
148 Eventually(Equals(unicode(u'\u963f\u5e03\u4ece'))).match(attr)160 with expected_runtime(0.0, 1.0):
149 # this should not take more than 1 second161 Eventually(Equals(u'\u963f\u5e03\u4ece')).match(attr)
150 self.assertThat(abs(time() - start), LessThan(1))
151162
152 def test_mismatch_with_bool(self):163 def test_mismatch_with_bool(self):
153 """The mismatch value must fail boolean values."""164 """The mismatch value must fail boolean values."""
@@ -160,7 +171,7 @@
160 """The mismatch value must fail with str and unicode mix."""171 """The mismatch value must fail with str and unicode mix."""
161 attr = make_fake_attribute_with_result(str("阿布从1"), 'wait_for')172 attr = make_fake_attribute_with_result(str("阿布从1"), 'wait_for')
162 mismatch = Eventually(Equals(173 mismatch = Eventually(Equals(
163 unicode(u'\u963f\u5e03\u4ece')), timeout=.5).match(attr)174 u'\u963f\u5e03\u4ece'), timeout=.5).match(attr)
164 self.assertThat(175 self.assertThat(
165 mismatch.describe(), Contains('failed'))176 mismatch.describe(), Contains('failed'))
166177
@@ -169,6 +180,6 @@
169 self.skip("mismatch Contains returns ascii error")180 self.skip("mismatch Contains returns ascii error")
170 attr = make_fake_attribute_with_result(str("阿布从1"), 'wait_for')181 attr = make_fake_attribute_with_result(str("阿布从1"), 'wait_for')
171 mismatch = Eventually(Equals(182 mismatch = Eventually(Equals(
172 unicode(u'\u963f\u5e03\u4ece')), timeout=.5).match(attr)183 u'\u963f\u5e03\u4ece'), timeout=.5).match(attr)
173 self.assertThat(184 self.assertThat(
174 mismatch.describe(), Contains("阿布从11"))185 mismatch.describe(), Contains("阿布从11"))
175186
=== modified file 'autopilot/tests/unit/test_pick_backend.py'
--- autopilot/tests/unit/test_pick_backend.py 2013-07-22 03:30:31 +0000
+++ autopilot/tests/unit/test_pick_backend.py 2013-09-16 17:17:50 +0000
@@ -107,7 +107,7 @@
107 raised = True107 raised = True
108 self.assertTrue(hasattr(e, 'original_exception'))108 self.assertTrue(hasattr(e, 'original_exception'))
109 self.assertThat(e.original_exception, IsInstance(ValueError))109 self.assertThat(e.original_exception, IsInstance(ValueError))
110 self.assertThat(e.original_exception.message, Equals("Foo"))110 self.assertThat(str(e.original_exception), Equals("Foo"))
111 self.assertTrue(raised)111 self.assertTrue(raised)
112112
113 def test_failure_of_all_backends(self):113 def test_failure_of_all_backends(self):
114114
=== modified file 'autopilot/tests/unit/test_platform.py'
--- autopilot/tests/unit/test_platform.py 2013-07-24 00:07:54 +0000
+++ autopilot/tests/unit/test_platform.py 2013-09-16 17:17:50 +0000
@@ -21,7 +21,13 @@
2121
22import autopilot.platform as platform22import autopilot.platform as platform
2323
24from StringIO import StringIO24try:
25 # Python 2
26 from StringIO import StringIO
27except ImportError:
28 # Python 3
29 from io import StringIO
30
25from testtools import TestCase31from testtools import TestCase
26from testtools.matchers import Equals32from testtools.matchers import Equals
2733
2834
=== added file 'autopilot/tests/unit/test_types.py'
--- autopilot/tests/unit/test_types.py 1970-01-01 00:00:00 +0000
+++ autopilot/tests/unit/test_types.py 2013-09-16 17:17:50 +0000
@@ -0,0 +1,579 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#
3# Autopilot Functional Test Tool
4# Copyright (C) 2013 Canonical
5#
6# This program is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# This program is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with this program. If not, see <http://www.gnu.org/licenses/>.
18#
19
20from __future__ import absolute_import
21
22from datetime import datetime, time
23from mock import patch
24from testscenarios import TestWithScenarios
25from testtools import TestCase
26from testtools.matchers import Equals, IsInstance, NotEquals, raises
27import dbus
28
29from autopilot.introspection.types import (
30 Color,
31 create_value_instance,
32 DateTime,
33 PlainType,
34 Point,
35 Rectangle,
36 Size,
37 Time,
38 ValueType,
39)
40from autopilot.introspection.dbus import DBusIntrospectionObject
41
42
43class FakeObject(object):
44
45 def __init__(self):
46 self.get_new_state_called = False
47 self.set_properties_called = False
48
49 def get_new_state(self):
50 self.get_new_state_called = True
51
52 def _set_properties(self, state):
53 self.set_properties_called = True
54
55
56class PlainTypeTests(TestWithScenarios, TestCase):
57
58 scenarios = [
59 ('bool true', dict(t=dbus.Boolean, v=True)),
60 ('bool false', dict(t=dbus.Boolean, v=False)),
61 ('int16 +ve', dict(t=dbus.Int16, v=123)),
62 ('int16 -ve', dict(t=dbus.Int16, v=-23000)),
63 ('int32 +ve', dict(t=dbus.Int32, v=30000000)),
64 ('int32 -ve', dict(t=dbus.Int32, v=-3002050)),
65 ('int64 +ve', dict(t=dbus.Int64, v=234234002050)),
66 ('int64 -ve', dict(t=dbus.Int64, v=-234234002050)),
67 ('string', dict(t=dbus.String, v="Hello World")),
68 ]
69
70 def test_can_construct(self):
71 p = PlainType(self.t(self.v))
72
73 self.assertThat(p, Equals(self.v))
74 self.assertThat(hasattr(p, 'wait_for'), Equals(True))
75 self.assertThat(p, IsInstance(self.t))
76
77
78class RectangleTypeTests(TestCase):
79
80 def test_can_construct_rectangle(self):
81 r = Rectangle(1, 2, 3, 4)
82 self.assertThat(r, IsInstance(dbus.Array))
83
84 def test_rectangle_has_xywh_properties(self):
85 r = Rectangle(1, 2, 3, 4)
86
87 self.assertThat(r.x, Equals(1))
88 self.assertThat(r.y, Equals(2))
89 self.assertThat(r.w, Equals(3))
90 self.assertThat(r.width, Equals(3))
91 self.assertThat(r.h, Equals(4))
92 self.assertThat(r.height, Equals(4))
93
94 def test_rectangle_has_slice_access(self):
95 r = Rectangle(1, 2, 3, 4)
96
97 self.assertThat(r[0], Equals(1))
98 self.assertThat(r[1], Equals(2))
99 self.assertThat(r[2], Equals(3))
100 self.assertThat(r[3], Equals(4))
101
102 def test_equality_with_rectangle(self):
103 r1 = Rectangle(1, 2, 3, 4)
104 r2 = Rectangle(1, 2, 3, 4)
105
106 self.assertThat(r1, Equals(r2))
107
108 def test_equality_with_list(self):
109 r1 = Rectangle(1, 2, 3, 4)
110 r2 = [1, 2, 3, 4]
111
112 self.assertThat(r1, Equals(r2))
113
114
115class PointTypeTests(TestCase):
116
117 def test_can_construct_point(self):
118 r = Point(1, 2)
119 self.assertThat(r, IsInstance(dbus.Array))
120
121 def test_point_has_xy_properties(self):
122 r = Point(1, 2)
123
124 self.assertThat(r.x, Equals(1))
125 self.assertThat(r.y, Equals(2))
126
127 def test_point_has_slice_access(self):
128 r = Point(1, 2)
129
130 self.assertThat(r[0], Equals(1))
131 self.assertThat(r[1], Equals(2))
132
133 def test_equality_with_point(self):
134 p1 = Point(1, 2)
135 p2 = Point(1, 2)
136
137 self.assertThat(p1, Equals(p2))
138
139 def test_equality_with_list(self):
140 p1 = Point(1, 2)
141 p2 = [1, 2]
142
143 self.assertThat(p1, Equals(p2))
144
145
146class SizeTypeTests(TestCase):
147
148 def test_can_construct_size(self):
149 r = Size(1, 2)
150 self.assertThat(r, IsInstance(dbus.Array))
151
152 def test_size_has_wh_properties(self):
153 r = Size(1, 2)
154
155 self.assertThat(r.w, Equals(1))
156 self.assertThat(r.width, Equals(1))
157 self.assertThat(r.h, Equals(2))
158 self.assertThat(r.height, Equals(2))
159
160 def test_size_has_slice_access(self):
161 r = Size(1, 2)
162
163 self.assertThat(r[0], Equals(1))
164 self.assertThat(r[1], Equals(2))
165
166 def test_equality_with_size(self):
167 s1 = Size(50, 100)
168 s2 = Size(50, 100)
169
170 self.assertThat(s1, Equals(s2))
171
172 def test_equality_with_list(self):
173 s1 = Size(50, 100)
174 s2 = [50, 100]
175
176 self.assertThat(s1, Equals(s2))
177
178
179class ColorTypeTests(TestCase):
180
181 def test_can_construct_color(self):
182 r = Color(123, 234, 55, 255)
183 self.assertThat(r, IsInstance(dbus.Array))
184
185 def test_color_has_rgba_properties(self):
186 r = Color(123, 234, 55, 255)
187
188 self.assertThat(r.red, Equals(123))
189 self.assertThat(r.green, Equals(234))
190 self.assertThat(r.blue, Equals(55))
191 self.assertThat(r.alpha, Equals(255))
192
193 def test_color_has_slice_access(self):
194 r = Color(123, 234, 55, 255)
195
196 self.assertThat(r[0], Equals(123))
197 self.assertThat(r[1], Equals(234))
198 self.assertThat(r[2], Equals(55))
199 self.assertThat(r[3], Equals(255))
200
201 def test_eqiality_with_color(self):
202 c1 = Color(123, 234, 55, 255)
203 c2 = Color(123, 234, 55, 255)
204
205 self.assertThat(c1, Equals(c2))
206
207 def test_eqiality_with_list(self):
208 c1 = Color(123, 234, 55, 255)
209 c2 = [123, 234, 55, 255]
210
211 self.assertThat(c1, Equals(c2))
212
213
214class DateTimeTests(TestCase):
215
216 def test_can_construct_datetime(self):
217 dt = DateTime(1377209927)
218 self.assertThat(dt, IsInstance(dbus.Array))
219
220 def test_datetime_has_slice_access(self):
221 dt = DateTime(1377209927)
222
223 self.assertThat(dt[0], Equals(1377209927))
224
225 def test_datetime_has_properties(self):
226 dt = DateTime(1377209927)
227
228 self.assertThat(dt.timestamp, Equals(1377209927))
229 self.assertThat(dt.year, Equals(2013))
230 self.assertThat(dt.month, Equals(8))
231 self.assertThat(dt.day, Equals(22))
232 self.assertThat(dt.hour, Equals(22))
233 self.assertThat(dt.minute, Equals(18))
234 self.assertThat(dt.second, Equals(47))
235
236 def test_equality_with_datetime(self):
237 dt1 = DateTime(1377209927)
238 dt2 = DateTime(1377209927)
239
240 self.assertThat(dt1, Equals(dt2))
241
242 def test_equality_with_list(self):
243 dt1 = DateTime(1377209927)
244 dt2 = [1377209927]
245
246 self.assertThat(dt1, Equals(dt2))
247
248 def test_equality_with_datetime(self):
249 dt1 = DateTime(1377209927)
250 dt2 = datetime.utcfromtimestamp(1377209927)
251 dt3 = datetime.utcfromtimestamp(1377209928)
252
253 self.assertThat(dt1, Equals(dt2))
254 self.assertThat(dt1, NotEquals(dt3))
255
256 def test_can_convert_to_datetime(self):
257 dt1 = DateTime(1377209927)
258
259 self.assertThat(dt1.datetime, IsInstance(datetime))
260
261
262class TimeTests(TestCase):
263
264 def test_can_construct_time(self):
265 dt = Time(0, 0, 0, 0)
266 self.assertThat(dt, IsInstance(dbus.Array))
267
268 def test_time_has_slice_access(self):
269 dt = Time(0, 1, 2, 3)
270
271 self.assertThat(dt[0], Equals(0))
272 self.assertThat(dt[1], Equals(1))
273 self.assertThat(dt[2], Equals(2))
274 self.assertThat(dt[3], Equals(3))
275
276 def test_time_has_properties(self):
277 dt = Time(0, 1, 2, 3)
278
279 self.assertThat(dt.hour, Equals(0))
280 self.assertThat(dt.minute, Equals(1))
281 self.assertThat(dt.second, Equals(2))
282 self.assertThat(dt.millisecond, Equals(3))
283
284 def test_equality_with_time(self):
285 dt1 = Time(0, 1, 2, 3)
286 dt2 = Time(0, 1, 2, 3)
287 dt3 = Time(4, 1, 2, 3)
288
289 self.assertThat(dt1, Equals(dt2))
290 self.assertThat(dt1, NotEquals(dt3))
291
292 def test_equality_with_time(self):
293 dt1 = Time(0, 1, 2, 3)
294 dt2 = [0, 1, 2, 3]
295 dt3 = [4, 1, 2, 3]
296
297 self.assertThat(dt1, Equals(dt2))
298 self.assertThat(dt1, NotEquals(dt3))
299
300 def test_equality_with_time(self):
301 dt1 = Time(2, 3, 4, 5)
302 dt2 = time(2, 3, 4, 5000)
303 dt3 = time(5, 4, 3, 2000)
304
305 self.assertThat(dt1, Equals(dt2))
306 self.assertThat(dt1, NotEquals(dt3))
307
308 def test_can_convert_to_time(self):
309 dt1 = Time(1, 2, 3, 4)
310
311 self.assertThat(dt1.time, IsInstance(time))
312
313
314class CreateValueInstanceTests(TestCase):
315
316 """Tests to check that create_value_instance does the right thing."""
317
318 def test_plain_string(self):
319 data = dbus.Array([dbus.Int32(ValueType.PLAIN), dbus.String("Hello")])
320 attr = create_value_instance(data, None, None)
321
322 self.assertThat(attr, Equals("Hello"))
323 self.assertThat(attr, IsInstance(PlainType))
324
325 def test_plain_boolean(self):
326 data = dbus.Array([dbus.Int32(ValueType.PLAIN), dbus.Boolean(False)])
327 attr = create_value_instance(data, None, None)
328
329 self.assertThat(attr, Equals(False))
330 self.assertThat(attr, IsInstance(PlainType))
331
332 def test_plain_int16(self):
333 data = dbus.Array([dbus.Int32(ValueType.PLAIN), dbus.Int16(-2**14)])
334 attr = create_value_instance(data, None, None)
335
336 self.assertThat(attr, Equals(-2**14))
337 self.assertThat(attr, IsInstance(PlainType))
338
339 def test_plain_int32(self):
340 data = dbus.Array([dbus.Int32(ValueType.PLAIN), dbus.Int32(-2**30)])
341 attr = create_value_instance(data, None, None)
342
343 self.assertThat(attr, Equals(-2**30))
344 self.assertThat(attr, IsInstance(PlainType))
345
346 def test_plain_int64(self):
347 data = dbus.Array([dbus.Int32(ValueType.PLAIN), dbus.Int64(-2**40)])
348 attr = create_value_instance(data, None, None)
349
350 self.assertThat(attr, Equals(-2**40))
351 self.assertThat(attr, IsInstance(PlainType))
352
353 def test_plain_uint16(self):
354 data = dbus.Array([dbus.Int32(ValueType.PLAIN), dbus.UInt16(2**14)])
355 attr = create_value_instance(data, None, None)
356
357 self.assertThat(attr, Equals(2**14))
358 self.assertThat(attr, IsInstance(PlainType))
359
360 def test_plain_uint32(self):
361 data = dbus.Array([dbus.Int32(ValueType.PLAIN), dbus.UInt32(2**30)])
362 attr = create_value_instance(data, None, None)
363
364 self.assertThat(attr, Equals(2**30))
365 self.assertThat(attr, IsInstance(PlainType))
366
367 def test_plain_uint64(self):
368 data = dbus.Array([dbus.Int32(ValueType.PLAIN), dbus.UInt64(2**40)])
369 attr = create_value_instance(data, None, None)
370
371 self.assertThat(attr, Equals(2**40))
372 self.assertThat(attr, IsInstance(PlainType))
373
374 def test_plain_array(self):
375 data = dbus.Array([
376 dbus.Int32(ValueType.PLAIN),
377 dbus.Array([
378 dbus.String("Hello"),
379 dbus.String("World")
380 ])
381 ])
382 attr = create_value_instance(data, None, None)
383 self.assertThat(attr, Equals(["Hello", "World"]))
384 self.assertThat(attr, IsInstance(PlainType))
385
386 def test_rectangle(self):
387 data = dbus.Array(
388 [
389 dbus.Int32(ValueType.RECTANGLE),
390 dbus.Int32(0),
391 dbus.Int32(10),
392 dbus.Int32(20),
393 dbus.Int32(30),
394 ]
395 )
396
397 attr = create_value_instance(data, None, None)
398
399 self.assertThat(attr, IsInstance(Rectangle))
400
401 def test_invalid_rectanlge(self):
402 data = dbus.Array(
403 [
404 dbus.Int32(ValueType.RECTANGLE),
405 dbus.Int32(0),
406 ]
407 )
408
409 fn = lambda: create_value_instance(data, None, None)
410
411 self.assertThat(fn, raises(
412 ValueError("Rectangle must be constructed with 4 arguments, not 1")
413 ))
414
415 def test_point(self):
416 data = dbus.Array(
417 [
418 dbus.Int32(ValueType.POINT),
419 dbus.Int32(0),
420 dbus.Int32(10),
421 ]
422 )
423
424 attr = create_value_instance(data, None, None)
425
426 self.assertThat(attr, IsInstance(Point))
427
428 def test_invalid_point(self):
429 data = dbus.Array(
430 [
431 dbus.Int32(ValueType.POINT),
432 dbus.Int32(0),
433 dbus.Int32(0),
434 dbus.Int32(0),
435 ]
436 )
437
438 fn = lambda: create_value_instance(data, None, None)
439
440 self.assertThat(fn, raises(
441 ValueError("Point must be constructed with 2 arguments, not 3")
442 ))
443
444 def test_size(self):
445 data = dbus.Array(
446 [
447 dbus.Int32(ValueType.SIZE),
448 dbus.Int32(0),
449 dbus.Int32(10),
450 ]
451 )
452
453 attr = create_value_instance(data, None, None)
454
455 self.assertThat(attr, IsInstance(Size))
456
457 def test_invalid_size(self):
458 data = dbus.Array(
459 [
460 dbus.Int32(ValueType.SIZE),
461 dbus.Int32(0),
462 ]
463 )
464
465 fn = lambda: create_value_instance(data, None, None)
466
467 self.assertThat(fn, raises(
468 ValueError("Size must be constructed with 2 arguments, not 1")
469 ))
470
471 def test_date_time(self):
472 data = dbus.Array(
473 [
474 dbus.Int32(ValueType.DATETIME),
475 dbus.Int32(0),
476 ]
477 )
478
479 attr = create_value_instance(data, None, None)
480
481 self.assertThat(attr, IsInstance(DateTime))
482
483 def test_invalid_date_time(self):
484 data = dbus.Array(
485 [
486 dbus.Int32(ValueType.DATETIME),
487 dbus.Int32(0),
488 dbus.Int32(0),
489 dbus.Int32(0),
490 ]
491 )
492
493 fn = lambda: create_value_instance(data, None, None)
494
495 self.assertThat(fn, raises(
496 ValueError("DateTime must be constructed with 1 arguments, not 3")
497 ))
498
499 def test_time(self):
500 data = dbus.Array(
501 [
502 dbus.Int32(ValueType.TIME),
503 dbus.Int32(0),
504 dbus.Int32(0),
505 dbus.Int32(0),
506 dbus.Int32(0),
507 ]
508 )
509
510 attr = create_value_instance(data, None, None)
511
512 self.assertThat(attr, IsInstance(Time))
513
514 def test_invalid_time(self):
515 data = dbus.Array(
516 [
517 dbus.Int32(ValueType.TIME),
518 dbus.Int32(0),
519 dbus.Int32(0),
520 dbus.Int32(0),
521 ]
522 )
523
524 fn = lambda: create_value_instance(data, None, None)
525
526 self.assertThat(fn, raises(
527 ValueError("Time must be constructed with 4 arguments, not 3")
528 ))
529
530 def test_unknown_type_id(self):
531 """Unknown type Ids should result in a plain type, along with a log
532 message.
533
534 """
535
536 data = dbus.Array(
537 [
538 dbus.Int32(543),
539 dbus.Int32(0),
540 dbus.Boolean(False),
541 dbus.String("Hello World")
542 ]
543 )
544 attr = create_value_instance(data, None, None)
545 self.assertThat(attr, IsInstance(PlainType))
546 self.assertThat(attr, IsInstance(dbus.Array))
547 self.assertThat(attr, Equals([0, False, "Hello World"]))
548
549 def test_invalid_no_data(self):
550 data = dbus.Array(
551 [
552 dbus.Int32(0),
553 ]
554 )
555 fn = lambda: create_value_instance(data, None, None)
556
557 self.assertThat(fn, raises(
558 ValueError("Cannot create attribute, no data supplied")
559 ))
560
561
562class DBusIntrospectionObjectTests(TestCase):
563
564 @patch('autopilot.introspection.dbus.logger.warning')
565 def test_dbus_introspection_object_logs_bad_data(self, error_logger):
566 """The DBusIntrospectionObject class must log an error when it gets
567 bad data from the autopilot backend.
568
569 """
570 my_obj = DBusIntrospectionObject(
571 dict(foo=[0]),
572 '/some/dummy/path'
573 )
574 error_logger.assert_called_once_with(
575 "While constructing attribute '%s.%s': %s",
576 "DBusIntrospectionObject",
577 "foo",
578 "Cannot create attribute, no data supplied"
579 )
0580
=== modified file 'autopilot/tests/unit/test_version_utility_fns.py'
--- autopilot/tests/unit/test_version_utility_fns.py 2013-07-22 03:14:42 +0000
+++ autopilot/tests/unit/test_version_utility_fns.py 2013-09-16 17:17:50 +0000
@@ -43,7 +43,7 @@
43 """The _get_package_installed_version function must return None when43 """The _get_package_installed_version function must return None when
44 subprocess raises an error while calling dpkg-query.44 subprocess raises an error while calling dpkg-query.
45 """45 """
46 def raise_error(*args):46 def raise_error(*args, **kwargs):
47 raise subprocess.CalledProcessError(1, "dpkg-query")47 raise subprocess.CalledProcessError(1, "dpkg-query")
48 with patch('subprocess.check_output', new=raise_error):48 with patch('subprocess.check_output', new=raise_error):
49 self.assertThat(_get_package_installed_version(), Equals(None))49 self.assertThat(_get_package_installed_version(), Equals(None))
@@ -54,7 +54,7 @@
5454
55 """55 """
56 with patch('subprocess.check_output',56 with patch('subprocess.check_output',
57 new=lambda *a: "1.3daily13.05.22\n"):57 new=lambda *a, **kwargs: "1.3daily13.05.22\n"):
58 self.assertThat(58 self.assertThat(
59 _get_package_installed_version(), Equals("1.3daily13.05.22"))59 _get_package_installed_version(), Equals("1.3daily13.05.22"))
6060
6161
=== modified file 'autopilot/utilities.py'
--- autopilot/utilities.py 2013-09-04 04:53:40 +0000
+++ autopilot/utilities.py 2013-09-16 17:17:50 +0000
@@ -34,13 +34,14 @@
3434
35def _pick_backend(backends, preferred_backend):35def _pick_backend(backends, preferred_backend):
36 """Pick a backend and return an instance of it."""36 """Pick a backend and return an instance of it."""
37 possible_backends = backends.keys()37 possible_backends = list(backends.keys())
38 get_debug_logger().debug(38 get_debug_logger().debug(
39 "Possible backends: %s", ','.join(possible_backends))39 "Possible backends: %s", ','.join(possible_backends))
40 if preferred_backend:40 if preferred_backend:
41 if preferred_backend in possible_backends:41 if preferred_backend in possible_backends:
42 possible_backends.sort(42 # make preferred_backend the first list item
43 lambda a, b: -1 if a == preferred_backend else 0)43 possible_backends.remove(preferred_backend)
44 possible_backends.insert(0, preferred_backend)
44 else:45 else:
45 raise RuntimeError("Unknown backend '%s'" % (preferred_backend))46 raise RuntimeError("Unknown backend '%s'" % (preferred_backend))
46 failure_reasons = []47 failure_reasons = []
@@ -67,7 +68,7 @@
67 ...68 ...
6869
69 """70 """
70 def __init__(self, stdout=os.devnull, stderr=os.devnull, mode='w'):71 def __init__(self, stdout=os.devnull, stderr=os.devnull, mode='wb'):
71 self.outfiles = stdout, stderr72 self.outfiles = stdout, stderr
72 self.combine = (stdout == stderr)73 self.combine = (stdout == stderr)
73 self.mode = mode74 self.mode = mode
@@ -309,9 +310,7 @@
309 return class_object310 return class_object
310311
311312
312class CleanupRegistered(object):313CleanupRegistered = _TestCleanupMeta('CleanupRegistered', (object,), {})
313
314 __metaclass__ = _TestCleanupMeta
315314
316315
317def action_on_test_start(test_instance):316def action_on_test_start(test_instance):
318317
=== modified file 'autopilot/vis/bus_enumerator.py'
--- autopilot/vis/bus_enumerator.py 2013-07-14 08:17:17 +0000
+++ autopilot/vis/bus_enumerator.py 2013-09-16 17:17:50 +0000
@@ -79,7 +79,7 @@
7979
80 def get_found_connections(self):80 def get_found_connections(self):
81 """Get a list of found connection names. This may not be up to date."""81 """Get a list of found connection names. This may not be up to date."""
82 return self._data.keys()82 return list(self._data.keys())
8383
84 def get_found_objects(self, connection_string):84 def get_found_objects(self, connection_string):
85 """Get a list of found objects for a particular connection name.85 """Get a list of found objects for a particular connection name.
@@ -87,9 +87,9 @@
87 This may be out of date.87 This may be out of date.
8888
89 """89 """
90 if connection_string not in self._data.keys():90 if connection_string not in self._data:
91 raise KeyError("%s not in results" % connection_string)91 raise KeyError("%s not in results" % connection_string)
92 return self._data[connection_string].keys()92 return list(self._data[connection_string].keys())
9393
94 def get_found_interfaces(self, connection_string, object_path):94 def get_found_interfaces(self, connection_string, object_path):
95 """Get a list of found interfaces for a particular connection name and95 """Get a list of found interfaces for a particular connection name and
@@ -98,9 +98,9 @@
98 This may be out of date.98 This may be out of date.
9999
100 """100 """
101 if connection_string not in self._data.keys():101 if connection_string not in self._data:
102 raise KeyError("connection %s not in results" % connection_string)102 raise KeyError("connection %s not in results" % connection_string)
103 if object_path not in self._data[connection_string].keys():103 if object_path not in self._data[connection_string]:
104 raise KeyError(104 raise KeyError(
105 "object %s not in results for connection %s" %105 "object %s not in results for connection %s" %
106 (object_path, connection_string))106 (object_path, connection_string))
107107
=== modified file 'autopilot/vis/main_window.py'
--- autopilot/vis/main_window.py 2013-07-14 08:13:25 +0000
+++ autopilot/vis/main_window.py 2013-09-16 17:17:50 +0000
@@ -20,6 +20,7 @@
2020
21from __future__ import absolute_import21from __future__ import absolute_import
2222
23import sys
23import dbus24import dbus
24from PyQt4 import QtGui, QtCore25from PyQt4 import QtGui, QtCore
2526
@@ -33,6 +34,8 @@
33from autopilot.vis.objectproperties import TreeNodeDetailWidget34from autopilot.vis.objectproperties import TreeNodeDetailWidget
34from autopilot.vis.resources import get_qt_icon35from autopilot.vis.resources import get_qt_icon
3536
37_PY3 = sys.version >= '3'
38
3639
37class MainWindow(QtGui.QMainWindow):40class MainWindow(QtGui.QMainWindow):
38 def __init__(self, dbus_bus):41 def __init__(self, dbus_bus):
@@ -44,8 +47,12 @@
4447
45 def readSettings(self):48 def readSettings(self):
46 settings = QtCore.QSettings()49 settings = QtCore.QSettings()
47 self.restoreGeometry(settings.value("geometry").toByteArray())50 if _PY3:
48 self.restoreState(settings.value("windowState").toByteArray())51 self.restoreGeometry(settings.value("geometry").data())
52 self.restoreState(settings.value("windowState").data())
53 else:
54 self.restoreGeometry(settings.value("geometry").toByteArray())
55 self.restoreState(settings.value("windowState").toByteArray())
4956
50 def closeEvent(self, event):57 def closeEvent(self, event):
51 settings = QtCore.QSettings()58 settings = QtCore.QSettings()
@@ -90,17 +97,16 @@
90 def update_selectable_interfaces(self):97 def update_selectable_interfaces(self):
91 selected_text = self.connection_list.currentText()98 selected_text = self.connection_list.currentText()
92 self.connection_list.clear()99 self.connection_list.clear()
93 self.connection_list.addItem("Please select a connection",100 self.connection_list.addItem("Please select a connection", None)
94 QtCore.QVariant(None))101 for name, proxy_obj in self.selectable_interfaces.items():
95 for name, proxy_obj in self.selectable_interfaces.iteritems():
96 if isinstance(proxy_obj, QtObjectProxyMixin):102 if isinstance(proxy_obj, QtObjectProxyMixin):
97 self.connection_list.addItem(103 self.connection_list.addItem(
98 get_qt_icon(),104 get_qt_icon(),
99 name,105 name,
100 QtCore.QVariant(proxy_obj)106 proxy_obj
101 )107 )
102 else:108 else:
103 self.connection_list.addItem(name, QtCore.QVariant(proxy_obj))109 self.connection_list.addItem(name, proxy_obj)
104110
105 prev_selected = self.connection_list.findText(selected_text,111 prev_selected = self.connection_list.findText(selected_text,
106 QtCore.Qt.MatchExactly)112 QtCore.Qt.MatchExactly)
@@ -109,7 +115,9 @@
109 self.connection_list.setCurrentIndex(prev_selected)115 self.connection_list.setCurrentIndex(prev_selected)
110116
111 def conn_list_activated(self, index):117 def conn_list_activated(self, index):
112 dbus_details = self.connection_list.itemData(index).toPyObject()118 dbus_details = self.connection_list.itemData(index)
119 if not _PY3:
120 dbus_details = dbus_details.toPyObject()
113 if dbus_details:121 if dbus_details:
114 self.tree_model = VisTreeModel(dbus_details)122 self.tree_model = VisTreeModel(dbus_details)
115 self.tree_view.setModel(self.tree_model)123 self.tree_view.setModel(self.tree_model)
@@ -211,16 +219,16 @@
211219
212 def data(self, index, role):220 def data(self, index, role):
213 if not index.isValid():221 if not index.isValid():
214 return QtCore.QVariant()222 return None
215 if role == QtCore.Qt.DisplayRole:223 if role == QtCore.Qt.DisplayRole:
216 return QtCore.QVariant(index.internalPointer().name)224 return index.internalPointer().name
217225
218 def headerData(self, column, orientation, role):226 def headerData(self, column, orientation, role):
219 if (orientation == QtCore.Qt.Horizontal and227 if (orientation == QtCore.Qt.Horizontal and
220 role == QtCore.Qt.DisplayRole):228 role == QtCore.Qt.DisplayRole):
221 try:229 try:
222 return QtCore.QVariant("Tree Node")230 return "Tree Node"
223 except IndexError:231 except IndexError:
224 pass232 pass
225233
226 return QtCore.QVariant()234 return None
227235
=== modified file 'autopilot/vis/objectproperties.py'
--- autopilot/vis/objectproperties.py 2013-07-14 08:10:50 +0000
+++ autopilot/vis/objectproperties.py 2013-09-16 17:17:50 +0000
@@ -20,8 +20,11 @@
2020
21"""Code for introspection tree object properties."""21"""Code for introspection tree object properties."""
2222
23import sys
24
23from PyQt4 import QtGui, QtCore25from PyQt4 import QtGui, QtCore
2426
27
25from autopilot.vis.resources import get_qt_icon, dbus_string_rep28from autopilot.vis.resources import get_qt_icon, dbus_string_rep
26from autopilot.introspection.qt import QtObjectProxyMixin29from autopilot.introspection.qt import QtObjectProxyMixin
2730
@@ -94,7 +97,10 @@
94 def __init__(self, *args, **kwargs):97 def __init__(self, *args, **kwargs):
95 super(PropertyView, self).__init__(*args, **kwargs)98 super(PropertyView, self).__init__(*args, **kwargs)
9699
97 header_titles = QtCore.QStringList(["Name", "Value"])100 if sys.version >= '3':
101 header_titles = ["Name", "Value"]
102 else:
103 header_titles = QtCore.QStringList(["Name", "Value"])
98 self.details_layout = QtGui.QVBoxLayout(self)104 self.details_layout = QtGui.QVBoxLayout(self)
99105
100 self.table_view = QtGui.QTableWidget()106 self.table_view = QtGui.QTableWidget()
@@ -122,7 +128,7 @@
122 details_string = dbus_string_rep(object_details[key])128 details_string = dbus_string_rep(object_details[key])
123 item_name = QtGui.QTableWidgetItem(key)129 item_name = QtGui.QTableWidgetItem(key)
124 item_details = QtGui.QTableWidgetItem(130 item_details = QtGui.QTableWidgetItem(
125 details_string.decode('utf-8'))131 details_string)
126 self.table_view.setItem(i, 0, item_name)132 self.table_view.setItem(i, 0, item_name)
127 self.table_view.setItem(i, 1, item_details)133 self.table_view.setItem(i, 1, item_details)
128 self.table_view.setSortingEnabled(True)134 self.table_view.setSortingEnabled(True)
129135
=== modified file 'autopilot/vis/resources.py'
--- autopilot/vis/resources.py 2013-07-14 08:05:09 +0000
+++ autopilot/vis/resources.py 2013-09-16 17:17:50 +0000
@@ -32,7 +32,7 @@
32 if isinstance(dbus_type, dbus.Boolean):32 if isinstance(dbus_type, dbus.Boolean):
33 return repr(bool(dbus_type))33 return repr(bool(dbus_type))
34 if isinstance(dbus_type, dbus.String):34 if isinstance(dbus_type, dbus.String):
35 return dbus_type.encode('utf-8', errors='ignore')35 return dbus_type.encode('utf-8', errors='ignore').decode('utf-8')
36 if (isinstance(dbus_type, dbus.Int16) or36 if (isinstance(dbus_type, dbus.Int16) or
37 isinstance(dbus_type, dbus.UInt16) or37 isinstance(dbus_type, dbus.UInt16) or
38 isinstance(dbus_type, dbus.Int32) or38 isinstance(dbus_type, dbus.Int32) or
3939
=== modified file 'bin/autopilot'
--- bin/autopilot 2013-08-01 15:33:53 +0000
+++ bin/autopilot 2013-09-16 17:17:50 +0000
@@ -66,21 +66,21 @@
6666
67 suite_names = ["%s.%s" % (t.__module__, t.__class__.__name__)67 suite_names = ["%s.%s" % (t.__module__, t.__class__.__name__)
68 for t in test_list_fn()]68 for t in test_list_fn()]
69 unique_suite_names = OrderedDict.fromkeys(suite_names).keys()69 unique_suite_names = list(OrderedDict.fromkeys(suite_names).keys())
70 num_tests = len(unique_suite_names)70 num_tests = len(unique_suite_names)
71 total_title = "suites"71 total_title = "suites"
72 print " %s" % ("\n ".join(unique_suite_names))72 print(" %s" % ("\n ".join(unique_suite_names)))
73 else:73 else:
74 for test in test_list_fn():74 for test in test_list_fn():
75 has_scenarios = (hasattr(test, "scenarios")75 has_scenarios = (hasattr(test, "scenarios")
76 and type(test.scenarios) is list)76 and type(test.scenarios) is list)
77 if has_scenarios:77 if has_scenarios:
78 num_tests += len(test.scenarios)78 num_tests += len(test.scenarios)
79 print " *%d %s" % (len(test.scenarios), test.id())79 print(" *%d %s" % (len(test.scenarios), test.id()))
80 else:80 else:
81 num_tests += 181 num_tests += 1
82 print " ", test.id()82 print(" " + test.id())
83 print "\n\n %d total %s." % (num_tests, total_title)83 print("\n\n %d total %s." % (num_tests, total_title))
8484
8585
86def run_tests(args):86def run_tests(args):
@@ -89,7 +89,7 @@
8989
90 if args.random_order:90 if args.random_order:
91 shuffle(test_suite._tests)91 shuffle(test_suite._tests)
92 print "Running tests in random order"92 print("Running tests in random order")
9393
94 import autopilot.globals94 import autopilot.globals
9595
@@ -104,8 +104,8 @@
104 stdout=subprocess.PIPE104 stdout=subprocess.PIPE
105 )105 )
106 if call_ret_code != 0:106 if call_ret_code != 0:
107 print "ERROR: The application 'recordmydesktop' needs to be " \107 print("ERROR: The application 'recordmydesktop' needs to be "
108 "installed to record failing jobs."108 "installed to record failing jobs.")
109 exit(1)109 exit(1)
110 autopilot.globals.configure_video_recording(True,110 autopilot.globals.configure_video_recording(True,
111 args.record_directory,111 args.record_directory,
@@ -181,7 +181,7 @@
181 datetime.now().strftime("%d.%m.%y-%H%M%S")181 datetime.now().strftime("%d.%m.%y-%H%M%S")
182 )182 )
183 log_file = os.path.join(log_file, default_log)183 log_file = os.path.join(log_file, default_log)
184 print "Using default log filename: %s " % default_log184 print("Using default log filename: %s " % default_log)
185 if args.format == 'xml':185 if args.format == 'xml':
186 _output_stream = open(log_file, 'w')186 _output_stream = open(log_file, 'w')
187 else:187 else:
@@ -216,7 +216,7 @@
216def load_test_suite_from_name(test_names):216def load_test_suite_from_name(test_names):
217 """Returns a test suite object given a dotted test names."""217 """Returns a test suite object given a dotted test names."""
218 loader = TestLoader()218 loader = TestLoader()
219 if isinstance(test_names, basestring):219 if isinstance(test_names, str):
220 test_names = list(test_names)220 test_names = list(test_names)
221 elif not isinstance(test_names, list):221 elif not isinstance(test_names, list):
222 raise TypeError("test_names must be either a string or list, not %r"222 raise TypeError("test_names must be either a string or list, not %r"
@@ -241,7 +241,7 @@
241 all_tests = TestSuite(tests)241 all_tests = TestSuite(tests)
242242
243 test_dirs = ", ".join(sorted(test_package_locations))243 test_dirs = ", ".join(sorted(test_package_locations))
244 print "Loading tests from: %s\n" % test_dirs244 print("Loading tests from: %s\n" % test_dirs)
245 sys.stdout.flush()245 sys.stdout.flush()
246246
247 requested_tests = {}247 requested_tests = {}
@@ -256,7 +256,7 @@
256 try:256 try:
257 test.debug()257 test.debug()
258 except Exception as e:258 except Exception as e:
259 print e259 print(e)
260 else:260 else:
261 test_id = test.id()261 test_id = test.id()
262 if any([test_id.startswith(name) for name in test_names]):262 if any([test_id.startswith(name) for name in test_names]):
@@ -287,9 +287,10 @@
287 app_name = args.application[0]287 app_name = args.application[0]
288 if not isabs(app_name) or not exists(app_name):288 if not isabs(app_name) or not exists(app_name):
289 try:289 try:
290 app_name = subprocess.check_output(["which", app_name]).strip()290 app_name = subprocess.check_output(["which", app_name],
291 universal_newlines=True).strip()
291 except subprocess.CalledProcessError:292 except subprocess.CalledProcessError:
292 print "Error: cannot find application '%s'" % (app_name)293 print("Error: cannot find application '%s'" % (app_name))
293 exit(1)294 exit(1)
294295
295 # We now have a full path to the application.296 # We now have a full path to the application.
@@ -298,21 +299,21 @@
298 try:299 try:
299 launcher = get_application_launcher(app_name)300 launcher = get_application_launcher(app_name)
300 except RuntimeError as e:301 except RuntimeError as e:
301 print "Error detecting launcher: %s" % e.message302 print("Error detecting launcher: %s" % str(e))
302 print "(Perhaps use the '-i' argument to specify an interface.)"303 print("(Perhaps use the '-i' argument to specify an interface.)")
303 exit(1)304 exit(1)
304 else:305 else:
305 launcher = get_application_launcher_from_string_hint(args.interface)306 launcher = get_application_launcher_from_string_hint(args.interface)
306 if launcher is None:307 if launcher is None:
307 print "Error: Could not determine introspection type to use for " \308 print("Error: Could not determine introspection type to use for " \
308 "application '%s'." % app_name309 "application '%s'." % app_name)
309 print "(Perhaps use the '-i' argument to specify an interface.)"310 print("(Perhaps use the '-i' argument to specify an interface.)")
310 exit(1)311 exit(1)
311312
312 try:313 try:
313 launch_application(launcher, *args.application, capture_output=False)314 launch_application(launcher, *args.application, capture_output=False)
314 except RuntimeError as e:315 except RuntimeError as e:
315 print "Error: " + e.message316 print("Error: " + str(e))
316 exit(1)317 exit(1)
317318
318319
319320
=== modified file 'debian/changelog'
--- debian/changelog 2013-09-06 10:03:58 +0000
+++ debian/changelog 2013-09-16 17:17:50 +0000
@@ -1,3 +1,10 @@
1autopilot (1.4-0ubuntu1) saucy; urgency=low
2
3 [ Thomi Richards ]
4 * Update to new DBus wire protocol, bump autopilot version number.
5
6 -- Thomi Richards <thomi.richards@canonical.com> Fri, 16 Aug 2013 08:47:12 +1200
7
1autopilot (1.3.1+13.10.20130906.1-0ubuntu1) saucy; urgency=low8autopilot (1.3.1+13.10.20130906.1-0ubuntu1) saucy; urgency=low
29
3 [ Christopher Lee ]10 [ Christopher Lee ]
411
=== modified file 'debian/control'
--- debian/control 2013-09-09 21:34:21 +0000
+++ debian/control 2013-09-16 17:17:50 +0000
@@ -4,24 +4,37 @@
4Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>4Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
5XSBC-Original-Maintainer: Thomi Richards <thomi.richards@canonical.com>5XSBC-Original-Maintainer: Thomi Richards <thomi.richards@canonical.com>
6Build-Depends: debhelper (>= 9.0.0),6Build-Depends: debhelper (>= 9.0.0),
7 gir1.2-ibus-1.0,7 dh-python,
8 dvipng,
8 gir1.2-gconf-2.0,9 gir1.2-gconf-2.0,
9 gir1.2-gtk-3.0,10 gir1.2-gtk-3.0,
10 python (>= 2.6),11 gir1.2-ibus-1.0,
12 graphviz,
13 liblttng-ust-dev,
14 python-all-dev (>= 2.6),
15 python3-all-dev (>= 3.3),
11 python-dbus,16 python-dbus,
12 python-dev,
13 python-debian,17 python-debian,
18 python-gi,
14 python-mock,19 python-mock,
15 python-setuptools,20 python-setuptools,
16 python-sphinx,21 python-sphinx,
22 python-testscenarios,
17 python-testtools,23 python-testtools,
18 python-testscenarios,
19 python-xlib,24 python-xlib,
20 python-gi,25 python3-dbus,
21 liblttng-ust-dev,26 python3-gi,
27 python3-mock,
28 python3-setuptools,
29 python3-testscenarios,
30 python3-testtools,
31 python3-xlib,
32 texlive-latex-extra,
22Standards-Version: 3.9.433Standards-Version: 3.9.4
23Homepage: https://launchpad.net/autopilot34Homepage: https://launchpad.net/autopilot
24Vcs-bzr: https://code.launchpad.net/+branch/ubuntu/autopilot35Vcs-bzr: https://code.launchpad.net/+branch/ubuntu/autopilot
36X-Python-Version: >= 2.7
37X-Python3-Version: >= 3.3
2538
26Package: python-autopilot39Package: python-autopilot
27Architecture: all40Architecture: all
@@ -33,8 +46,8 @@
33 python-testtools,46 python-testtools,
34 udev,47 udev,
35Recommends: autopilot-desktop | autopilot-touch,48Recommends: autopilot-desktop | autopilot-touch,
36 libautopilot-gtk (>= 1.3),49 libautopilot-gtk (>= 1.4),
37 libautopilot-qt (>= 1.3),50 libautopilot-qt (>= 1.4),
38 python-autopilot-trace,51 python-autopilot-trace,
39 recordmydesktop,52 recordmydesktop,
40Description: Utility to write and run integration tests easily53Description: Utility to write and run integration tests easily
@@ -43,6 +56,31 @@
43 keyboard. It also provides a lot of utilities linked to the X server56 keyboard. It also provides a lot of utilities linked to the X server
44 and detecting applications.57 and detecting applications.
4558
59Package: python3-autopilot
60Architecture: all
61Depends: ${misc:Depends},
62 ${python3:Depends},
63 python3-dbus,
64 python3-junitxml,
65 python3-testscenarios,
66 python3-testtools,
67 udev,
68Recommends: python3-xlib,
69 python3-evdev,
70 gir1.2-gconf-2.0,
71 gir1.2-glib-2.0,
72 gir1.2-gtk-3.0,
73 gir1.2-ibus-1.0,
74 libautopilot-gtk (>= 1.4),
75 libautopilot-qt (>= 1.4),
76 python3-autopilot-trace,
77 recordmydesktop,
78Description: Utility to write and run integration tests easily (Python 3)
79 The autopilot engine enables to ease the writing of python tests
80 for your application manipulating your inputs like the mouse and
81 keyboard. It also provides a lot of utilities linked to the X server
82 and detecting applications.
83
46Package: python-autopilot-trace84Package: python-autopilot-trace
47Architecture: any85Architecture: any
48Depends: ${misc:Depends},86Depends: ${misc:Depends},
@@ -53,12 +91,21 @@
53 autopilot tests. This is useful when using autopilot to exercise91 autopilot tests. This is useful when using autopilot to exercise
54 an instrumented application.92 an instrumented application.
5593
94Package: python3-autopilot-trace
95Architecture: any
96Depends: ${misc:Depends},
97 ${shlibs:Depends},
98 python3-autopilot,
99Description: Support for tracing in autopilot (Python 3)
100 This package contains the binary lttng trace point for tracing
101 autopilot tests. This is useful when using autopilot to exercise
102 an instrumented application.
56103
57Package: autopilot-touch104Package: autopilot-touch
58Architecture: any105Architecture: any
59Section: metapackages106Section: metapackages
60Depends: ${misc:Depends},107Depends: ${misc:Depends},
61 libautopilot-qt (>= 1.3) [!powerpc],108 libautopilot-qt (>= 1.4) [!powerpc],
62 python-autopilot,109 python-autopilot,
63 python-evdev,110 python-evdev,
64 python-ubuntu-platform-api [armhf],111 python-ubuntu-platform-api [armhf],
@@ -91,7 +138,20 @@
91 python-qt4,138 python-qt4,
92 python-qt4-dbus,139 python-qt4-dbus,
93Description: The visualisation application for Autopilot.140Description: The visualisation application for Autopilot.
94 The Autopilot vis tool allows you to inspect an application introspectioin141 The Autopilot vis tool allows you to inspect an application introspection
142 tree. It is a useful tool for test authors, but not required to run autopilot
143 tests.
144
145Package: python3-autopilot-vis
146Architecture: all
147Depends: ${misc:Depends},
148 ${python3:Depends},
149 python3-autopilot,
150 python3-dbus,
151 python3-pyqt4,
152 python3-dbus.mainloop.qt,
153Description: The visualisation application for Autopilot (Python 3)
154 The Autopilot vis tool allows you to inspect an application introspection
95 tree. It is a useful tool for test authors, but not required to run autopilot155 tree. It is a useful tool for test authors, but not required to run autopilot
96 tests.156 tests.
97157
@@ -99,8 +159,8 @@
99Architecture: all159Architecture: all
100Depends: ${misc:Depends},160Depends: ${misc:Depends},
101 ${python:Depends},161 ${python:Depends},
102 libautopilot-gtk,162 libautopilot-gtk (>= 1.4),
103 libautopilot-qt,163 libautopilot-qt (>= 1.4),
104 python-autopilot,164 python-autopilot,
105 python-evdev,165 python-evdev,
106 python-mock,166 python-mock,
@@ -112,3 +172,21 @@
112 You can use this package to verify that autopilot is functioning172 You can use this package to verify that autopilot is functioning
113 correctly, or to copy the techniques used in the autopilot tests173 correctly, or to copy the techniques used in the autopilot tests
114 themselves.174 themselves.
175
176Package: python3-autopilot-tests
177Architecture: all
178Depends: ${misc:Depends},
179 ${python3:Depends},
180 libautopilot-gtk (>= 1.4),
181 libautopilot-qt (>= 1.4),
182 python3-autopilot,
183 python3-evdev,
184 python3-mock,
185 python-windowmocker,
186 qmlscene,
187 recordmydesktop,
188Description: Tests for the autopilot functional test tool. (Python 3)
189 This package contains tests for the python3-autopilot package.
190 You can use this package to verify that autopilot is functioning
191 correctly, or to copy the techniques used in the autopilot tests
192 themselves.
115193
=== added file 'debian/python-autopilot-tests.install'
--- debian/python-autopilot-tests.install 1970-01-01 00:00:00 +0000
+++ debian/python-autopilot-tests.install 2013-09-16 17:17:50 +0000
@@ -0,0 +1,3 @@
1usr/lib/python2*/*/autopilot/tests/*.py
2usr/lib/python2*/*/autopilot/tests/unit/*.py
3usr/lib/python2*/*/autopilot/tests/functional/*.py
04
=== removed file 'debian/python-autopilot-tests.pyinstall'
--- debian/python-autopilot-tests.pyinstall 2013-05-08 23:43:42 +0000
+++ debian/python-autopilot-tests.pyinstall 1970-01-01 00:00:00 +0000
@@ -1,3 +0,0 @@
1autopilot/tests/*.py
2autopilot/tests/unit/*.py
3autopilot/tests/functional/*.py
40
=== added file 'debian/python-autopilot-vis.install'
--- debian/python-autopilot-vis.install 1970-01-01 00:00:00 +0000
+++ debian/python-autopilot-vis.install 2013-09-16 17:17:50 +0000
@@ -0,0 +1,1 @@
1usr/lib/python2*/*/autopilot/vis/*.py
02
=== removed file 'debian/python-autopilot-vis.pyinstall'
--- debian/python-autopilot-vis.pyinstall 2013-04-30 16:49:02 +0000
+++ debian/python-autopilot-vis.pyinstall 1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
1autopilot/vis/*.py
20
=== modified file 'debian/python-autopilot.install'
--- debian/python-autopilot.install 2013-05-04 01:01:06 +0000
+++ debian/python-autopilot.install 2013-09-16 17:17:50 +0000
@@ -1,2 +1,8 @@
1usr/lib/python2*/*/autopilot/*.py
2usr/lib/python2*/*/autopilot/display/*.py
3usr/lib/python2*/*/autopilot/input/*.py
4usr/lib/python2*/*/autopilot/introspection/*.py
5usr/lib/python2*/*/autopilot/matchers/*.py
6usr/lib/python2*/*/autopilot/process/*.py
1usr/bin/autopilot /usr/bin/7usr/bin/autopilot /usr/bin/
2debian/61-autopilot-uinput.rules /lib/udev/rules.d8debian/61-autopilot-uinput.rules /lib/udev/rules.d
39
=== removed file 'debian/python-autopilot.pyinstall'
--- debian/python-autopilot.pyinstall 2013-04-30 16:49:02 +0000
+++ debian/python-autopilot.pyinstall 1970-01-01 00:00:00 +0000
@@ -1,6 +0,0 @@
1autopilot/*.py
2autopilot/display/*.py
3autopilot/input/*.py
4autopilot/introspection/*.py
5autopilot/matchers/*.py
6autopilot/process/*.py
70
=== added file 'debian/python3-autopilot-tests.install'
--- debian/python3-autopilot-tests.install 1970-01-01 00:00:00 +0000
+++ debian/python3-autopilot-tests.install 2013-09-16 17:17:50 +0000
@@ -0,0 +1,3 @@
1usr/lib/python3*/*/autopilot/tests/*.py
2usr/lib/python3*/*/autopilot/tests/unit/*.py
3usr/lib/python3*/*/autopilot/tests/functional/*.py
04
=== added file 'debian/python3-autopilot-trace.install'
--- debian/python3-autopilot-trace.install 1970-01-01 00:00:00 +0000
+++ debian/python3-autopilot-trace.install 2013-09-16 17:17:50 +0000
@@ -0,0 +1,1 @@
1usr/lib/python3*/*/autopilot/tracepoint.*.so
02
=== added file 'debian/python3-autopilot-vis.install'
--- debian/python3-autopilot-vis.install 1970-01-01 00:00:00 +0000
+++ debian/python3-autopilot-vis.install 2013-09-16 17:17:50 +0000
@@ -0,0 +1,1 @@
1usr/lib/python3*/*/autopilot/vis/*.py
02
=== added file 'debian/python3-autopilot.docs'
--- debian/python3-autopilot.docs 1970-01-01 00:00:00 +0000
+++ debian/python3-autopilot.docs 2013-09-16 17:17:50 +0000
@@ -0,0 +1,2 @@
1docs/_build/html/
2
03
=== added file 'debian/python3-autopilot.install'
--- debian/python3-autopilot.install 1970-01-01 00:00:00 +0000
+++ debian/python3-autopilot.install 2013-09-16 17:17:50 +0000
@@ -0,0 +1,9 @@
1usr/bin/autopilot /usr/bin/
2debian/61-autopilot-uinput.rules /lib/udev/rules.d
3usr/lib/python3*/*/autopilot/*.py
4usr/lib/python3*/*/autopilot/display/*.py
5usr/lib/python3*/*/autopilot/input/*.py
6usr/lib/python3*/*/autopilot/introspection/*.py
7usr/lib/python3*/*/autopilot/matchers/*.py
8usr/lib/python3*/*/autopilot/process/*.py
9
010
=== added file 'debian/python3-autopilot.postinst'
--- debian/python3-autopilot.postinst 1970-01-01 00:00:00 +0000
+++ debian/python3-autopilot.postinst 2013-09-16 17:17:50 +0000
@@ -0,0 +1,24 @@
1#!/bin/sh
2# postinst for autopilot
3
4set -e
5
6AUTOPILOT_GROUP=autopilot
7
8if [ "$1" = configure ]; then
9 # Add the autopilot group
10 addgroup --quiet \
11 --system \
12 --no-create-home \
13 "$AUTOPILOT_GROUP" || true
14
15 # Add each sudo user to the autopilot group
16 for u in $(getent group sudo | sed -e "s/^.*://" -e "s/,/ /g"); do
17 adduser --quiet "$u" "$AUTOPILOT_GROUP" > /dev/null || true
18 done
19
20 # udev rules were installed, trigger creation of the /dev node
21 udevadm trigger --action=change
22fi
23
24#DEBHELPER#
025
=== modified file 'debian/rules'
--- debian/rules 2013-05-30 12:00:24 +0000
+++ debian/rules 2013-09-16 17:17:50 +0000
@@ -5,14 +5,42 @@
5# export DH_VERBOSE=15# export DH_VERBOSE=1
66
7%:7%:
8 dh $@ --with python28 dh $@ --with python2,python3
9
10override_dh_auto_clean:
11 set -ex; for python in $(shell py3versions -r); do \
12 $$python setup.py clean || true; \
13 done
14 dh_auto_clean
15 rm -rf build
916
10override_dh_auto_build:17override_dh_auto_build:
11 sphinx-build -b html -W docs/ docs/_build/html/18 sphinx-build -b html -W docs/ docs/_build/html/
19 set -ex; for python in $(shell py3versions -r); do \
20 $$python setup.py build; \
21 done
12 dh_auto_build22 dh_auto_build
1323
14override_dh_auto_test:24override_dh_auto_test:
25ifeq (, $(findstring nocheck, $(DEB_BUILD_OPTIONS)))
15 python -m testtools.run discover autopilot.tests.unit26 python -m testtools.run discover autopilot.tests.unit
27 python3 -m testtools.run discover autopilot.tests.unit
28endif
29
30override_dh_auto_install:
31 set -ex; for python in $(shell py3versions -r); do \
32 $$python setup.py install --root=$(CURDIR)/debian/tmp --install-layout=deb; \
33 done
34 dh_auto_install
1635
17override_dh_install:36override_dh_install:
18 dh_install --list-missing37 # note, egg-info isn't important unless we have entry_points or other
38 # things that runtime wants to inspect
39 dh_install -X.pyc -X.egg-info --fail-missing
40 # provide python3-ized runner and avoid file conflicts
41 set -e; if [ -d debian/python3-autopilot ]; then \
42 cd debian/python3-autopilot; \
43 mv usr/bin/autopilot usr/bin/autopilot-py3; \
44 sed -i '1 s/python$$/python3/' usr/bin/autopilot-py3; \
45 mv lib/udev/rules.d/61-autopilot-uinput.rules lib/udev/rules.d/61-autopilot-py3-uinput.rules; \
46 fi
1947
=== modified file 'docs/_templates/indexcontent.html'
--- docs/_templates/indexcontent.html 2013-06-14 03:31:10 +0000
+++ docs/_templates/indexcontent.html 2013-09-16 17:17:50 +0000
@@ -38,6 +38,12 @@
38 <span class="linkdescr">How to port your tests from earlier versions of Autopilot.</span>38 <span class="linkdescr">How to port your tests from earlier versions of Autopilot.</span>
39 </p>39 </p>
40 </td>40 </td>
41 <td>
42 <p class="biglink">
43 <a class="biglink" href="{{ pathto("appendix/appendix") }}">Appendices</a><br/>
44 <span class="linkdescr">Technical documentation that doesn't fit anywhere else.</span>
45 </p>
46 </td>
41 </tr>47 </tr>
42 </table>48 </table>
4349
4450
=== modified file 'docs/api/introspection.rst'
--- docs/api/introspection.rst 2013-06-25 23:10:27 +0000
+++ docs/api/introspection.rst 2013-09-16 17:17:50 +0000
@@ -4,3 +4,6 @@
4.. automodule:: autopilot.introspection4.. automodule:: autopilot.introspection
5 :members: CustomEmulatorBase, DBusIntrospectionObject, get_proxy_object_for_existing_process5 :members: CustomEmulatorBase, DBusIntrospectionObject, get_proxy_object_for_existing_process
66
7
8.. automodule:: autopilot.introspection.types
9 :members: PlainType, Rectangle, Point, Size, DateTime, Time
710
=== added directory 'docs/appendix'
=== added file 'docs/appendix/appendix.rst'
--- docs/appendix/appendix.rst 1970-01-01 00:00:00 +0000
+++ docs/appendix/appendix.rst 2013-09-16 17:17:50 +0000
@@ -0,0 +1,9 @@
1Appendices
2###########
3
4These technical documents describe features of the autopilot ecosystem that test authors probably don't need to know about, but may be useful to developers of autopilot itself.
5
6.. toctree::
7 :maxdepth: 2
8
9 protocol
010
=== added file 'docs/appendix/protocol.rst'
--- docs/appendix/protocol.rst 1970-01-01 00:00:00 +0000
+++ docs/appendix/protocol.rst 2013-09-16 17:17:50 +0000
@@ -0,0 +1,366 @@
1XPathSelect Query Protocol
2##########################
3
4
5This document defines the protocol used between autopilot and the application under test. In almost all cases, the application under test needs no knowledge of this protocol, since it is handled by one of the UI toolkit specific autopilot drivers (we support both Gtk and Qt). If you are a test author, you should be aware that this document likely contains no useful information for you!
6
7.. contents::
8
9**Who should read this document:**
10
11 * People wanting to hack on autopilot itself.
12 * People wanting to hack on xpathselect.
13 * People wanting to use autopilot to test an application not supported by the current autopilot UI toolkit drivers.
14 * People wanting to write a new UI toolkit driver.
15
16
17DBus Interface
18==============
19
20Every application under test must register itself on a DBus bus. Traditionally this is the DBus session bus, but autopilot supports any DBus bus that the user running autopilot has access to. Applications may choose to us ea well-known connection name, but it is not required.
21
22The only requirement for the DBus connection is that the ``com.canonical.Autopilot.Introspection`` interface is presented on exactly one exported object. The interface has two methods:
23
24 * ``GetVersion()``. The ``GetVersion`` method takes no parameters, and returns a string describing the DBus wire protocol version being used by the application under test. Autopilot will refuse to connect to DBus wire protocol versions that are different than the expected version. The current version string is described later in this document. The version string should be in the format "X.Y", where ``X``, ``Y`` are the major and minor version numbers, respectively.
25
26 * ``GetState(...)``. The ``GetState`` method takes a single string parameter, which is the "XPath Query". The format of that string parameter, and the return value, are the subject of the rest of this document.
27
28
29Object Trees
30============
31
32Autopilot assumes that the application under test is constructed of a tree of objects. This is true for both Gtk and Qt applications, where the tree usually starts with an "application" object, and roughly follows widget stacking order.
33
34The purpose of the XPathSelect protocol is to allow autopilot to select the parts of the application that it is interested in.
35
36Autopilot passes a query string to the ``GetState`` method, and the application replies with the objects selected (if any), and their internal state details.
37
38The object tree is a tree of objects. Objects have several properties that are worth mentioning:
39
40 * Each object *must* have a name that can be used as a python identifier. This is usually the class name or widget type (for example, ``QPushButton``, in a Qt GUI application).
41 * Each object *must* have an attribute named "id", whose value is an integer that is guaranteed to be unique for that object (note however that the same object may appear multiple times in the introspection tree in different places. In this case, the object id must remain consistent between each appearance).
42 * Objects *may* have more attributes. Each attribute name must be a string that can be represented as a python variable name. Attribute values can be any type that is representable over dbus.
43 * Objects *may* have zero or more child objects.
44
45This tree of objects is known as an "introspection tree" throughout the autopilot documentation.
46
47Selecting Objects
48=================
49
50Objects in a tree are selected in a very similar fashion to files in a filesystem. The ``/`` character is used to separate between levels of the tree.
51
52Selecting the Root Object
53+++++++++++++++++++++++++
54
55A shortcut exists for selecting the root object in the introspection tree. A query of ``/`` will always return the root object. This is used when autopilot does not yet know the name of, the root object.
56
57Absolute Queries
58++++++++++++++++
59
60Absolute queries specify the entire path to the object, or objects autopilot is interested in. They must start with ``/`` and specify object names, separated by ``/`` characters. For example:
61
62.. list-table:: **XPathSelect Absolute Queries**
63 :header-rows: 1
64
65 * - Query:
66 - Selects:
67 * - ``/``
68 - The root object (see above).
69 * - ``/foo``
70 - The root object, which must be named 'foo'.
71 * - ``/foo/bar``
72 - Object 'bar', which is a direct child of the root object 'foo'.
73
74
75Using absolute queries, it is possible to select nodes in a tree of objects. However, there is no requirement for an absolute query path to match to exactly one object in the tree. For example, given a tree that looks like this:
76
77.. graphviz::
78
79 digraph foo {
80 node [shape=box];
81 a [label="foo"];
82 b [label="bar"];
83 c [label="bar"];
84 d [label="bar"];
85
86 a -> b;
87 a -> c;
88 a -> d;
89 }
90
91a query of ``/foo/bar`` will select two objects. This is allowed, but not always what we want. There are several ways to avoid this, they will be covered later in this document.
92
93Relative Queries
94++++++++++++++++
95
96Absolute queries are very fast for the application under test to process, and are used whenever autopilot knows where the object it wants to look at exists within the introspection tree. However, sometimes we need to be able to retrieve all the objects of a certain type within the tree. XPathSelect understands relative queries, which will select objects of a specified type anywhere in the tree. For example:
97
98.. list-table:: **XPathSelect Relative Queries**
99 :header-rows: 1
100
101 * - Query:
102 - Selects:
103 * - ``//foo``
104 - All objects named 'foo', anywhere in the tree.
105
106Relative queries are much slower for the application under test to process, since the entire introspection tree must be searched for the objects that match the search criteria. Additionally, relative queries can generate a large amount of data that has to be sent across DBus, which can slow things down further.
107
108Mixed Queries
109+++++++++++++
110
111Absolute and relative queries can be mixed. All the relative queries in the above table will search the entire object tree. However, sometimes you only want to search part of the object tree, in which case you can use a mixed query:
112
113.. list-table:: **XPathSelect Mixed Queries**
114 :header-rows: 1
115
116 * - Query:
117 - Selects:
118 * - ``/foo/bar//baz``
119 - Select all objects named 'baz' which are in the tree beneath '/foo/bar'
120 * - ``/foo/far//bar/baz``
121 - Select all 'baz' objects which are immeadiate children of a 'bar' object, which itself is in the subtree beneath '/foo/far'.
122
123As you can see, mixed queries can get reasonably complicated.
124
125Attribute Queries
126+++++++++++++++++
127
128Sometimes we want to select an object whose attributes match certain values. For example, if the application under test is a Qt application, we may want to retrieve a list of 'QPushButton' objects whose 'active' attribute is set to ``True``.
129
130The XPathSelect query protocol supports three value types for attributes:
131
132 * Boolean attribute values are represented as ``True`` or ``False``.
133
134 * String attribute values are represented as strings inside double quote characters. The XPathSelect library understands the common character escape codes, as well as the ``\x__`` hexidecimal escape sequence (For exmaple: ``"\x41"`` would evaluate to a string with a single character 'A'.).
135
136 * Integer attribute values are supported. Integers may use a sign (either '+' or '-'). The sign may be omitted for positive numbers. The range for integer values is from :math:`-2^{32}` to :math:`2^{31}-1`.
137
138Attribute queries are done inside square brackets (``[...]``) next to the object they apply to. The following table lists a number of attribute queries, as examples of what can be achieved.
139
140.. list-table:: **XPathSelect Attribute Queries**
141 :header-rows: 1
142
143 * - Query:
144 - Selects:
145 * - ``//QPushButton[active=True]``
146 - Select all ``QPushbutton`` objects whose "active" attribute is set to True.
147 * - ``//QPushButton[label="Deploy Robots!"]``
148 - Select all ``QPushButton`` objects whose labels are set to the string "Deploy Robots".
149 * - ``//QPushButton[label="Deploy Robots!",active=True]``
150 - Select all ``QPushButton`` objects whose labels are set to the string "Deploy Robots", *and* whose "active" attribute is set to True.
151 * - ``//QSpinBox[value=-10]``
152 - Select all ``QSpinBox`` objects whose value attribute is set to -10.
153
154.. note::
155 While the XPathSelect protocol has a fairly limited list of supported types for attribute matching queries, it is important to note that autopilot transparently supports matching object attributes of any type. Autopilot will send attribute filters to the application under test using the XPathSelect protocol only if the attribute filters are supported by XPathSelect. In all other cases, the filtering will be done within autopilot. At worst, the test author may notice that some queries take longer than others.
156
157Wildcard Nodes
158==============
159
160As well as selecting a node in the introspection tree by node name, one can also use ``*`` to select any node. However, there are a few restrictions on this feature, to stop the inadvertent selection of the entire tree.
161
162Selecting All Children
163++++++++++++++++++++++
164
165Wildcard nodes are often used to select all the children of a particular object. For example, if the query ``/path/to/object[id=123]`` returns the parent object you are interested in, then the query ``/path/to/object[id=123]/*`` will return all the immediate children of that object.
166
167Selecting Nodes based on Attributes
168+++++++++++++++++++++++++++++++++++
169
170The second use of wildcard nodes is to select nodes based purely on their attributes, rather than their type. For example, to select every object with a 'visible' property set to 'True', the following query will work: ``//*[visible=True]``. However, care must be taken - this query will traverse the entire introspection tree, and may take a long time. Additionally, a large amount of data may be returned over DBus.
171
172Invalid Wildcard Queries
173++++++++++++++++++++++++
174
175The wildcard specifier may only be used after a search separator if you have also specified attribute filters. For example, all the following queries are invalid:
176
177**Invalid Queries**
178
179* ``//*``
180
181* ``/path/to/some/node//*``
182
183* ``//node//*``
184
185However, the following queries are all valid:
186
187**Valid Queries**
188
189* ``//node/*``
190
191* ``/node//*[key="value"]``
192
193* ``//node//*[key=True]``
194
195Returning State Data
196====================
197
198Once the application under test has parsed the XPathSleect query, and has a list (possibly empty!) of objects that match the given query, it must serialize those objects back across DBus as the return value from the ``GetState`` method. The data structure used is reasonably complex, and is described below:
199
200* At the top level, the return type must be an array of objects. Each item in the array represents an object that matched the supplied query. If no objects matched the supplied query, an empty array must be returned.
201
202 * Each object is a DBus structure that has two parts: a string, and a map. The string specifies the full tree path to the object being returned (for example "/path/to/object").
203
204 * The map represents the object state, and is a map of strings to arrays. The keys in this map are property names (for example "visible").
205
206 * The arrays represents the property value. It contains at least two parts, a value type id (see below for a list of these ids and what they mean), and one or more values. The values can be any type representable over dbus. Some values may actually be arrays of values, for example.
207
208.. graphviz::
209
210 digraph dbus_data {
211 node [shape=record];
212
213 objects [label="Object|<f1>Object|..."];
214 object2 [label="Object_name|<f1>Object_state"];
215 object_state [label="property|<f0>property|..."]
216 property [label="key|value_type|value|..."]
217
218 objects:f1 -> object2;
219 object2:f1 -> object_state;
220 object_state:f0 -> property;
221 }
222
223
224Valid IDs
225+++++++++
226
227The following table lists the various type Ids, and their meaning.
228
229.. list-table:: **Autopilot Type IDs and their meaning**
230 :header-rows: 1
231 :widths: 5 90
232
233 * - Type ID:
234 - Meaning:
235 * - 0
236 - Simple Type. The value is a DBus integer, boolean, or string, and that is exactly how it should be represented to the user.
237 * - 1
238 - Rectangle. The next four values are all integers, and represent a rectangle in cartesian space. The four numbers must represent the x, y, width and height of the rectangle, respectively. Autopilot will likely present the four values as 'x', 'y', 'w' and 'h' to test authors. Autopilot makes no assumptions about the coordinate space used.
239 * - 2
240 - Point. The next two values are integers, and represent an X, Y, point in catesian space.
241 * - 3
242 - Size. The next two value are integers, and represent a width,height pair, describing a size in catesian space.
243 * - 4
244 - Color. The next four values are all integers, and represent the red, green, blue, and alpha components of the color, respectively. Each component is bounded between 0 and 255.
245 * - 5
246 - Date or Date/Time. The next value is an integer representing the number of seconds since the unix epoch (1970-01-011 00:00:00), UTC time.
247 * - 6
248 - Time. The next values are all integers, and represent hours, minutes, seconds, milliseconds.
249
250Special Attributes
251++++++++++++++++++
252
253Most attributes that are returned will be attributes of the UI toolkit class itself. However, there are two special attributes:
254
255* The ``id`` attribute *must* be present, and must contain an integer number. This number must be unique for this instance of the object. This number must also be within the range suitable for integer parameter matching.
256
257* The ``Children`` attribute *may* be present if the object being serialized has any children in the introspection tree. If it is present, it must be an array of strings, where each string is the class name of the immediate child object.
258
259* The ``globalRect`` property should be present for any components that have an on-screen presence. It should be a 4-element array containing the x,y,w,h values of the items on-screen coordinates. Note that these coordinates should be in screen-space, rather than local application space.
260
261Example GetState Return Values
262++++++++++++++++++++++++++++++
263
264All the examples in this section have had whitespace added to make them more readable.
265
266**Example 1: Unity Shell**
267
268Query: ``/``
269
270Return Value::
271
272 [
273 (
274 '/Unity',
275 {
276 'id': 0L,
277 'Children':
278 [
279 'DashController',
280 'HudController',
281 'LauncherController',
282 'PanelController',
283 'Screen'
284 'SessionController',
285 'ShortcutController',
286 'SwitcherController',
287 'WindowManager',
288 ]
289 }
290 )
291 ]
292
293
294**Example 2: Qt Creator Menu**
295
296This is a much larger object, and shows the ``globalRect`` attribute.
297
298Query: ``/QtCreator/QMenu[objectName="Project.Menu.Session"]``
299
300Return Value::
301
302 [
303 (
304 '/QtCreator/QMenu',
305 {
306 '_autopilot_id': 2L,
307 'acceptDrops': False,
308 'accessibleDescription': '',
309 'accessibleName': '',
310 'autoFillBackground': False,
311 'baseSize': [0, 0],
312 'Children': ['QAction'],
313 'childrenRect': [0, 0, 0, 0],
314 'contextMenuPolicy': 1,
315 'enabled': True,
316 'focus': False,
317 'focusPolicy': 0,
318 'frameGeometry': [0, 0, 100, 30],
319 'frameSize': [100, 30],
320 'fullScreen': False,
321 'geometry': [0, 0, 100, 30],
322 'globalRect': [0, 0, 100, 30],
323 'height': 30,
324 'id': 2L,
325 'inputMethodHints': 0,
326 'isActiveWindow': False,
327 'layoutDirection': 0,
328 'maximized': False,
329 'maximumHeight': 16777215,
330 'maximumSize': [16777215, 16777215]
331 'maximumWidth': 16777215,
332 'minimized': False,
333 'minimumHeight': 0,
334 'minimumSize': [0, 0],
335 'minimumSizeHint': [-1, -1],
336 'minimumWidth': 0,
337 'modal': False,
338 'mouseTracking': True,
339 'normalGeometry': [0, 0, 0, 0],
340 'objectName': 'Project.Menu.Session',
341 'pos': [0, 0],
342 'rect': [0, 0, 100, 30],
343 'separatorsCollapsible': True,
344 'size': [100, 30],
345 'sizeHint': [379, 205],
346 'sizeIncrement': [0, 0],
347 'statusTip': '',
348 'styleSheet': '',
349 'tearOffEnabled': False,
350 'title': '',
351 'toolTip': '',
352 'updatesEnabled': True,
353 'visible': False,
354 'whatsThis': '',
355 'width': 100,
356 'windowFilePath': '',
357 'windowIconText': '',
358 'windowModality': 0,
359 'windowModified': False,
360 'windowOpacity': 1.0,
361 'windowTitle': '',
362 'x': 0,
363 'y': 0,
364 }
365 )
366 ]
0367
=== modified file 'docs/conf.py'
--- docs/conf.py 2013-07-25 05:47:36 +0000
+++ docs/conf.py 2013-09-16 17:17:50 +0000
@@ -50,6 +50,8 @@
50# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.50# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
51extensions = [51extensions = [
52 'sphinx.ext.autodoc',52 'sphinx.ext.autodoc',
53 'sphinx.ext.graphviz',
54 'sphinx.ext.pngmath',
53 'sphinx.ext.viewcode',55 'sphinx.ext.viewcode',
54 'otto',56 'otto',
55]57]
@@ -84,7 +86,7 @@
84except ImportError:86except ImportError:
85 # If we don't have python-debian installed, guess a coarse-grained version87 # If we don't have python-debian installed, guess a coarse-grained version
86 # string88 # string
87 version = '1.3'89 version = '1.4'
8890
89# The full version, including alpha/beta/rc tags.91# The full version, including alpha/beta/rc tags.
90release = version92release = version
9193
=== modified file 'docs/contents.rst'
--- docs/contents.rst 2013-06-14 02:05:36 +0000
+++ docs/contents.rst 2013-09-16 17:17:50 +0000
@@ -11,3 +11,4 @@
11 porting/porting11 porting/porting
12 faq/faq12 faq/faq
13 faq/contribute13 faq/contribute
14 appendix/appendix
1415
=== modified file 'docs/porting/porting.rst'
--- docs/porting/porting.rst 2013-04-27 02:59:44 +0000
+++ docs/porting/porting.rst 2013-09-16 17:17:50 +0000
@@ -12,6 +12,24 @@
1212
13Autopilot versions earlier than 1.2 were not publicly announced, and were only used within Canonical. For that reason, this document assumes that version 1.2 is the lowest version of autopilot present `"in the wild"`.13Autopilot versions earlier than 1.2 were not publicly announced, and were only used within Canonical. For that reason, this document assumes that version 1.2 is the lowest version of autopilot present `"in the wild"`.
1414
15Porting to Autopilot v1.4.x
16===========================
17
18The 1.4 release contains several changes that required a break in the DBus wire protocol between autopilot and the applications under test. Most of these changes require no change to test code.
19
20Gtk Tests and Boolean Parameters
21++++++++++++++++++++++++++++++++
22
23Version 1.3 of the autopilot-gtk backend contained `a bug <https://bugs.launchpad.net/autopilot-gtk/+bug/1214249>`_ that caused all Boolean properties to be exported as integers instead of boolean values. This in turn meant that test code would fail to return the correct objects when using selection criteria such as::
24
25 visible_buttons = app.select_many("GtkPushButton", visible=True)
26
27and instead had to write something like this::
28
29 visible_buttons = app.select_many("GtkPushButton", visible=1)
30
31This bug has now been fixed, and using the integer selection will fail.
32
15Porting to Autopilot v1.3.x33Porting to Autopilot v1.3.x
16===========================34===========================
1735
1836
=== modified file 'docs/tutorial/advanced_autopilot.rst'
--- docs/tutorial/advanced_autopilot.rst 2013-08-23 04:07:36 +0000
+++ docs/tutorial/advanced_autopilot.rst 2013-09-16 17:17:50 +0000
@@ -236,7 +236,7 @@
236 >>> try:236 >>> try:
237 ... kbd = Keyboard.create("uinput")237 ... kbd = Keyboard.create("uinput")
238 ... except RuntimeError as e:238 ... except RuntimeError as e:
239 ... print "Unable to create keyboard:", e239 ... print("Unable to create keyboard: " + str(e))
240 ...240 ...
241 Unable to create keyboard: Unknown backend 'uinput'241 Unable to create keyboard: Unknown backend 'uinput'
242242
243243
=== modified file 'setup.py'
--- setup.py 2013-07-24 05:56:06 +0000
+++ setup.py 2013-09-16 17:17:50 +0000
@@ -28,7 +28,7 @@
28except ImportError:28except ImportError:
29 # If we don't have python-debian installed, guess a coarse-grained version29 # If we don't have python-debian installed, guess a coarse-grained version
30 # string30 # string
31 version = '1.3.1'31 version = '1.4.0'
3232
33autopilot_tracepoint = Extension(33autopilot_tracepoint = Extension(
34 'autopilot.tracepoint',34 'autopilot.tracepoint',

Subscribers

People subscribed via source and target branches