Merge lp:~canonical-platform-qa/autopilot/fix-cpo-having-different-name-1337004 into lp:autopilot

Proposed by Christopher Lee
Status: Superseded
Proposed branch: lp:~canonical-platform-qa/autopilot/fix-cpo-having-different-name-1337004
Merge into: lp:autopilot
Prerequisite: lp:~canonical-platform-qa/autopilot/update-fix-failing-tests
Diff against target: 286 lines (+149/-39)
5 files modified
autopilot/introspection/_search.py (+12/-17)
autopilot/introspection/dbus.py (+37/-1)
autopilot/tests/functional/test_introspection_features.py (+34/-4)
autopilot/tests/unit/test_introspection_dbus.py (+42/-0)
autopilot/tests/unit/test_introspection_search.py (+24/-17)
To merge this branch: bzr merge lp:~canonical-platform-qa/autopilot/fix-cpo-having-different-name-1337004
Reviewer Review Type Date Requested Status
Autopilot Hackers Pending
Review via email: mp+259845@code.launchpad.net

This proposal has been superseded by a proposal from 2015-06-16.

Commit message

Allow Custom Proxy Object classes to have a different name to that of the underlying UI Type

Description of the change

Allow Custom Proxy Object classes to have a different name to that of the underlying UI Type (I.e. having a RedRect CPO for a QQuickRectangle).

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

Hi,

You asked for my feedback, so.... brace yourself :D

First, I think this will solve your immediate problem, so don't take anything else I write here as an immediate disapproval. However, I think you're painting yourself into a corner API-design-wise, and it can be avoided.

Let's consider the 60k ft view:

* Test authors want to select one or more objects from the object tree.
* From a data flow POV, that looks like:

 test query (in python)
   -> xpathselect query string (over dbus)
   -> libxpathselect
   -> query data structure (over dbus)
   -> proxy objects (in python)

Currently we give test authors a way to filter what proxy objects get created from the query data structure (via validate_dbus_object), and the long-standing bug you're trying to solve is that the API for generating the query is very limiting on the client-side.

Your solution below is to allow test authors to override the class name generated in the source query.

I think that doesn't go far enough.

Since autopilot 1.5 was released, we have a pretty good encapsulation of the xpathselect query protocol. Why not make the tweaks necessary so that test authors can:

 - create any valid xpathselect query object
 - filter the results as they come back
 - tell autopilot to create proxies with any given class or class mixin

The big, fundamental mistake we made in autopilot was to conflate "a connection to an application" with "a set of classes that can be used to represent application internals". I think that separating those wouldn't be too hard.

The specific issue I see with this merge proposal is, it'll only be a matter of time before Leo (sorry Leo, I'm totally picking on you here) says "I want to write a test where I only select QQuickRectangles that have property X set to Z, and filtering them on the client side is too costly". That's a valid use case, and the only way you'll be able to support that is by further extending an already bloated API.

I suggest you think about how you can separate the concerns mentioned above, and start writing a new query API, while leaving the old API in place for a release to give people time to migrate (hell, you can probably make it 99% backwards compatible).

Let me know if anything doesn't make sense. I'm not subscribed to this MP, so if you reply, you'll need to ping me on IRC...

562. By Christopher Lee

Remove errounous test fix pre-req.

563. By Christopher Lee

Pyflakes fix

564. By Christopher Lee

Merge trunk updates

Unmerged revisions

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'autopilot/introspection/_search.py'
2--- autopilot/introspection/_search.py 2015-06-16 02:39:44 +0000
3+++ autopilot/introspection/_search.py 2015-05-06 10:03:43 +0000
4@@ -430,6 +430,11 @@
5 return type("DefaultEmulatorBase", (ap_dbus.DBusIntrospectionObject,), {})
6
7
8+WRONG_CPO_CLASS_MSG = '''\
9+base_class: {passed} does not appear to be the actual base CPO class.
10+Perhaps you meant to use: {actual}.'''
11+
12+
13 def _raise_if_base_class_not_actually_base(base_class):
14 """Raises ValueError if the provided base_class is not actually the
15 base_class
16@@ -447,25 +452,15 @@
17 actual_base_class = cls
18
19 if actual_base_class != base_class:
20- error_message = _get_wrong_cpo_error_message(
21- base_class,
22- actual_base_class
23+ raise(
24+ ValueError(
25+ WRONG_CPO_CLASS_MSG.format(
26+ passed=base_class,
27+ actual=actual_base_class
28+ )
29+ )
30 )
31
32- raise ValueError(error_message)
33-
34-
35-WRONG_CPO_CLASS_MSG = '''\
36-base_class: {passed} does not appear to be the actual base CPO class.
37-Perhaps you meant to use: {actual}.'''
38-
39-
40-def _get_wrong_cpo_error_message(passed_base, actual_base):
41- return WRONG_CPO_CLASS_MSG.format(
42- passed=passed_base,
43- actual=actual_base
44- )
45-
46
47 def _make_proxy_object_async(
48 data_source, emulator_base, reply_handler, error_handler):
49
50=== modified file 'autopilot/introspection/dbus.py'
51--- autopilot/introspection/dbus.py 2014-07-31 04:42:08 +0000
52+++ autopilot/introspection/dbus.py 2015-06-16 02:39:44 +0000
53@@ -544,6 +544,36 @@
54 class_name = cls.__name__.encode('utf-8')
55 return state_name == class_name
56
57+ @classmethod
58+ def get_type_query_name(cls):
59+ """Return the Type node name to use within the search query.
60+
61+ This allows for a Custom Proxy Object to be named differently to the
62+ underlying node type name.
63+
64+ For instance if you have a QML type defined in the file RedRect.qml::
65+
66+ import QtQuick 2.0
67+ Rectangle {
68+ color: red;
69+ }
70+
71+ You can then define a Custom Proxy Object for this type like so::
72+
73+ class RedRect(DBusIntrospectionObject):
74+ @classmethod
75+ def get_type_query_name(cls):
76+ return 'QQuickRectangle'
77+
78+ This is due to the qml engine storing 'RedRect' as a QQuickRectangle in
79+ the UI tree and the xpathquery query needs a node type to query for.
80+ By default the query will use the class name (in this case RedRect) but
81+ this will not match any node type in the tree.
82+
83+ """
84+
85+ return cls.__name__
86+
87
88 # TODO - can we add a deprecation warning around this somehow?
89 CustomEmulatorBase = DBusIntrospectionObject
90@@ -557,5 +587,11 @@
91
92 """
93 if not isinstance(maybe_string_or_class, str):
94- return maybe_string_or_class.__name__
95+ return _get_class_type_name(maybe_string_or_class)
96 return maybe_string_or_class
97+
98+def _get_class_type_name(maybe_cpo_class):
99+ if hasattr(maybe_cpo_class, 'get_type_query_name'):
100+ return maybe_cpo_class.get_type_query_name()
101+ else:
102+ return maybe_cpo_class.__name__
103
104=== modified file 'autopilot/tests/functional/test_introspection_features.py'
105--- autopilot/tests/functional/test_introspection_features.py 2015-06-16 02:39:44 +0000
106+++ autopilot/tests/functional/test_introspection_features.py 2015-06-16 02:39:44 +0000
107@@ -42,6 +42,7 @@
108 from autopilot import platform
109 from autopilot.matchers import Eventually
110 from autopilot.testcase import AutopilotTestCase
111+from autopilot.tests.functional import QmlScriptRunnerMixin
112 from autopilot.tests.functional.fixtures import TempDesktopFile
113 from autopilot.introspection import CustomEmulatorBase
114 from autopilot.introspection import _object_registry as object_registry
115@@ -139,8 +140,8 @@
116
117 self.start_mock_app(WindowMockerApp)
118
119- def test_raises_exception_when_using_incorrect_cpo_base_class(self):
120- """Launching a proxy app with incorrect base must raise exception."""
121+ def test_warns_when_using_incorrect_cpo_base_class(self):
122+ # Ensure the warning method is called when launching a proxy.
123 with object_registry.patch_registry({}):
124 class TestCPO(CustomEmulatorBase):
125 pass
126@@ -150,8 +151,9 @@
127 def validate_dbus_object(cls, path, _state):
128 return path == b'/window-mocker'
129
130- self.assertRaises(ValueError, self.start_mock_app, WindowMockerApp)
131-
132+ with patch.object(_search, 'logger') as p_logger:
133+ self.start_mock_app(WindowMockerApp)
134+ self.assertTrue(p_logger.warning.called)
135
136 def test_can_select_custom_emulators_by_name(self):
137 """Must be able to select a custom emulator type by name."""
138@@ -396,3 +398,31 @@
139 [e[1] for e in result2.decorated.errors]
140 )
141 )
142+
143+
144+class CustomCPOTest(AutopilotTestCase, QmlScriptRunnerMixin):
145+
146+ def launch_simple_qml_script(self):
147+ simple_script = dedent("""
148+ import QtQuick 2.0
149+ Rectangle {
150+ objectName: "ExampleRectangle"
151+ }
152+ """)
153+ return self.start_qml_script(simple_script)
154+
155+ def test_cpo_can_be_named_different_to_underlying_type(self):
156+ """A CPO with the correct name match method must be matched if the
157+ class name is different to the Type name.
158+
159+ """
160+ with object_registry.patch_registry({}):
161+ class RandomNamedCPORectangle(CustomEmulatorBase):
162+ @classmethod
163+ def get_type_query_name(cls):
164+ return 'QQuickRectangle'
165+
166+ app = self.launch_simple_qml_script()
167+ rectangle = app.select_single(RandomNamedCPORectangle)
168+
169+ self.assertThat(rectangle.objectName, Equals('ExampleRectangle'))
170
171=== modified file 'autopilot/tests/unit/test_introspection_dbus.py'
172--- autopilot/tests/unit/test_introspection_dbus.py 2014-07-22 02:30:19 +0000
173+++ autopilot/tests/unit/test_introspection_dbus.py 2015-06-16 02:39:44 +0000
174@@ -118,6 +118,28 @@
175 ),
176 )
177
178+ def test_base_class_provides_correct_query_name(self):
179+ self.assertThat(
180+ dbus.DBusIntrospectionObject.get_type_query_name(),
181+ Equals('ProxyBase')
182+ )
183+
184+ def test_inherited_uses_default_get_node_name(self):
185+ class TestCPO(dbus.DBusIntrospectionObject):
186+ pass
187+
188+ self.assertThat(
189+ TestCPO.get_type_query_name(),
190+ Equals('TestCPO')
191+ )
192+
193+ def test_inherited_overwrites_node_name_is_correct(self):
194+ class TestCPO(dbus.DBusIntrospectionObject):
195+ @classmethod
196+ def get_type_query_name(cls):
197+ return "TestCPO"
198+ self.assertThat(TestCPO.get_type_query_name(), Equals("TestCPO"))
199+
200
201 class ProxyObjectPrintTreeTests(TestCase):
202
203@@ -222,3 +244,23 @@
204 class FooBarBaz(object):
205 pass
206 self.assertEqual("FooBarBaz", dbus.get_type_name(FooBarBaz))
207+
208+ def test_get_type_name_returns_classname(self):
209+ class CustomCPO(dbus.DBusIntrospectionObject):
210+ pass
211+
212+ type_name = dbus.get_type_name(CustomEmulatorBase)
213+ self.assertThat(type_name, Equals('ProxyBase'))
214+
215+ def test_get_type_name_returns_custom_node_name(self):
216+ class CustomCPO(dbus.DBusIntrospectionObject):
217+ @classmethod
218+ def get_type_query_name(cls):
219+ return 'TestingCPO'
220+ type_name = dbus.get_type_name(CustomCPO)
221+ self.assertThat(type_name, Equals('TestingCPO'))
222+
223+ def test_get_type_name_returns_classname_of_non_proxybase_classes(self):
224+ class Foo(object):
225+ pass
226+ self.assertEqual('Foo', dbus.get_type_name(Foo))
227
228=== modified file 'autopilot/tests/unit/test_introspection_search.py'
229--- autopilot/tests/unit/test_introspection_search.py 2015-06-16 02:39:44 +0000
230+++ autopilot/tests/unit/test_introspection_search.py 2015-05-06 10:00:49 +0000
231@@ -740,7 +740,10 @@
232 class ActualBase(CustomEmulatorBase):
233 pass
234
235- _s._raise_if_base_class_not_actually_base(ActualBase)
236+ try:
237+ _s._raise_if_base_class_not_actually_base(ActualBase)
238+ except ValueError:
239+ self.fail('Unexpected ValueError exception')
240
241 def test_raises_if_passed_incorrect_base_class(self):
242 class ActualBase(CustomEmulatorBase):
243@@ -797,23 +800,27 @@
244 )
245
246 def test_dont_raise_when_using_default_emulator_base(self):
247+ # _make_proxy_object potentially creates a default base.
248 DefaultBase = _s._make_default_emulator_base()
249- _s._raise_if_base_class_not_actually_base(DefaultBase)
250+ try:
251+ _s._raise_if_base_class_not_actually_base(DefaultBase)
252+ except ValueError:
253+ self.fail('Unexpected ValueError exception')
254
255 def test_exception_message_contains_useful_information(self):
256- actual_base = 'ActualBase'
257-
258- passed_base = 'InheritedCPO'
259-
260- error_message = _s._get_wrong_cpo_error_message(
261- passed_base,
262- actual_base
263- )
264-
265- self.assertEqual(
266- error_message,
267- _s.WRONG_CPO_CLASS_MSG.format(
268- passed=passed_base,
269- actual=actual_base
270+ class ActualBase(CustomEmulatorBase):
271+ pass
272+
273+ class InheritedCPO(ActualBase):
274+ pass
275+
276+ try:
277+ _s._raise_if_base_class_not_actually_base(InheritedCPO)
278+ except ValueError as err:
279+ self.assertEqual(
280+ str(err),
281+ _s.WRONG_CPO_CLASS_MSG.format(
282+ passed=InheritedCPO,
283+ actual=ActualBase
284+ )
285 )
286- )

Subscribers

People subscribed via source and target branches