Merge lp:~canonical-platform-qa/autopilot/cpo-base-classes-fix into lp:autopilot

Proposed by Federico Gimenez
Status: Merged
Approved by: Christopher Lee
Approved revision: 578
Merged at revision: 554
Proposed branch: lp:~canonical-platform-qa/autopilot/cpo-base-classes-fix
Merge into: lp:autopilot
Diff against target: 590 lines (+399/-24)
9 files modified
apparmor/click.rules (+0/-12)
autopilot/introspection/_object_registry.py (+68/-6)
autopilot/introspection/_search.py (+35/-0)
autopilot/tests/functional/test_introspection_features.py (+47/-2)
autopilot/tests/unit/test_introspection_object_registry.py (+147/-1)
autopilot/tests/unit/test_introspection_search.py (+94/-0)
debian/autopilot-touch.dirs (+0/-1)
debian/autopilot-touch.install (+0/-1)
debian/changelog (+8/-1)
To merge this branch: bzr merge lp:~canonical-platform-qa/autopilot/cpo-base-classes-fix
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Approve
Christopher Lee (community) Needs Information
Review via email: mp+257623@code.launchpad.net

Commit message

CPO base classes fix

Description of the change

CPO base classes fix

To post a comment you must log in.
Revision history for this message
Christopher Lee (veebers) wrote :

Hi Federico, you'll see that I added some tests but haven't filled them in.

While back-filling the tests but stopped short of actually implementing them; mainly because the way you've made the fix is different to what I originally had and my initial thoughts wouldn't work :-)

Could you take a look at writing the tests? My current concern with the current implementation is that _combine_base_and_extensions has to know about class stuff (__bases__ and mro) where as the previous method only dealt with tuples of information.
Not only does this mean that it didn't need to know more than 'merge these tuples' it was easy to write tests for (again, just dealing with tuples if data, could be strings etc.).

Also, without a test in place I'm not sure that the sorting of the mro is the correct thing to do.
While I didn't have a test in place to prove it, the intention of the purely tuple approach is that it kept the expected order of merging 2 bases.

Revision history for this message
Federico Gimenez (fgimenez) wrote :

Hi Chris, yes, I totally agree that adding the MRO stuff to the combine method is changing its initial behaviour. I added it after trying to make the current tests pass, I've commented in the code trying to make it clear.

Using the combine method to put together arbitrary tuples was giving at first an error about a cyclic dependency, one of the elements of the tuple was the class itself.

  TypeError: a __bases__ item causes an inheritance cycle

This can be solved by passing the original class itself to the method, and checking that it is not a member of the collections being merged. At this point, we could continue treating the combine method as an abstract information merger. But after this, a second error arose:

  TypeError: Cannot create a consistent method resolution order (MRO)

So that, to combine them appropriately, the method has to know which is the right order, and this information can be obtained from the classes MROs.

Unless I'm missing something it seems that the combine method has to know that it is combining classes, moreover it needs to know that the return value is going to be the bases tuple of one of the parameters. IMO the responsibility is well delimited, maybe we could split it.

Anyway, I'll try to implement the tests that you mention, thanks a lot Chris!

Revision history for this message
Federico Gimenez (fgimenez) wrote :

I've already added the tests for the current implementation of _combine_base_and_extensions.

While writing them I've noticed that when two classes had the same mro the order wasn't deterministic, I've added another function for having a more fine-grained control. Currently the classes in the original __bases__ tuple have lower index than the extensions in the resulting tuple, this can be changed.

Thanks,

Revision history for this message
Christopher Lee (veebers) wrote :

Looking good guys, just a couple of comments inline.

As per the changes I made to day; I was thinking about how to handle detecting and choosing the correct base class when there was multiple inheritance involved (i.e. multiple CPO inheritance).

I came to the conclusion that it is bad form to change things under the cover in this fashion as it can be misleading and cause unexpected changes.
Instead I've opted for issuing a warning when this is detected with the option to upgrade this to raising an error in the new future (i.e. when we can be certain we don't break everyone with this new enforcement rule.)

I was of 2 minds making it raise an exception straight off, but I feel that it was to heavy handed at this stage.
What do you guys think?

I've done some small scale testing locally and the address book and unity8 tests pass as expected. I would love to see this go through the gatekeeper to see the results there.

Revision history for this message
Federico Gimenez (fgimenez) wrote :

Thanks Chris, I've addressed the issues you mentioned, putting this in review

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
570. By Federico Gimenez

Fixed flake8

Revision history for this message
Vincent Ladeuil (vila) wrote :

> Looking good guys, just a couple of comments inline.
>
> As per the changes I made to day; I was thinking about how to handle detecting
> and choosing the correct base class when there was multiple inheritance
> involved (i.e. multiple CPO inheritance).
>
> I came to the conclusion that it is bad form to change things under the cover
> in this fashion as it can be misleading and cause unexpected changes.

/me nods

> Instead I've opted for issuing a warning when this is detected with the option
> to upgrade this to raising an error in the new future (i.e. when we can be
> certain we don't break everyone with this new enforcement rule.)

I would prefer raising an error than fixing the base class too.

Misuse of the CPO is a bug, clients should be fixed.

Especially when looking at Leo's branches fixing this issue in several clients, the fix seems light enough that it's worth breaking the clients so they are fixed asap.

A warning may be missed and the client test suites will not be fixed until it's noticed.

>
> I was of 2 minds making it raise an exception straight off, but I feel that it
> was to heavy handed at this stage.
> What do you guys think?

Let's collect some test results to get a better understanding ;)

>
> I've done some small scale testing locally and the address book and unity8
> tests pass as expected. I would love to see this go through the gatekeeper to
> see the results there.

+1

Looking at http://q-jenkins.ubuntu-ci:8080/job/autopilot-release-gatekeeper it seems we can pass a PPA parameter (it doesn't have to be associated with a silo) so it should be possible to test the two approaches (warning/error) by building in 2 different PPAs and get an idea of the fallouts for both.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
571. By Federico Gimenez

More on comment about the classes ordering

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
572. By Federico Gimenez

merged vila's branch with packaging fix

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
573. By Federico Gimenez

Removing validate_dbus_object method in the extesnsions before adding to the bases to preserve the original method resolution

574. By Federico Gimenez

Fixed changelog

575. By Federico Gimenez

changelog signature

576. By Federico Gimenez

Fixed unit test

Revision history for this message
Federico Gimenez (fgimenez) wrote :

I've uploaded some fixes for errors spotted in the gatekeeper jobs. These were related to the breadth-first nature of method resolution: given one class that doesn't have a validate_dbus_object method of its own and that method is inherited from a non-direct ancestor, when you insert in its bases an extension class with that method defined then that inserted method have preference over the inherited one.

The solution adopted is removing the validate_dbus_object method from the extension classes being added to the bases of the CPOs, so that the correct method resolution is preserved. This has given good results in gatekeeper runs with partial suites, currently there's a complete run being executed (job #271).

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

I'm really not a fan of modifying the classes in such a way, perhaps it's indicating that we're going down the wrong path and we need to find a better solution.

That being said the 2 tests that failed due to _try_custom_proxy_classes raising an exception [1] (the 3rd not being related) shows that the warning re: base CPO classes not being passed in correctly.

I'm just trying to get my local devices etc. setup so I can test this, but I think that if we instead change the warning to an error (yes, I was wrong there it should have always been an error :-) ). and use the updated addresss book (i.e. the one that is passing the correct base_emulator in aka Leos branch) we shouldn't need to modify the class attributes like we are here.

I'll try to post the results when I get them, but at this rate it's going to take ages (my device is acting up.)

[1] http://q-jenkins.ubuntu-ci:8080/job/autopilot-release-gatekeeper/263/label=daily-mako/testReport/

review: Needs Information
Revision history for this message
Christopher Lee (veebers) wrote :

Follow up:
I ran the fixed (i.e. Leos base_emulator update branch) and non fixed address book tests, the fixed worked well, the non-fixed exhibited the error we see.

We should remove the additional code that removes the method attribute and change the warning into an error (probably RuntimeError? or ValueError?).

577. By Federico Gimenez

The validate_dbus_object method is not removed from extensions classes before adding them to cpos; raised exception instead of logging warning when the emulator_base does not match the base extension

578. By Federico Gimenez

Updated error message

Revision history for this message
Federico Gimenez (fgimenez) wrote :

>
> We should remove the additional code that removes the method attribute and
> change the warning into an error (probably RuntimeError? or ValueError?).

Done, regarding the exception after talking with vila we have gone for ValueError, it can be changed of course. I've also updated the unit tests accordingly.

Cheers!

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
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
=== removed directory 'apparmor'
=== removed file 'apparmor/click.rules'
--- apparmor/click.rules 2014-05-23 09:35:07 +0000
+++ apparmor/click.rules 1970-01-01 00:00:00 +0000
@@ -1,12 +0,0 @@
1dbus (receive, send)
2 bus=session
3 path=/com/canonical/Autopilot/**,
4# Allow writes to various (application-specific) XDG directories
5 owner @{HOME}/autopilot/fakeenv/*/.cache/@{APP_PKGNAME}/ rw, # subdir of XDG_CACHE_HOME
6 owner @{HOME}/autopilot/fakeenv/*/.cache/@{APP_PKGNAME}/** mrwkl,
7 owner @{HOME}/autopilot/fakeenv/*/.config/@{APP_PKGNAME}/ rw, # subdir of XDG_CONFIG_HOME
8 owner @{HOME}/autopilot/fakeenv/*/.config/@{APP_PKGNAME}/** mrwkl,
9 owner @{HOME}/autopilot/fakeenv/*/.local/share/@{APP_PKGNAME}/ rw, # subdir of XDG_DATA_HOME
10 owner @{HOMEDIRS}/*/autopilot/fakeenv/*/.local/share/@{APP_PKGNAME}/** mrwklix,
11 owner @{HOME}/autopilot/fakeenv/*/confined/@{APP_PKGNAME}/ rw, # subdir of XDG_RUNTIME_DIR
12 owner @{HOME}/autopilot/fakeenv/*/confined/@{APP_PKGNAME}/** mrwkl,
130
=== modified file 'autopilot/introspection/_object_registry.py'
--- autopilot/introspection/_object_registry.py 2015-02-25 21:51:40 +0000
+++ autopilot/introspection/_object_registry.py 2015-05-06 10:03:54 +0000
@@ -1,7 +1,7 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#2#
3# Autopilot Functional Test Tool3# Autopilot Functional Test Tool
4# Copyright (C) 2014 Canonical4# Copyright (C) 2014, 2015 Canonical
5#5#
6# This program is free software: you can redistribute it and/or modify6# 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 by7# it under the terms of the GNU General Public License as published by
@@ -134,7 +134,6 @@
134 :param object_id: The _id attribute of the class doing the lookup. This is134 :param object_id: The _id attribute of the class doing the lookup. This is
135 used to index into the object registry to retrieve the dict of proxy135 used to index into the object registry to retrieve the dict of proxy
136 classes to try.136 classes to try.
137 :param default_class: default class to use if nothing in dict matches
138 :param path: dbus path137 :param path: dbus path
139 :param state: dbus state138 :param state: dbus state
140 :returns: appropriate custom proxy class139 :returns: appropriate custom proxy class
@@ -153,7 +152,8 @@
153 """Identify which custom proxy class matches the dbus path and state.152 """Identify which custom proxy class matches the dbus path and state.
154153
155 If more than one class in proxy_class_dict matches, raise an exception.154 If more than one class in proxy_class_dict matches, raise an exception.
156 :param proxy_class_dict: dict of proxy classes to try155
156 :param object_id: id to use to get the dict of proxy classes to try
157 :param path: dbus path157 :param path: dbus path
158 :param state: dbus state dict158 :param state: dbus state dict
159 :returns: matching custom proxy class159 :returns: matching custom proxy class
@@ -173,18 +173,80 @@
173 )173 )
174 )174 )
175 if len(possible_classes) == 1:175 if len(possible_classes) == 1:
176 extended_proxy_bases = _get_proxy_bases_for_id(object_id)
177 mixed = _combine_base_and_extensions(
178 possible_classes[0],
179 extended_proxy_bases
180 )
181 possible_classes[0].__bases__ = mixed
176 return possible_classes[0]182 return possible_classes[0]
177 return None183 return None
178184
179185
186def _combine_base_and_extensions(kls, extensions):
187 """Returns the bases of the given class augmented with extensions
188
189 In order to get the right bases tuple, the given class is removed
190 from the result (to prevent cyclic dependencies), there's only one
191 occurrence of each final base class in the result and the result
192 is ordered following the inheritance order (classes lower in the
193 inheritance tree are listed before in the resulting tuple)
194
195 :param kls: class for which we are combining bases and extensions
196 :param extensions: tuple of extensions to be added to kls' bases
197 :returns: bases tuple for kls, including its former bases and the
198 extensions
199
200 """
201 # set of bases + extensions removing the original class to prevent
202 # TypeError: a __bases__ item causes an inheritance cycle
203 unique_bases = {x for x in kls.__bases__ + extensions if x != kls}
204
205 # sort them taking into account inheritance to prevent
206 # TypeError: Cannot create a consistent method resolution order (MRO)
207 return tuple(
208 sorted(
209 unique_bases,
210 key=lambda cls: _get_mro_sort_order(cls, extensions),
211 reverse=True
212 )
213 )
214
215
216def _get_mro_sort_order(cls, promoted_collection=()):
217 """Returns the comparable numerical order for the given class honouring
218 its MRO
219
220 It accepts an optional parameter for promoting classes in a certain
221 group, this can give more control over the sorting when two classes
222 have the a MRO of the same length
223
224 :param cls: the subject class
225 :param promoted_collection: tuple of classes which must be promoted
226 :returns: comparable numerical order, higher for classes with MROs of
227 greater length
228
229 """
230 # Multiplying by 2 the lenght of the MRO list gives the chance to promote
231 # items in the promoted_collection by adding them 1 later: non promoted
232 # classes will have even scores and promoted classes with MRO of the same
233 # length will have odd scores one point higher
234 order = 2 * len(cls.mro())
235
236 if cls in promoted_collection:
237 order += 1
238
239 return order
240
241
180def _get_default_proxy_class(id, name):242def _get_default_proxy_class(id, name):
181 """Return a custom proxy object class of the default or a base class.243 """Return a custom proxy object class of the default or a base class.
182244
183 We want the object to inherit from the class that is set as the emulator245 We want the object to inherit from the class that is set as the emulator
184 base class, not the class that is doing the selecting (which will be the246 base class, not the class that is doing the selecting. Using the passed id
185 'default_class' parameter).247 we retrieve the relevant bases from the object registry.
186248
187 :param default_class: default class to use if no bases match249 :param id: The object id (_id attribute) of the class doing the lookup.
188 :param name: name of new class250 :param name: name of new class
189 :returns: custom proxy object class251 :returns: custom proxy object class
190252
191253
=== modified file 'autopilot/introspection/_search.py'
--- autopilot/introspection/_search.py 2015-04-02 08:27:42 +0000
+++ autopilot/introspection/_search.py 2015-05-06 10:03:54 +0000
@@ -387,12 +387,15 @@
387 # make sure we always have an emulator base. Either use the one the user387 # make sure we always have an emulator base. Either use the one the user
388 # gave us, or make one:388 # gave us, or make one:
389 emulator_base = emulator_base or _make_default_emulator_base()389 emulator_base = emulator_base or _make_default_emulator_base()
390 _raise_if_base_class_not_actually_base(emulator_base)
391
390 # Get the dbus introspection Xml for the backend.392 # Get the dbus introspection Xml for the backend.
391 intro_xml = _get_introspection_xml_from_backend(dbus_address)393 intro_xml = _get_introspection_xml_from_backend(dbus_address)
392 try:394 try:
393 # Figure out if the backend has any extension methods, and return395 # Figure out if the backend has any extension methods, and return
394 # classes that understand how to use each of those extensions:396 # classes that understand how to use each of those extensions:
395 extension_classes = _get_proxy_bases_from_introspection_xml(intro_xml)397 extension_classes = _get_proxy_bases_from_introspection_xml(intro_xml)
398
396 # Register those base classes for everything that will derive from this399 # Register those base classes for everything that will derive from this
397 # emulator base class.400 # emulator base class.
398 _object_registry.register_extension_classes_for_proxy_base(401 _object_registry.register_extension_classes_for_proxy_base(
@@ -427,6 +430,38 @@
427 return type("DefaultEmulatorBase", (ap_dbus.DBusIntrospectionObject,), {})430 return type("DefaultEmulatorBase", (ap_dbus.DBusIntrospectionObject,), {})
428431
429432
433WRONG_CPO_CLASS_MSG = '''\
434base_class: {passed} does not appear to be the actual base CPO class.
435Perhaps you meant to use: {actual}.'''
436
437
438def _raise_if_base_class_not_actually_base(base_class):
439 """Raises ValueError if the provided base_class is not actually the
440 base_class
441
442 To ensure that the expected base classes are used when creating proxy
443 objects.
444
445 :param base_class: The base class to check.
446 :raises ValueError: The actual base class is not the one provided
447
448 """
449 actual_base_class = base_class
450 for cls in base_class.mro():
451 if hasattr(cls, '_id'):
452 actual_base_class = cls
453
454 if actual_base_class != base_class:
455 raise(
456 ValueError(
457 WRONG_CPO_CLASS_MSG.format(
458 passed=base_class,
459 actual=actual_base_class
460 )
461 )
462 )
463
464
430def _make_proxy_object_async(465def _make_proxy_object_async(
431 data_source, emulator_base, reply_handler, error_handler):466 data_source, emulator_base, reply_handler, error_handler):
432 """Make a proxy object for a dbus backend.467 """Make a proxy object for a dbus backend.
433468
=== modified file 'autopilot/tests/functional/test_introspection_features.py'
--- autopilot/tests/functional/test_introspection_features.py 2015-02-26 00:40:54 +0000
+++ autopilot/tests/functional/test_introspection_features.py 2015-05-06 10:03:54 +0000
@@ -24,7 +24,7 @@
24import subprocess24import subprocess
25import tempfile25import tempfile
26from tempfile import mktemp26from tempfile import mktemp
27from testtools import skip, skipIf27from testtools import skipIf
28from testtools.matchers import (28from testtools.matchers import (
29 Contains,29 Contains,
30 Equals,30 Equals,
@@ -36,6 +36,7 @@
36 StartsWith,36 StartsWith,
37)37)
38from textwrap import dedent38from textwrap import dedent
39from unittest.mock import patch
39from io import StringIO40from io import StringIO
4041
41from autopilot import platform42from autopilot import platform
@@ -43,6 +44,8 @@
43from autopilot.testcase import AutopilotTestCase44from autopilot.testcase import AutopilotTestCase
44from autopilot.tests.functional.fixtures import TempDesktopFile45from autopilot.tests.functional.fixtures import TempDesktopFile
45from autopilot.introspection import CustomEmulatorBase46from autopilot.introspection import CustomEmulatorBase
47from autopilot.introspection import _object_registry as object_registry
48from autopilot.introspection import _search
46from autopilot.introspection.qt import QtObjectProxyMixin49from autopilot.introspection.qt import QtObjectProxyMixin
47from autopilot.display import Display50from autopilot.display import Display
4851
@@ -99,7 +102,6 @@
99 Equals(WindowMockerApp)102 Equals(WindowMockerApp)
100 )103 )
101104
102 @skip("Currently fails due to lp:1425721 (and lp:1376996)")
103 def test_customised_proxy_classes_have_extension_classes(self):105 def test_customised_proxy_classes_have_extension_classes(self):
104 class WindowMockerApp(EmulatorBase):106 class WindowMockerApp(EmulatorBase):
105 @classmethod107 @classmethod
@@ -109,6 +111,49 @@
109 app = self.start_mock_app(EmulatorBase)111 app = self.start_mock_app(EmulatorBase)
110 self.assertThat(app.__class__.__bases__, Contains(QtObjectProxyMixin))112 self.assertThat(app.__class__.__bases__, Contains(QtObjectProxyMixin))
111113
114 def test_customised_proxy_classes_have_multiple_extension_classes(self):
115 with object_registry.patch_registry({}):
116 class SecondEmulatorBase(CustomEmulatorBase):
117 pass
118
119 class WindowMockerApp(EmulatorBase, SecondEmulatorBase):
120 @classmethod
121 def validate_dbus_object(cls, path, _state):
122 return path == b'/window-mocker'
123
124 app = self.start_mock_app(EmulatorBase)
125 self.assertThat(app.__class__.__bases__, Contains(EmulatorBase))
126 self.assertThat(
127 app.__class__.__bases__,
128 Contains(SecondEmulatorBase)
129 )
130
131 def test_handles_using_app_cpo_base_class(self):
132 # This test replicates an issue found in an application test suite
133 # where using the App CPO caused an exception.
134 with object_registry.patch_registry({}):
135 class WindowMockerApp(CustomEmulatorBase):
136 @classmethod
137 def validate_dbus_object(cls, path, _state):
138 return path == b'/window-mocker'
139
140 self.start_mock_app(WindowMockerApp)
141
142 def test_warns_when_using_incorrect_cpo_base_class(self):
143 # Ensure the warning method is called when launching a proxy.
144 with object_registry.patch_registry({}):
145 class TestCPO(CustomEmulatorBase):
146 pass
147
148 class WindowMockerApp(TestCPO):
149 @classmethod
150 def validate_dbus_object(cls, path, _state):
151 return path == b'/window-mocker'
152
153 with patch.object(_search, 'logger') as p_logger:
154 self.start_mock_app(WindowMockerApp)
155 self.assertTrue(p_logger.warning.called)
156
112 def test_can_select_custom_emulators_by_name(self):157 def test_can_select_custom_emulators_by_name(self):
113 """Must be able to select a custom emulator type by name."""158 """Must be able to select a custom emulator type by name."""
114 class MouseTestWidget(EmulatorBase):159 class MouseTestWidget(EmulatorBase):
115160
=== modified file 'autopilot/tests/unit/test_introspection_object_registry.py'
--- autopilot/tests/unit/test_introspection_object_registry.py 2014-07-31 04:42:08 +0000
+++ autopilot/tests/unit/test_introspection_object_registry.py 2015-05-06 10:03:54 +0000
@@ -1,7 +1,7 @@
1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-1# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2#2#
3# Autopilot Functional Test Tool3# Autopilot Functional Test Tool
4# Copyright (C) 2014 Canonical4# Copyright (C) 2014, 2015 Canonical
5#5#
6# This program is free software: you can redistribute it and/or modify6# 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 by7# it under the terms of the GNU General Public License as published by
@@ -263,3 +263,149 @@
263 with object_registry.patch_registry(dict()):263 with object_registry.patch_registry(dict()):
264 object_registry._object_registry[token] = token264 object_registry._object_registry[token] = token
265 self.assertFalse(token in object_registry._object_registry)265 self.assertFalse(token in object_registry._object_registry)
266
267
268class CombineBasesTests(TestCase):
269 def test_returns_original_if_no_extensions(self):
270 class Base():
271 pass
272
273 class Sub(Base):
274 pass
275
276 self.assertEqual(
277 object_registry._combine_base_and_extensions(Sub, ()),
278 (Base,)
279 )
280
281 def test_returns_addition_of_extension_classes(self):
282 class Base():
283 pass
284
285 class Sub(Base):
286 pass
287
288 class Ext1():
289 pass
290
291 class Ext2():
292 pass
293
294 mixed = object_registry._combine_base_and_extensions(Sub, (Ext1, Ext2))
295
296 self.assertIn(Ext1, mixed)
297 self.assertIn(Ext2, mixed)
298
299 def test_excludes_duplication_of_base_class_within_extension_classes(self):
300 class Base():
301 pass
302
303 class Sub(Base):
304 pass
305
306 class Ext1():
307 pass
308
309 class Ext2(Ext1):
310 pass
311
312 self.assertEqual(
313 object_registry._combine_base_and_extensions(Sub, (Ext2, Base)),
314 (Ext2, Base,)
315 )
316
317 def test_excludes_addition_of_extension_base_classes(self):
318 class Base():
319 pass
320
321 class Sub(Base):
322 pass
323
324 class Ext1():
325 pass
326
327 class Ext2(Ext1):
328 pass
329
330 self.assertNotIn(
331 object_registry._combine_base_and_extensions(Sub, (Ext2,)),
332 Ext1
333 )
334
335 def test_excludes_addition_of_subject_class(self):
336 class Base():
337 pass
338
339 class Sub(Base):
340 pass
341
342 class Ext1():
343 pass
344
345 self.assertEqual(
346 object_registry._combine_base_and_extensions(Sub, (Ext1, Sub)),
347 (Ext1, Base)
348 )
349
350 def test_maintains_mro_order(self):
351 class Base():
352 pass
353
354 class Sub1(Base):
355 pass
356
357 class Sub2(Sub1, Base):
358 pass
359
360 class Ext1():
361 pass
362
363 mixed = object_registry._combine_base_and_extensions(Sub2, (Ext1,))
364
365 self.assertLess(
366 mixed.index(Sub1),
367 mixed.index(Base)
368 )
369
370
371class MROSortOrderTests(TestCase):
372 def test_returns_lower_values_for_lower_classes_in_mro(self):
373 class Parent():
374 pass
375
376 class Child(Parent):
377 pass
378
379 class GrandChild(Child):
380 pass
381
382 self.assertGreater(
383 object_registry._get_mro_sort_order(Child),
384 object_registry._get_mro_sort_order(Parent),
385 )
386 self.assertGreater(
387 object_registry._get_mro_sort_order(GrandChild),
388 object_registry._get_mro_sort_order(Child),
389 )
390
391 def test_return_higher_values_for_promoted_collections_in_ties(self):
392 class Parent1():
393 pass
394
395 class Child1(Parent1):
396 pass
397
398 class Parent2():
399 pass
400
401 class Child2(Parent2):
402 pass
403
404 promoted_collection = (Parent1, Child1)
405
406 self.assertGreater(
407 object_registry._get_mro_sort_order(
408 Child1, promoted_collection),
409 object_registry._get_mro_sort_order(
410 Child2, promoted_collection),
411 )
266412
=== modified file 'autopilot/tests/unit/test_introspection_search.py'
--- autopilot/tests/unit/test_introspection_search.py 2015-04-02 08:27:42 +0000
+++ autopilot/tests/unit/test_introspection_search.py 2015-05-06 10:03:54 +0000
@@ -34,6 +34,8 @@
34from autopilot.exceptions import ProcessSearchError34from autopilot.exceptions import ProcessSearchError
35from autopilot.utilities import sleep35from autopilot.utilities import sleep
36from autopilot.introspection import _search as _s36from autopilot.introspection import _search as _s
37
38from autopilot.introspection import CustomEmulatorBase
37from autopilot.introspection.constants import AUTOPILOT_PATH39from autopilot.introspection.constants import AUTOPILOT_PATH
3840
3941
@@ -730,3 +732,95 @@
730 _s._find_matching_connections(bus, lambda *args: True)732 _s._find_matching_connections(bus, lambda *args: True)
731733
732 dedupe.assert_called_once_with(["conn1"], bus)734 dedupe.assert_called_once_with(["conn1"], bus)
735
736
737class ActualBaseClassTests(TestCase):
738
739 def test_dont_raise_passed_base_when_is_only_base(self):
740 class ActualBase(CustomEmulatorBase):
741 pass
742
743 try:
744 _s._raise_if_base_class_not_actually_base(ActualBase)
745 except ValueError:
746 self.fail('Unexpected ValueError exception')
747
748 def test_raises_if_passed_incorrect_base_class(self):
749 class ActualBase(CustomEmulatorBase):
750 pass
751
752 class InheritedCPO(ActualBase):
753 pass
754
755 self.assertRaises(
756 ValueError,
757 _s._raise_if_base_class_not_actually_base,
758 InheritedCPO
759 )
760
761 def test_raises_parent_with_simple_non_ap_multi_inheritance(self):
762 """When mixing in non-customproxy classes must return the base."""
763
764 class ActualBase(CustomEmulatorBase):
765 pass
766
767 class InheritedCPO(ActualBase):
768 pass
769
770 class TrickyOne(object):
771 pass
772
773 class FinalForm(InheritedCPO, TrickyOne):
774 pass
775
776 self.assertRaises(
777 ValueError,
778 _s._raise_if_base_class_not_actually_base,
779 FinalForm
780 )
781
782 def test_raises_parent_with_non_ap_multi_inheritance(self):
783
784 class ActualBase(CustomEmulatorBase):
785 pass
786
787 class InheritedCPO(ActualBase):
788 pass
789
790 class TrickyOne(object):
791 pass
792
793 class FinalForm(TrickyOne, InheritedCPO):
794 pass
795
796 self.assertRaises(
797 ValueError,
798 _s._raise_if_base_class_not_actually_base,
799 FinalForm
800 )
801
802 def test_dont_raise_when_using_default_emulator_base(self):
803 # _make_proxy_object potentially creates a default base.
804 DefaultBase = _s._make_default_emulator_base()
805 try:
806 _s._raise_if_base_class_not_actually_base(DefaultBase)
807 except ValueError:
808 self.fail('Unexpected ValueError exception')
809
810 def test_exception_message_contains_useful_information(self):
811 class ActualBase(CustomEmulatorBase):
812 pass
813
814 class InheritedCPO(ActualBase):
815 pass
816
817 try:
818 _s._raise_if_base_class_not_actually_base(InheritedCPO)
819 except ValueError as err:
820 self.assertEqual(
821 str(err),
822 _s.WRONG_CPO_CLASS_MSG.format(
823 passed=InheritedCPO,
824 actual=ActualBase
825 )
826 )
733827
=== removed file 'debian/autopilot-touch.dirs'
--- debian/autopilot-touch.dirs 2013-09-20 19:06:30 +0000
+++ debian/autopilot-touch.dirs 1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
1usr/share/autopilot-touch/apparmor
20
=== removed file 'debian/autopilot-touch.install'
--- debian/autopilot-touch.install 2013-09-20 19:06:30 +0000
+++ debian/autopilot-touch.install 1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
1apparmor/click.rules usr/share/autopilot-touch/apparmor
20
=== modified file 'debian/changelog'
--- debian/changelog 2015-02-26 21:20:18 +0000
+++ debian/changelog 2015-05-06 10:03:54 +0000
@@ -1,12 +1,19 @@
1autopilot (1.5.0) UNRELEASED; urgency=medium1autopilot (1.5.0) UNRELEASED; urgency=medium
22
3 [ Christopher Lee ]
3 * Fix for desktop file name change (lp:1411096)4 * Fix for desktop file name change (lp:1411096)
4 Fix for config value containing '=' char (lp:1408317)5 Fix for config value containing '=' char (lp:1408317)
5 Fix for skipped tests not appearing in log (lp:1414632)6 Fix for skipped tests not appearing in log (lp:1414632)
6 Add json docs build (for web publish) (lp:1409778)7 Add json docs build (for web publish) (lp:1409778)
7 Documentation improvements for API, Tutorial, FAQ, and Guidelines8 Documentation improvements for API, Tutorial, FAQ, and Guidelines
89
9 -- Christopher Lee <chris.lee@canonical.com> Fri, 27 Feb 2015 10:16:42 +130010 [ Vincent Ladeuil ]
11 * Remove apparmor/click.rules which is now shipped by dbus-property-service.
12
13 [ Federico Gimenez ]
14 * Fixed CPO class resolution (lp:1425721, lp:1376996)
15
16 -- Federico Gimenez <fgimenez@canonical.com> mon, 4 May 2015 12:12:42 +0200
1017
11autopilot (1.5.0+15.04.20141031-0ubuntu1) vivid; urgency=low18autopilot (1.5.0+15.04.20141031-0ubuntu1) vivid; urgency=low
1219
1320
=== modified file 'debian/rules' (properties changed: -x to +x)

Subscribers

People subscribed via source and target branches