Merge lp:~pitti/autopilot/print-tree into lp:autopilot

Proposed by Martin Pitt
Status: Merged
Approved by: Thomi Richards
Approved revision: 357
Merged at revision: 361
Proposed branch: lp:~pitti/autopilot/print-tree
Merge into: lp:autopilot
Diff against target: 216 lines (+159/-1)
3 files modified
autopilot/introspection/dbus.py (+41/-0)
autopilot/tests/functional/test_introspection_features.py (+45/-1)
autopilot/tests/unit/test_introspection_features.py (+73/-0)
To merge this branch: bzr merge lp:~pitti/autopilot/print-tree
Reviewer Review Type Date Requested Status
Thomi Richards (community) Approve
PS Jenkins bot continuous-integration Approve
Review via email: mp+192134@code.launchpad.net

Commit message

Add print_tree() introspection method for writing a textual representation of the object and all of its children to stdout, a file object, or a file name.

Description of the change

When writing autopilot tests, finding the type and exact properties of the
exact widget that I'm seeing somewhere on the screen is one of the biggest
challenges. Many widgets can get an explicit identifier (objectName in QML or
GtkBuilder ID in Gtk), but often this doesn't help: For example the actually
visible labels in a QML list view are hidden many layers benath some
unintelligible nested QtQuickItem, HandlerDelegate, and other bits, which makes
finding anything in vis hard.

This adds a print_tree() method which gives a textual dump to stdout, a file
object, or a file name which is much easier to use in above cases, or where vis
is not available (ssh to phone). It supports depth-limiting, too.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
lp:~pitti/autopilot/print-tree updated
355. By Martin Pitt

fix docstring format

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 :

Erk, why don't I get these pep8 errors in a local build and when running "pep8 ."? Fixing..

lp:~pitti/autopilot/print-tree updated
356. By Martin Pitt

Fix PEP-8

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

Hi,

Nice branch! I have a few suggestions though:

1)

16 + def print_tree(self, output=None, maxdepth=None, _curdepth=0):

Instead of making the default value for 'output' be 'None', why not make it 'sys.stdout' ? That way, you can delete this:

39 + if output is None:
40 + output = sys.stdout

And the docs should say that output should be a "file-like object that supports the 'write' method. That way you can delete these lines as well:

41 + elif isinstance(output, six.string_types):
42 + output = open(output, 'w')

I'm not sure that using four spaces as an indentation is a good idea - trees can get pretty large... have you tried this one a bug tree like Unity 7? Perhaps 2 spaces would be better? I'm really not sure either way - but I 'd like to see some larger output....

review: Needs Fixing
lp:~pitti/autopilot/print-tree updated
357. By Martin Pitt

reduce indentation to 2 characters

Revision history for this message
Martin Pitt (pitti) wrote :

> 1)
>
> 16 + def print_tree(self, output=None, maxdepth=None, _curdepth=0):
>
> Instead of making the default value for 'output' be 'None', why not make it
> 'sys.stdout' ? That way, you can delete this:
>
> 39 + if output is None:
> 40 + output = sys.stdout

Python has a rather unintuitive semantics when trying to provide non-trivial values as default argument values (First time I ran into that I actually understood it, and just avoid it ever since). In this case as well, if you actually do the change you'll notice that the tests will fail and you'll get the test tree literally dumped to stdout during the tests (although it's supposed to go into a StringIO).

> And the docs should say that output should be a "file-like object that
> supports the 'write' method. That way you can delete these lines as well:
>
> 41 + elif isinstance(output, six.string_types):
> 42 + output = open(output, 'w')

This is a debugging-only function, so it ought to be as convenient as possible. Thus I would also like to support

  mywidget.print_tree("/tmp/dump.txt")

instead of requiring the user to write something like

  with open("/tmp/dump.txt", "w") as f:
     mywidget.print_tree(f)

> I'm not sure that using four spaces as an indentation is a good idea - trees
> can get pretty large... have you tried this one a bug tree like Unity 7?

I didn't, but I suppose it would take half an hour to do that :-) It already takes several seconds for relatively small trees.

> Perhaps 2 spaces would be better? I'm really not sure either way - but I 'd
> like to see some larger output....

Yes, good point. I reduced it.

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 :

I seeded trusty in the autopilot PPA, should work now.

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

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'autopilot/introspection/dbus.py'
2--- autopilot/introspection/dbus.py 2013-10-13 21:49:49 +0000
3+++ autopilot/introspection/dbus.py 2013-10-25 05:07:27 +0000
4@@ -28,6 +28,7 @@
5 from __future__ import absolute_import
6
7 from contextlib import contextmanager
8+import sys
9 import logging
10 import re
11 import six
12@@ -583,6 +584,46 @@
13 class_type = type(str(name), (base_class,), {})
14 return class_type(state, path, self._backend)
15
16+ def print_tree(self, output=None, maxdepth=None, _curdepth=0):
17+ """Print properties of the object and its children to a stream.
18+
19+ When writing new tests, this can be called when it is too difficult to
20+ find the widget or property that you are interested in in "vis".
21+
22+ .. warning:: Do not use this in production tests, this is expensive and
23+ not at all appropriate for actual testing. Only call this
24+ temporarily and replace with proper select_single/select_many
25+ calls.
26+
27+ :param output: A file object or path name where the output will be
28+ written to. If not given, write to stdout.
29+
30+ :param maxdepth: If given, limit the maximum recursion level to that
31+ number, i. e. only print children which have at most maxdepth-1
32+ intermediate parents.
33+
34+ """
35+ if maxdepth is not None and _curdepth > maxdepth:
36+ return
37+
38+ indent = " " * _curdepth
39+ if output is None:
40+ output = sys.stdout
41+ elif isinstance(output, six.string_types):
42+ output = open(output, 'w')
43+
44+ # print path
45+ if _curdepth > 0:
46+ output.write("\n")
47+ output.write("%s== %s ==\n" % (indent, self._path))
48+ # print properties
49+ for p in sorted(self.get_properties()):
50+ output.write("%s%s: %s\n" % (indent, p, repr(getattr(self, p))))
51+ # print children
52+ if maxdepth is None or _curdepth < maxdepth:
53+ for c in self.get_children():
54+ c.print_tree(output, maxdepth, _curdepth + 1)
55+
56 @contextmanager
57 def no_automatic_refreshing(self):
58 """Context manager function to disable automatic DBus refreshing when
59
60=== modified file 'autopilot/tests/functional/test_introspection_features.py'
61--- autopilot/tests/functional/test_introspection_features.py 2013-09-29 22:45:15 +0000
62+++ autopilot/tests/functional/test_introspection_features.py 2013-10-25 05:07:27 +0000
63@@ -24,8 +24,9 @@
64 import subprocess
65 import tempfile
66 from tempfile import mktemp
67-from testtools.matchers import Equals, IsInstance, Not
68+from testtools.matchers import Equals, IsInstance, Not, Contains
69 from textwrap import dedent
70+from six import StringIO
71
72 from autopilot.matchers import Eventually
73 from autopilot.testcase import AutopilotTestCase
74@@ -119,6 +120,49 @@
75 Not(IsInstance(type(generic_window)))
76 )
77
78+ def test_print_tree_full(self):
79+ """Print tree of full application"""
80+
81+ app = self.start_mock_app(EmulatorBase)
82+ win = app.select_single("QMainWindow")
83+
84+ stream = StringIO()
85+ win.print_tree(stream)
86+ out = stream.getvalue()
87+
88+ # starts with root node
89+ self.assertThat(out.startswith("== /Root/QMainWindow ==\nChildren:"),
90+ Equals(True))
91+ # has root node properties
92+ self.assertThat(out, Contains("windowTitle: 'Default Window Title'\n"))
93+ # has level-1 widgets with expected indent
94+ self.assertThat(out,
95+ Contains(" == /Root/QMainWindow/QRubberBand ==\n"))
96+ self.assertThat(out, Contains(" objectName: 'qt_rubberband'\n"))
97+ # has level-2 widgets with expected indent
98+ self.assertThat(out, Contains(" == /Root/QMainWindow/QMenuBar/"
99+ "QToolButton =="))
100+ self.assertThat(out, Contains(" objectName: "
101+ "'qt_menubar_ext_button'"))
102+
103+ def test_print_tree_depth_limit(self):
104+ """Print depth-limited tree for a widget"""
105+
106+ app = self.start_mock_app(EmulatorBase)
107+ win = app.select_single("QMainWindow")
108+
109+ stream = StringIO()
110+ win.print_tree(stream, 1)
111+ out = stream.getvalue()
112+
113+ # has level-0 (root) node
114+ self.assertThat(out, Contains("== /Root/QMainWindow =="))
115+ # has level-1 widgets
116+ self.assertThat(out, Contains("/Root/QMainWindow/QMenuBar"))
117+ # no level-2 widgets
118+ self.assertThat(out, Not(Contains(
119+ "/Root/QMainWindow/QMenuBar/QToolButton")))
120+
121
122 class QMLCustomEmulatorTestCase(AutopilotTestCase):
123 """Test the introspection of a QML application with a custom emulator."""
124
125=== modified file 'autopilot/tests/unit/test_introspection_features.py'
126--- autopilot/tests/unit/test_introspection_features.py 2013-10-07 03:14:09 +0000
127+++ autopilot/tests/unit/test_introspection_features.py 2013-10-25 05:07:27 +0000
128@@ -17,11 +17,17 @@
129 # along with this program. If not, see <http://www.gnu.org/licenses/>.
130 #
131
132+import sys
133+import tempfile
134+import shutil
135+import os.path
136
137 from mock import patch, Mock
138+from textwrap import dedent
139 from testtools import TestCase
140 from testtools.matchers import Equals, NotEquals
141 from testscenarios import TestWithScenarios
142+from six import StringIO
143
144
145 from autopilot.introspection.dbus import (
146@@ -166,3 +172,70 @@
147 fake_object.get_state_by_path('some_query')
148
149 self.assertThat(mock_logger.warning.called, Equals(False))
150+
151+ def _print_test_fake_object(self):
152+ """common fake object for print_tree tests"""
153+
154+ fake_object = DBusIntrospectionObject(
155+ dict(id=[0, 123], path=[0, '/some/path'], text=[0, 'Hello']),
156+ '/some/path',
157+ Mock()
158+ )
159+ # get_properties() always refreshes state, so can't use
160+ # no_automatic_refreshing()
161+ fake_object.refresh_state = lambda: None
162+ fake_object.get_state_by_path = lambda query: []
163+ return fake_object
164+
165+ def test_print_tree_stdout(self):
166+ """print_tree with default output (stdout)"""
167+
168+ fake_object = self._print_test_fake_object()
169+ orig_sys_stdout = sys.stdout
170+ sys.stdout = StringIO()
171+ try:
172+ fake_object.print_tree()
173+ result = sys.stdout.getvalue()
174+ finally:
175+ sys.stdout = orig_sys_stdout
176+
177+ self.assertEqual(result, dedent("""\
178+ == /some/path ==
179+ id: 123
180+ path: '/some/path'
181+ text: 'Hello'
182+ """))
183+
184+ def test_print_tree_fileobj(self):
185+ """print_tree with file object output"""
186+
187+ fake_object = self._print_test_fake_object()
188+ out = StringIO()
189+
190+ fake_object.print_tree(out)
191+
192+ self.assertEqual(out.getvalue(), dedent("""\
193+ == /some/path ==
194+ id: 123
195+ path: '/some/path'
196+ text: 'Hello'
197+ """))
198+
199+ def test_print_tree_path(self):
200+ """print_tree with file path output"""
201+
202+ fake_object = self._print_test_fake_object()
203+ workdir = tempfile.mkdtemp()
204+ self.addCleanup(shutil.rmtree, workdir)
205+ outfile = os.path.join(workdir, 'widgets.txt')
206+
207+ fake_object.print_tree(outfile)
208+
209+ with open(outfile) as f:
210+ result = f.read()
211+ self.assertEqual(result, dedent("""\
212+ == /some/path ==
213+ id: 123
214+ path: '/some/path'
215+ text: 'Hello'
216+ """))

Subscribers

People subscribed via source and target branches