Merge lp:~veebers/autopilot/recursive-object-tree into lp:autopilot

Proposed by Christopher Lee
Status: Merged
Approved by: Thomi Richards
Approved revision: 177
Merged at revision: 173
Proposed branch: lp:~veebers/autopilot/recursive-object-tree
Merge into: lp:autopilot
Diff against target: 308 lines (+233/-47)
3 files modified
autopilot/introspection/__init__.py (+2/-47)
autopilot/introspection/dbus.py (+70/-0)
autopilot/tests/test_dbus_query.py (+161/-0)
To merge this branch: bzr merge lp:~veebers/autopilot/recursive-object-tree
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Approve
Thomi Richards (community) Approve
Review via email: mp+160538@code.launchpad.net

Commit message

Queries are now recursive from the node itself, not the root.

Description of the change

As per bug lp:1144288 queries were from the root 'downwards', now the query starts from the given node.

To post a comment you must log in.
Revision history for this message
Thomi Richards (thomir-deactivatedaccount) wrote :

Still need to do:

1) Update docstring to mention that:
  a) The search is recursive
  b) The search starts with the node you call this method on.
  c) You must either specify a type name, or keyword parameters, or both.
     i) When this happens. Raise an exception.
     ii) Show doctest examples in the docstring for each scenarios.

2) Fix copyright header in autopilot/tests/test_dbus_query. Merge autopilot trunk and copy a header from another python file.

3) Fix docstring formatting on diff line 151 - closing quotes should be on a line by themselves, preceeded by a newline.

Things that are not tested:

Calling select_single with:
 * neither type name nor keyword parameters specified.
 * Type name specified that does not match anything in the tree (should return None)
 * keyword arguments specified that does not match anything in the tree (should return None)

Calling select single with a query that returns more than one item (must raise ValueError).

Calling select_many with:
 * Neither type name nor keyword arguments specified.
 * kwargs only.
 * a query that returns nothing.

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
172. By Christopher Lee

Added failing test

173. By Christopher Lee

Fixed failing tests

174. By Christopher Lee

Fixes re: MP comments. Formatting, documentation and extra tests.

175. By Christopher Lee

Merge trunk

176. By Christopher Lee

Updated license comment in test

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
177. By Christopher Lee

Further MP fixes, documentation and removed 'pick_app_launcher' in the test

Revision history for this message
Thomi Richards (thomir-deactivatedaccount) :
review: Approve
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
1=== modified file 'autopilot/introspection/__init__.py'
2--- autopilot/introspection/__init__.py 2013-04-25 22:45:51 +0000
3+++ autopilot/introspection/__init__.py 2013-04-26 02:36:26 +0000
4@@ -268,55 +268,10 @@
5 class ApplicationProxyObect(DBusIntrospectionObject):
6 """A class that better supports query data from an application."""
7
8- def __init__(self, state, path_info=None):
9- super(ApplicationProxyObect, self).__init__(state, path_info)
10+ def __init__(self, state, path):
11+ super(ApplicationProxyObect, self).__init__(state, path)
12 self._process = None
13
14- def select_single(self, type_name='*', **kwargs):
15- """Get a single node from the introspection tree, with type equal to
16- *type_name* and (optionally) matching the keyword filters present in
17- *kwargs*.
18-
19- For example:
20-
21- >>> app.select_single('QPushButton', objectName='clickme')
22- ... returns a QPushButton whose 'objectName' property is 'clickme'.
23-
24- If nothing is returned from the query, this method returns None.
25-
26- :raises: **ValueError** if the query returns more than one item. *If you
27- want more than one item, use select_many instead*.
28-
29- """
30- instances = self.select_many(type_name, **kwargs)
31- if len(instances) > 1:
32- raise ValueError("More than one item was returned for query")
33- if not instances:
34- return None
35- return instances[0]
36-
37- def select_many(self, type_name='*', **kwargs):
38- """Get a list of nodes from the introspection tree, with type equal to
39- *type_name* and (optionally) matching the keyword filters present in
40- *kwargs*.
41-
42- For example:
43-
44- >>> app.select_many('QPushButton', enabled=True)
45- ... returns a list of QPushButtons that are enabled.
46-
47- If you only want to get one item, use select_single instead.
48-
49- """
50- logger.debug("Selecting objects of %s with attributes: %r",
51- 'any type' if type_name == '*' else 'type ' + type_name,
52- kwargs)
53-
54- path = "//%s" % type_name
55- state_dicts = self.get_state_by_path(path)
56- instances = [self.make_introspection_object(i) for i in state_dicts]
57- return filter(lambda i: object_passes_filters(i, **kwargs), instances)
58-
59 def set_process(self, process):
60 """Set the subprocess.Popen object of the process that this is a proxy for.
61
62
63=== modified file 'autopilot/introspection/dbus.py'
64--- autopilot/introspection/dbus.py 2013-04-23 04:15:54 +0000
65+++ autopilot/introspection/dbus.py 2013-04-26 02:36:26 +0000
66@@ -287,6 +287,76 @@
67 children = [self.make_introspection_object(i) for i in state_dicts]
68 return children
69
70+ def select_single(self, type_name='*', **kwargs):
71+ """Get a single node from the introspection tree, with type equal to
72+ *type_name* and (optionally) matching the keyword filters present in
73+ *kwargs*.
74+ You must specify either *type_name*, keyword filters or both.
75+
76+ Searches recursively from the node this method is called on. For
77+ example:
78+
79+ >>> app.select_single('QPushButton', objectName='clickme')
80+ ... returns a QPushButton whose 'objectName' property is 'clickme'.
81+
82+ If nothing is returned from the query, this method returns None.
83+
84+ :raises: **ValueError** if the query returns more than one item. *If you
85+ want more than one item, use select_many instead*.
86+
87+ :raises: **TypeError** if neither *type_name* or keyword filters are
88+ provided.
89+
90+ """
91+ instances = self.select_many(type_name, **kwargs)
92+ if len(instances) > 1:
93+ raise ValueError("More than one item was returned for query")
94+ if not instances:
95+ return None
96+ return instances[0]
97+
98+ def select_many(self, type_name='*', **kwargs):
99+ """Get a list of nodes from the introspection tree, with type equal to
100+ *type_name* and (optionally) matching the keyword filters present in
101+ *kwargs*.
102+ You must specify either *type_name*, keyword filters or both.
103+
104+ Searches recursively from the node this method is called on.
105+
106+ For example:
107+
108+ >>> app.select_many('QPushButton', enabled=True)
109+ ... returns a list of QPushButtons that are enabled.
110+
111+ >>> file_menu = app.select_one('QMenu', title='File')
112+ >>> file_menu.select_many('QAction')
113+ ... returns a list of QAction objects who appear below file_menu in the
114+ object tree.
115+
116+ If you only want to get one item, use select_single instead.
117+
118+ :raises: **TypeError** if neither *type_name* or keyword filters are
119+ provided.
120+
121+ """
122+ if type_name == "*" and not kwargs:
123+ raise TypeError("You must specify either a type name or a filter.")
124+
125+ logger.debug("Selecting objects of %s with attributes: %r",
126+ 'any type' if type_name == '*' else 'type ' + type_name,
127+ kwargs)
128+
129+ first_param = ''
130+ if kwargs:
131+ first_param = '[{}={}]'.format(*kwargs.popitem())
132+ query_path = "%s//%s%s" % (self.get_class_query_string(),
133+ type_name,
134+ first_param)
135+
136+ state_dicts = self.get_state_by_path(query_path)
137+ instances = [self.make_introspection_object(i) for i in state_dicts]
138+ return filter(lambda i: object_passes_filters(i, **kwargs), instances)
139+
140 def refresh_state(self):
141 """Refreshes the object's state from unity.
142
143
144=== added file 'autopilot/tests/test_dbus_query.py'
145--- autopilot/tests/test_dbus_query.py 1970-01-01 00:00:00 +0000
146+++ autopilot/tests/test_dbus_query.py 2013-04-26 02:36:26 +0000
147@@ -0,0 +1,161 @@
148+# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
149+#
150+# Autopilot Functional Test Tool
151+# Copyright (C) 2012-2013 Canonical
152+#
153+# This program is free software: you can redistribute it and/or modify
154+# it under the terms of the GNU General Public License as published by
155+# the Free Software Foundation, either version 3 of the License, or
156+# (at your option) any later version.
157+#
158+# This program is distributed in the hope that it will be useful,
159+# but WITHOUT ANY WARRANTY; without even the implied warranty of
160+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
161+# GNU General Public License for more details.
162+#
163+# You should have received a copy of the GNU General Public License
164+# along with this program. If not, see <http://www.gnu.org/licenses/>.
165+#
166+
167+
168+import json
169+import os
170+from tempfile import mktemp
171+
172+from autopilot.testcase import AutopilotTestCase
173+from testtools.matchers import Equals, NotEquals, raises
174+
175+
176+class DbusQueryTests(AutopilotTestCase):
177+ """A collection of dbus query tests for autopilot."""
178+
179+ def start_fully_featured_app(self):
180+ """Create an application that includes menus and other nested
181+ elements.
182+
183+ """
184+ window_spec = {
185+ "Menu": [
186+ {
187+ "Title": "File",
188+ "Menu": [
189+ "Open",
190+ "Save",
191+ "Save As",
192+ "Quit"
193+ ]
194+ },
195+ {
196+ "Title": "Help",
197+ "Menu": [
198+ "Help 1",
199+ "Help 2",
200+ "Help 3",
201+ "Help 4"
202+ ]
203+ }
204+ ],
205+ "Contents": "TextEdit"
206+ }
207+
208+ file_path = mktemp()
209+ json.dump(window_spec, open(file_path, 'w'))
210+ self.addCleanup(os.remove, file_path)
211+
212+ return self.launch_test_application('window-mocker', file_path, app_type="qt")
213+
214+ def test_select_single_selects_only_available_object(self):
215+ """Must be able to select a single unique object."""
216+ app = self.start_fully_featured_app()
217+ main_window = app.select_single('QMainWindow')
218+ self.assertThat(main_window, NotEquals(None))
219+
220+ def test_single_select_on_object(self):
221+ """Must be able to select a single unique child of an object."""
222+ app = self.start_fully_featured_app()
223+ main_win = app.select_single('QMainWindow')
224+ menu_bar = main_win.select_single('QMenuBar')
225+ self.assertThat(menu_bar, NotEquals(None))
226+
227+ def test_select_multiple_on_object_returns_all(self):
228+ """Must be able to select all child objects."""
229+ app = self.start_fully_featured_app()
230+ main_win = app.select_single('QMainWindow')
231+ menu_bar = main_win.select_single('QMenuBar')
232+ menus = menu_bar.select_many('QMenu')
233+ self.assertThat(len(menus), Equals(2))
234+
235+ def test_select_multiple_on_object_with_parameter(self):
236+ """Must be able to select a specific object determined by a
237+ parameter.
238+
239+ """
240+ app = self.start_fully_featured_app()
241+ main_win = app.select_single('QMainWindow')
242+ menu_bar = main_win.select_single('QMenuBar')
243+ help_menu = menu_bar.select_many('QMenu', title='Help')
244+ self.assertThat(len(help_menu), Equals(1))
245+ self.assertThat(help_menu[0].title, Equals('Help'))
246+
247+ def test_select_single_on_object_with_param(self):
248+ """Must only select a single unique object using a parameter."""
249+ app = self.start_fully_featured_app()
250+ main_win = app.select_single('QMainWindow')
251+ menu_bar = main_win.select_single('QMenuBar')
252+ help_menu = menu_bar.select_single('QMenu', title='Help')
253+ self.assertThat(help_menu, NotEquals(None))
254+ self.assertThat(help_menu.title, Equals('Help'))
255+
256+ def test_select_many_uses_unique_object(self):
257+ """Given 2 objects of the same type with childen, selection on one will
258+ only get its children.
259+
260+ """
261+ app = self.start_fully_featured_app()
262+ main_win = app.select_single('QMainWindow')
263+ menu_bar = main_win.select_single('QMenuBar')
264+ help_menu = menu_bar.select_single('QMenu', title='Help')
265+ actions = help_menu.select_many('QAction')
266+ self.assertThat(len(actions), Equals(5))
267+
268+ def test_select_single_no_name_no_parameter_raises_exception(self):
269+ app = self.start_fully_featured_app()
270+ fn = lambda: app.select_single()
271+ self.assertThat(fn, raises(TypeError))
272+
273+ def test_select_single_no_match_returns_none(self):
274+ app = self.start_fully_featured_app()
275+ failed_match = app.select_single("QMadeupType")
276+ self.assertThat(failed_match, Equals(None))
277+
278+ def test_select_single_parameters_only(self):
279+ app = self.start_fully_featured_app()
280+ main_win = app.select_single('QMainWindow')
281+ titled_help = main_win.select_single(title='Help')
282+ self.assertThat(titled_help, NotEquals(None))
283+ self.assertThat(titled_help.title, Equals('Help'))
284+
285+ def test_select_single_parameters_no_match_returns_none(self):
286+ app = self.start_fully_featured_app()
287+ failed_match = app.select_single(title="Non-existant object")
288+ self.assertThat(failed_match, Equals(None))
289+
290+ def test_select_single_returning_multiple_raises(self):
291+ app = self.start_fully_featured_app()
292+ fn = lambda: app.select_single('QMenu')
293+ self.assertThat(fn, raises(ValueError))
294+
295+ def test_select_many_no_name_no_parameter_raises_exception(self):
296+ app = self.start_fully_featured_app()
297+ fn = lambda: app.select_single()
298+ self.assertThat(fn, raises(TypeError))
299+
300+ def test_select_many_only_using_parameters(self):
301+ app = self.start_fully_featured_app()
302+ many_help_menus = app.select_many(title='Help')
303+ self.assertThat(len(many_help_menus), Equals(2))
304+
305+ def test_select_many_with_no_parameter_matches_returns_empty_list(self):
306+ app = self.start_fully_featured_app()
307+ failed_match = app.select_many('QMenu', title='qwerty')
308+ self.assertThat(failed_match, Equals([]))

Subscribers

People subscribed via source and target branches