Merge lp:~elopio/autopilot/no_uinput_side-effects into lp:autopilot

Proposed by Leo Arias
Status: Merged
Approved by: Thomi Richards
Approved revision: 455
Merged at revision: 439
Proposed branch: lp:~elopio/autopilot/no_uinput_side-effects
Merge into: lp:autopilot
Prerequisite: lp:~thomir-deactivatedaccount/autopilot/trunk-add-repr-compatibility
Diff against target: 1364 lines (+932/-213)
5 files modified
autopilot/input/_common.py (+12/-2)
autopilot/input/_uinput.py (+329/-203)
autopilot/tests/functional/test_input_stack.py (+6/-5)
autopilot/tests/unit/test_input.py (+583/-3)
debian/control (+2/-0)
To merge this branch: bzr merge lp:~elopio/autopilot/no_uinput_side-effects
Reviewer Review Type Date Requested Status
PS Jenkins bot continuous-integration Needs Fixing
VĂ­ctor R. Ruiz Pending
Autopilot Hackers Pending
Richard Huddie Pending
Christopher Lee Pending
Javier Collado Pending
Review via email: mp+207280@code.launchpad.net

This proposal supersedes a proposal from 2014-01-21.

Commit message

Refactor the _uinput module to avoid side-effects when we import it.

Description of the change

This is the first part of https://code.launchpad.net/~elopio/autopilot/no_uinput_side-effects2/+merge/202564
We need both to have all the tests passing, but together it's more than 1000 lines.
So, I suggest to review them separately, then merge them and confirm all the tests are passing on jenkins.
This one contains the refactors for the Keyboard, the other one for the Touch.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Leo Arias (elopio) wrote : Posted in a previous version of this proposal

I split the branch in two, to make it easier to review.
Here are the Touch refactors:
https://code.launchpad.net/~elopio/autopilot/no_uinput_side-effects2/+merge/202414

Revision history for this message
Christopher Lee (veebers) wrote : Posted in a previous version of this proposal

Hi,

First off I'm unable to build the package as the tests don't pass (see
[1]). I'm not sure if this is due to the merge being split into two,
if it is can you please either make it so the MR stands on itis own
and can be built/tests run or re-propose with both merges in one (I
don't mind large MRs if a bunch of the lines are tests).

What's the purpose of changing 'patch' -> 'mock.patch'?
One (admittedly weak) reason to keep it as it was is that all the
tests use just 'patch' so it breaks from the current standard.
Another reason is that it add extraneous text around the test without
really adding to it. It doesn't help clarify what the test does or
expected result is etc.

Diff Line 58, 81: What's the rational behind using local method
variables to name these values as opposed to Class variables?

WRT the tests (diff lines 242-401)
Creating self.keyboard within setUp:
  Tests should be explicit so you should be able to look at a test and
  see what's happening (or at least intended). I think you need to be
  more specific with creating the keyboard (especially since that's
  what's being tested)

  I would suggest something like:

  def test_example(self):
      keyboard = self._create_UIKeyboard_with_mocked_backend()
      keyboard.press . . .

  or something similar.

Minor nit-pick: between diff lines 253 and 269 there is helper methods
mixed in between test methods.

[1] http://pastebin.ubuntu.com/6794531/

review: Needs Fixing
Revision history for this message
Christopher Lee (veebers) wrote : Posted in a previous version of this proposal

Oops, hit enter too soon, corrections below.

> (I
> don't mind large MRs if a bunch of the lines are tests).

Should read:
(I don't mind if the MR is a little larger, it should be as small as
possible but currently it looks like it's a little too small if it
doesn't include all the details for the tests too.)

> Minor nit-pick: between diff lines 253 and 269 there is helper methods
> mixed in between test methods.

Should read:
Diff lines 253 and 269 Helper methods:
  The helper methods should be public (shouldn't start with '_')

  They are also mixed in between test methods, they should be at the
  top of the class with the test methods following.

Revision history for this message
Leo Arias (elopio) wrote : Posted in a previous version of this proposal

> First off I'm unable to build the package as the tests don't pass (see
> [1]). I'm not sure if this is due to the merge being split into two,
> if it is can you please either make it so the MR stands on itis own
> and can be built/tests run or re-propose with both merges in one (I
> don't mind large MRs if a bunch of the lines are tests).

Yes, if you take a look at the other MP, you will be able to build the package as all the tests are passing there.
But I'm going to change some things based on your comments and merge them.

> What's the purpose of changing 'patch' -> 'mock.patch'?
> One (admittedly weak) reason to keep it as it was is that all the
> tests use just 'patch' so it breaks from the current standard.
> Another reason is that it add extraneous text around the test without
> really adding to it. It doesn't help clarify what the test does or
> expected result is etc.

I have some reasons to prefer this. It maintains the imports section cleaner, as I just have to import mock instead of all the classes I use from that module. And, when reading the code, it's easier to understand where the things come from. A good example is mock.call. Without the context that provides the module, it's confusing when you find in the code call(4), or call.write(4). We discussed about this some months ago too, and there are some also weak reasons to support this style, like the way import works on symbols instead of modules. In the end, I'm just used to this and I prefer it.
Your reason about keeping the current standard makes sense, so if you prefer it that way, I'll change it.

> Diff Line 58, 81: What's the rational behind using local method
> variables to name these values as opposed to Class variables?

I think that the definitions need to be as close to code it's using them as possible, and the scope for everything should be as small as possible. Here I'm just defining the variable to make it more readable, and it's being used only on the following line, so I prefer to keep the definition and the use next to each other.

> WRT the tests (diff lines 242-401)
> Creating self.keyboard within setUp:
> Tests should be explicit so you should be able to look at a test and
> see what's happening (or at least intended). I think you need to be
> more specific with creating the keyboard (especially since that's
> what's being tested)
>
> I would suggest something like:
>
> def test_example(self):
> keyboard = self._create_UIKeyboard_with_mocked_backend()
> keyboard.press . . .
>
> or something similar.

Yes, I'll change that.

Revision history for this message
Leo Arias (elopio) wrote : Posted in a previous version of this proposal

> Oops, hit enter too soon, corrections below.
>
> > (I
> > don't mind large MRs if a bunch of the lines are tests).
>
> Should read:
> (I don't mind if the MR is a little larger, it should be as small as
> possible but currently it looks like it's a little too small if it
> doesn't include all the details for the tests too.)

Ok, and you already did a first review to this branch, so I'm merging them.

> > Minor nit-pick: between diff lines 253 and 269 there is helper methods
> > mixed in between test methods.
>
> Should read:
> Diff lines 253 and 269 Helper methods:
> The helper methods should be public (shouldn't start with '_')

Why should they be public if they are not going to be used outside of this test class?

> They are also mixed in between test methods, they should be at the
> top of the class with the test methods following.

According to the clean code book, the code should be read from top to bottom. So when you are reading method_a, and it uses method_b, the definition of method_b should come right after the definition of method_a.
That's really useful when you are reading the code of a class for the first time, because what you will find first is the highest level method, followed by all the methods needed to implement it.
When you are done reading the details of that first high level method, you will find a second high level method followed by all its helpers.
If you don't want to read all the lower level details of the implementation, you can just read the high level public ones, because it's easy to notice which ones are their helpers, and the helpers hopefully have a clear and nice name.
Also, it keeps the vertical distance of code definitions as close as possible to their use. So if you are new to a class and you try to understand a method, you know you will find all its helpers close.
So, when I'm writing test classes, I try to make them readable from top to bottom, so you don't have to make many vertical jumps to understand the details of the tests and their helpers.

Having said aaaall of that, if on the tests classes you still prefer to have the helpers before the test methods, I can change it. I prefer it this way, but I can change it easily.

Thanks for the review :)

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
Leo Arias (elopio) wrote : Posted in a previous version of this proposal
Download full text (6.8 KiB)

<veebers> elopio: to cover the comments: I wouldn't approve an MR that I couldn't build and run the tests for (even if the second half is just there) too much can go wrong.
<thomi> veebers: no only shouldn't you, it won't land if the tests don't pass
<elopio> veebers: last jenkins run was green. But now it's a 1000 MP :)
<thomi> (just thought I'd jump in there)
<veebers> elopio: I understand the import policy wrt to import mock vs from mock . . . But I think in this case it clutters the tests, which is really what this file is about. I feel test suites are special cases in some cases
<veebers> thomi: that's right, but the first thing I do is branch, build and run the tests
<veebers> so you normally wouldn't get a review either
<veebers> :-)
<elopio> veebers: ok, I'll change the imports.
<thomi> veebers: :)
<veebers> elopio: it does make a good point if you're calling "call.write(4)" for instance having mock.call.write is much more understandable
<elopio> veebers: yes, I don't agree with you here, but my policy is that if I can't convince my reviewer, I'll follow his suggestions and continue discussing later.
<elopio> we have a good chance to discuss over some beers next month :)
<veebers> elopio: aye sounds good
<elopio> veebers: pushed the imports change.
<elopio> oh, I did it too with the sleep and didn't notice.
<elopio> changing...
<veebers> elopio: ugh sorry, where was I
<veebers> elopio: the mixing of test and helper; Comes back to test suites being special cases and understanding the tests, being able to see all the tests that are there as well as the helper methods being secondary to anything
<elopio> veebers: ok, give me a couple of minutes for that.
<veebers> elopio: ack
<veebers> elopio: fyi, you can also use sleep as a context manager (i.e. "with sleep.mocked():") to move the mocking of sleep closer to the code that will use it
<elopio> veebers: pushed. I don't agree with you on the order of the methods, but I can live with it :)
<elopio> veebers: if I want all my tests to use the mocked sleep, I think it's cleaner to do it on the setup
<elopio> instead of using the context manager on every test.
<veebers> thomi: fyi, I think this issue is resolved now, but will create a vm/container to souble check
<veebers> double*
<veebers> elopio: heh sounds good :-)
<veebers> elopio: so I realise that your MR will break a couple of Unity8 tests, but that's my bad for checking input internals in a test :-\ So I'll sort the Unity8 tests
<veebers> is there a test if release() is called twice? Or if release is called when it's already released?
<veebers> (should call Runtime error I think from memory)
<veebers> Also, the public interface for Touch should state any exceptions it might raise (release, in this case -> RuntimeError)
<elopio> veebers: actually, the bug that caused your unity8 workaround is fixed.
<veebers> elopio: ah no, I'm talking about something else
<veebers> elopio: but also, awesome!
<veebers> I was wondering if this helped with that issue. Excellent
<veebers> I'm talking about code like this grossness: if self.touch._touch_finger is not None:
<elopio> veebers: this doesn't help that issue. They fixed it in mir.
<elopio> well, t...

Read more...

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
Thomi Richards (thomir-deactivatedaccount) wrote : Posted in a previous version of this proposal

Hi,

I'll jump in here :)

On Wed, Jan 22, 2014 at 4:05 PM, Leo Arias <email address hidden> wrote:

> > Should read:
> > Diff lines 253 and 269 Helper methods:
> > The helper methods should be public (shouldn't start with '_')
>
> Why should they be public if they are not going to be used outside of this
> test class?
>
>
They're assertion methods, and there's really no need to make them private.
I can easily imagine you might want to re-use them in the future. Making
them private has no benefits here, and has several drawbacks: they're less
visible, and it makes them look like methods that you don't want to be
reused. We're not exporting an API from test suites, so some slightly
different rules apply. If this were production code, I'd argue that you
make them private... but it's not, so I won't :)

> > They are also mixed in between test methods, they should be at the
> > top of the class with the test methods following.
>
> According to the clean code book, the code should be read from top to
> bottom. So when you are reading method_a, and it uses method_b, the
> definition of method_b should come right after the definition of method_a.
>

In production code, I totally agree with you. However, when reading a test
suite, it's helpful to be able to see all the helper functions together -
either at the top or the bottom of the class (I don't mind which). For
example, if I have a class like:

class FooTests(TestCase):

    def test_something(self):
        self.assert_widget_is_full(make_widget())

It's a pretty good bet that when I read this code next, one of the first
things I'll want to do is to satisfy myself that the
'assert_widget_is_full' method does what it says on the tin. Where do I go
to find that? You can't put it immeadiately below every test that uses
it.... so it makes more sense to either put it at the top of the class
(which means you'll scan it on your way to the test), or at the bottom of
the class.

> That's really useful when you are reading the code of a class for the
> first time, because what you will find first is the highest level method,
> followed by all the methods needed to implement it.
>

For production code, I entirely agree with you, but for tests, it's
unlikely that you'll be reading the first test case in the class. If you
start by reding the 7th test (for example), which way do you scroll to find
the assertion methods? :)

>
> Having said aaaall of that, if on the tests classes you still prefer to
> have the helpers before the test methods, I can change it. I prefer it this
> way, but I can change it easily.

Sorry to be so insistent, but please do - it'll make me happy :)

--
Thomi Richards
<email address hidden>

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
Christopher Lee (veebers) wrote : Posted in a previous version of this proposal

Hi Leo, thanks for making those changes. I only have a couple more :-)

Most important is line: 302 will raise an exception as _get_touch_events isn't
passed anything (I presume res_x/res_y are the intended vars.)

line 213: It is possible (not likely, but possible) that cls._device will None,
please make a check here.
(The on_test_end stuff should be going away in the near future as it's a little
hacky, but is here to stay for now.)

Can you please add a test for this and make the fix? Also, if you could run
this over the Unity8 tests to ensure there are no other regressions (due to
there not being 100% coverage of this module).

Line 249: Can you expand on the deprecated message please? Something like "the
Touch class to instantiate a device object" or similar?
(Currently the deprecated method outputs this string: "This function is
deprecated. Please use '%(your_string_here)s' instead.")

lines 401 - 404: The docstring appears to contain details intended for the
__init__ method.

line 410: Minor: It seems inconstant to me to have this number stored as a
class variable where previously (i.e. press_value line 975) has been used
locally. I'm not really suggesting a fix, just noticing it.

line 547-548: raises details: get_center_point can also raise exceptions,
either add them to the docstring or easier, add a mention re: get_center_point
and it's docs.

574,586: the word 'touch' should probably be '"finger"' in the docstring
(":raises RuntimeError: if the touch is not pressed." )

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
Leo Arias (elopio) wrote : Posted in a previous version of this proposal

> Most important is line: 302 will raise an exception as _get_touch_events isn't
> passed anything (I presume res_x/res_y are the intended vars.)

Yes, my bad. I added a tests for get_touch_events.

> line 213: It is possible (not likely, but possible) that cls._device will
> None,
> please make a check here.
> (The on_test_end stuff should be going away in the near future as it's a
> little
> hacky, but is here to stay for now.)

Of course, added with tests.
>
> Can you please add a test for this and make the fix? Also, if you could run
> this over the Unity8 tests to ensure there are no other regressions (due to
> there not being 100% coverage of this module).

I'm trying to get unity8 tests to run with this branch now. I'll let you know the results.

> Line 249: Can you expand on the deprecated message please? Something like "the
> Touch class to instantiate a device object" or similar?
> (Currently the deprecated method outputs this string: "This function is
> deprecated. Please use '%(your_string_here)s' instead.")

Done.

> lines 401 - 404: The docstring appears to contain details intended for the
> __init__ method.

Right, sorry. Done.

> line 410: Minor: It seems inconstant to me to have this number stored as a
> class variable where previously (i.e. press_value line 975) has been used
> locally. I'm not really suggesting a fix, just noticing it.

I moved it closer to where it's being used.

> line 547-548: raises details: get_center_point can also raise exceptions,
> either add them to the docstring or easier, add a mention re: get_center_point
> and it's docs.

Done.

> 574,586: the word 'touch' should probably be '"finger"' in the docstring
> (":raises RuntimeError: if the touch is not pressed." )

Done.

Thanks for the suggestions.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
Leo Arias (elopio) wrote : Posted in a previous version of this proposal

Here is the branch that updates unity:

https://code.launchpad.net/~elopio/unity8/no_uinput_side-effects/+merge/203876

I wanted to clean it more using only the public methods, but that needs this branch:
https://code.launchpad.net/~elopio/autopilot/fix1266601-Pointer-pressed-move/+merge/202206

Which in turn needs this refactor to land.
So for the moment it will be ugly, and when things start to get published, I'll clean it up.

Revision history for this message
Christopher Lee (veebers) wrote : Posted in a previous version of this proposal

Hi Leo,

Awesome, looking good.

Only one minor thing to ask of you:
Please add a test for L275 to ensure the deprecated message is output.
(An example can be found within
test_using_pick_app_launcher_produces_deprecation_message here:
https://code.launchpad.net/~veebers/autopilot/fix_deprecate_pick_app_launcher/+merge/202784)

Once this is done I'll be more than happy to approve.

Thanks again.

review: Needs Fixing
Revision history for this message
Leo Arias (elopio) wrote : Posted in a previous version of this proposal

Done.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote : Posted in a previous version of this proposal
review: Approve (continuous-integration)
Revision history for this message
Christopher Lee (veebers) wrote : Posted in a previous version of this proposal

Awesome, LGTM

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'autopilot/input/_common.py'
2--- autopilot/input/_common.py 2013-12-10 03:10:11 +0000
3+++ autopilot/input/_common.py 2014-02-19 18:32:31 +0000
4@@ -26,8 +26,18 @@
5
6
7 def get_center_point(object_proxy):
8- """Get the center point of an object, searching for several different ways
9- of determining exactly where the center is.
10+ """Get the center point of an object.
11+
12+ It searches for several different ways of determining exactly where the
13+ center is.
14+
15+ :raises ValueError: if `object_proxy` has the globalRect attribute but it
16+ is not of the correct type.
17+ :raises ValueError: if `object_proxy` doesn't have the globalRect
18+ attribute, it has the x and y attributes instead, but they are not of
19+ the correct type.
20+ :raises ValueError: if `object_proxy` doesn't have any recognised position
21+ attributes.
22
23 """
24 try:
25
26=== modified file 'autopilot/input/_uinput.py'
27--- autopilot/input/_uinput.py 2013-11-07 05:53:36 +0000
28+++ autopilot/input/_uinput.py 2014-02-19 18:32:31 +0000
29@@ -1,7 +1,7 @@
30 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
31 #
32 # Autopilot Functional Test Tool
33-# Copyright (C) 2012-2013 Canonical
34+# Copyright (C) 2012, 2013, 2014 Canonical
35 #
36 # This program is free software: you can redistribute it and/or modify
37 # it under the terms of the GNU General Public License as published by
38@@ -17,27 +17,23 @@
39 # along with this program. If not, see <http://www.gnu.org/licenses/>.
40 #
41
42-
43 """UInput device drivers."""
44
45+import logging
46+import os.path
47+
48+import six
49+from evdev import UInput, ecodes as e
50+
51+import autopilot.platform
52 from autopilot.input import Keyboard as KeyboardBase
53 from autopilot.input import Touch as TouchBase
54 from autopilot.input._common import get_center_point
55-from autopilot.utilities import sleep
56-import autopilot.platform
57+from autopilot.utilities import deprecated, sleep
58
59-import logging
60-from evdev import UInput, ecodes as e
61-import os.path
62-import six
63
64 logger = logging.getLogger(__name__)
65
66-PRESS = 1
67-RELEASE = 0
68-
69-_PRESSED_KEYS = []
70-
71
72 def _get_devnode_path():
73 """Provide a fallback uinput node for devices which don't support udev"""
74@@ -47,13 +43,78 @@
75 return devnode
76
77
78+class _UInputKeyboardDevice(object):
79+ """Wrapper for the UInput Keyboard to execute its primitives."""
80+
81+ def __init__(self, device_class=UInput):
82+ super(_UInputKeyboardDevice, self).__init__()
83+ self._device = device_class(devnode=_get_devnode_path())
84+ self._pressed_keys_ecodes = []
85+
86+ def press(self, key):
87+ """Press one key button.
88+
89+ It ignores case, so, for example, 'a' and 'A' are mapped to the same
90+ key.
91+
92+ """
93+ ecode = self._get_ecode_for_key(key)
94+ logger.debug('Pressing %s (%r).', key, ecode)
95+ self._emit_press_event(ecode)
96+ self._pressed_keys_ecodes.append(ecode)
97+
98+ def _get_ecode_for_key(self, key):
99+ key_name = key if key.startswith('KEY_') else 'KEY_' + key
100+ key_name = key_name.upper()
101+ ecode = e.ecodes.get(key_name, None)
102+ if ecode is None:
103+ raise ValueError('Unknown key name: %s.' % key)
104+ return ecode
105+
106+ def _emit_press_event(self, ecode):
107+ press_value = 1
108+ self._emit(ecode, press_value)
109+
110+ def _emit(self, ecode, value):
111+ self._device.write(e.EV_KEY, ecode, value)
112+ self._device.syn()
113+
114+ def release(self, key):
115+ """Release one key button.
116+
117+ It ignores case, so, for example, 'a' and 'A' are mapped to the same
118+ key.
119+
120+ :raises ValueError: if ``key`` is not pressed.
121+
122+ """
123+ ecode = self._get_ecode_for_key(key)
124+ if ecode in self._pressed_keys_ecodes:
125+ logger.debug('Releasing %s (%r).', key, ecode)
126+ self._emit_release_event(ecode)
127+ self._pressed_keys_ecodes.remove(ecode)
128+ else:
129+ raise ValueError('Key %r not pressed.' % key)
130+
131+ def _emit_release_event(self, ecode):
132+ release_value = 0
133+ self._emit(ecode, release_value)
134+
135+ def release_pressed_keys(self):
136+ """Release all the keys that are currently pressed."""
137+ for ecode in self._pressed_keys_ecodes:
138+ self._emit_release_event(ecode)
139+ self._pressed_keys_ecodes = []
140+
141+
142 class Keyboard(KeyboardBase):
143
144- _device = UInput(devnode=_get_devnode_path())
145+ _device = None
146
147- def _emit(self, event, value):
148- Keyboard._device.write(e.EV_KEY, event, value)
149- Keyboard._device.syn()
150+ def __init__(self, device_class=_UInputKeyboardDevice):
151+ super(Keyboard, self).__init__()
152+ if Keyboard._device is None:
153+ Keyboard._device = device_class()
154
155 def _sanitise_keys(self, keys):
156 if keys == '+':
157@@ -71,15 +132,15 @@
158
159 presses the 'Alt' and 'F2' keys.
160
161+ :raises TypeError: if ``keys`` is not a string.
162+
163 """
164 if not isinstance(keys, six.string_types):
165 raise TypeError("'keys' argument must be a string.")
166
167 for key in self._sanitise_keys(keys):
168- for event in Keyboard._get_events_for_key(key):
169- logger.debug("Pressing %s (%r)", key, event)
170- _PRESSED_KEYS.append(event)
171- self._emit(event, PRESS)
172+ for key_button in self._get_key_buttons(key):
173+ self._device.press(key_button)
174 sleep(delay)
175
176 def release(self, keys, delay=0.1):
177@@ -94,16 +155,16 @@
178
179 Keys are released in the reverse order in which they are specified.
180
181+ :raises TypeError: if ``keys`` is not a string.
182+ :raises ValueError: if one of the keys to be released is not pressed.
183+
184 """
185 if not isinstance(keys, six.string_types):
186 raise TypeError("'keys' argument must be a string.")
187
188 for key in reversed(self._sanitise_keys(keys)):
189- for event in Keyboard._get_events_for_key(key):
190- logger.debug("Releasing %s (%r)", key, event)
191- if event in _PRESSED_KEYS:
192- _PRESSED_KEYS.remove(event)
193- self._emit(event, RELEASE)
194+ for key_button in reversed(self._get_key_buttons(key)):
195+ self._device.release(key_button)
196 sleep(delay)
197
198 def press_and_release(self, keys, delay=0.1):
199@@ -118,6 +179,8 @@
200
201 presses both the 'Alt' and 'F2' keys, and then releases both keys.
202
203+ :raises TypeError: if ``keys`` is not a string.
204+
205 """
206 logger.debug("Pressing and Releasing: %s", keys)
207 self.press(keys, delay)
208@@ -129,6 +192,8 @@
209 Only 'normal' keys can be typed with this method. Control characters
210 (such as 'Alt' will be interpreted as an 'A', and 'l', and a 't').
211
212+ :raises TypeError: if ``keys`` is not a string.
213+
214 """
215 if not isinstance(string, six.string_types):
216 raise TypeError("'keys' argument must be a string.")
217@@ -145,98 +210,35 @@
218 any keys that were pressed and not released.
219
220 """
221- global _PRESSED_KEYS
222- if len(_PRESSED_KEYS) == 0:
223- return
224-
225- def _release(event):
226- Keyboard._device.write(e.EV_KEY, event, RELEASE)
227- Keyboard._device.syn()
228- for event in _PRESSED_KEYS:
229- logger.warning("Releasing key %r as part of cleanup call.", event)
230- _release(event)
231- _PRESSED_KEYS = []
232-
233- @staticmethod
234- def _get_events_for_key(key):
235- """Return a list of events required to generate 'key' as an input.
236-
237- Multiple keys will be returned when the key specified requires more
238+ if cls._device is not None:
239+ cls._device.release_pressed_keys()
240+
241+ def _get_key_buttons(self, key):
242+ """Return a list of the key buttons required to press.
243+
244+ Multiple buttons will be returned when the key specified requires more
245 than one keypress to generate (for example, upper-case letters).
246
247 """
248- events = []
249+ key_buttons = []
250 if key.isupper() or key in _SHIFTED_KEYS:
251- events.append(e.KEY_LEFTSHIFT)
252- keyname = _UINPUT_CODE_TRANSLATIONS.get(key.upper(), key)
253- evt = getattr(e, 'KEY_' + keyname.upper(), None)
254- if evt is None:
255- raise ValueError("Unknown key name: '%s'" % key)
256- events.append(evt)
257- return events
258-
259-
260-last_tracking_id = 0
261-
262-
263-def get_next_tracking_id():
264- global last_tracking_id
265- last_tracking_id += 1
266- return last_tracking_id
267-
268-
269+ key_buttons.append('KEY_LEFTSHIFT')
270+ key_name = _UINPUT_CODE_TRANSLATIONS.get(key.upper(), key)
271+ key_buttons.append(key_name)
272+ return key_buttons
273+
274+
275+@deprecated('the Touch class to instantiate a device object')
276 def create_touch_device(res_x=None, res_y=None):
277 """Create and return a UInput touch device.
278
279 If res_x and res_y are not specified, they will be queried from the system.
280
281 """
282-
283- if res_x is None or res_y is None:
284- from autopilot.display import Display
285- display = Display.create()
286- # TODO: This calculation needs to become part of the display module:
287- l = r = t = b = 0
288- for screen in range(display.get_num_screens()):
289- geometry = display.get_screen_geometry(screen)
290- if geometry[0] < l:
291- l = geometry[0]
292- if geometry[1] < t:
293- t = geometry[1]
294- if geometry[0] + geometry[2] > r:
295- r = geometry[0] + geometry[2]
296- if geometry[1] + geometry[3] > b:
297- b = geometry[1] + geometry[3]
298- res_x = r - l
299- res_y = b - t
300-
301- # android uses BTN_TOOL_FINGER, whereas desktop uses BTN_TOUCH. I have no
302- # idea why...
303- touch_tool = e.BTN_TOOL_FINGER
304- if autopilot.platform.model() == 'Desktop':
305- touch_tool = e.BTN_TOUCH
306-
307- cap_mt = {
308- e.EV_ABS: [
309- (e.ABS_X, (0, res_x, 0, 0)),
310- (e.ABS_Y, (0, res_y, 0, 0)),
311- (e.ABS_PRESSURE, (0, 65535, 0, 0)),
312- (e.ABS_MT_POSITION_X, (0, res_x, 0, 0)),
313- (e.ABS_MT_POSITION_Y, (0, res_y, 0, 0)),
314- (e.ABS_MT_TOUCH_MAJOR, (0, 30, 0, 0)),
315- (e.ABS_MT_TRACKING_ID, (0, 65535, 0, 0)),
316- (e.ABS_MT_PRESSURE, (0, 255, 0, 0)),
317- (e.ABS_MT_SLOT, (0, 9, 0, 0)),
318- ],
319- e.EV_KEY: [
320- touch_tool,
321- ]
322- }
323-
324- return UInput(cap_mt, name='autopilot-finger', version=0x2,
325- devnode=_get_devnode_path())
326-
327-_touch_device = create_touch_device()
328+ return UInput(events=_get_touch_events(res_x, res_y),
329+ name='autopilot-finger',
330+ version=0x2, devnode=_get_devnode_path())
331+
332
333 # Multiouch notes:
334 # ----------------
335@@ -281,90 +283,253 @@
336 # about this is that the SLOT refers to a finger number, and the TRACKING_ID
337 # identifies a unique touch for the duration of it's existance.
338
339-_touch_fingers_in_use = []
340-
341-
342-def _get_touch_finger():
343- """Claim a touch finger id for use.
344-
345- :raises: RuntimeError if no more fingers are available.
346-
347- """
348- global _touch_fingers_in_use
349-
350- for i in range(9):
351- if i not in _touch_fingers_in_use:
352- _touch_fingers_in_use.append(i)
353- return i
354- raise RuntimeError("All available fingers have been used already.")
355-
356-
357-def _release_touch_finger(finger_num):
358- """Relase a previously-claimed finger id.
359-
360- :raises: RuntimeError if the finger given was never claimed, or was already
361- released.
362-
363- """
364- global _touch_fingers_in_use
365-
366- if finger_num not in _touch_fingers_in_use:
367- raise RuntimeError(
368- "Finger %d was never claimed, or has already been released." %
369- (finger_num))
370- _touch_fingers_in_use.remove(finger_num)
371- assert(finger_num not in _touch_fingers_in_use)
372+
373+def _get_touch_events(res_x=None, res_y=None):
374+ if res_x is None or res_y is None:
375+ res_x, res_y = _get_system_resolution()
376+
377+ touch_tool = _get_touch_tool()
378+
379+ events = {
380+ e.EV_ABS: [
381+ (e.ABS_X, (0, res_x, 0, 0)),
382+ (e.ABS_Y, (0, res_y, 0, 0)),
383+ (e.ABS_PRESSURE, (0, 65535, 0, 0)),
384+ (e.ABS_MT_POSITION_X, (0, res_x, 0, 0)),
385+ (e.ABS_MT_POSITION_Y, (0, res_y, 0, 0)),
386+ (e.ABS_MT_TOUCH_MAJOR, (0, 30, 0, 0)),
387+ (e.ABS_MT_TRACKING_ID, (0, 65535, 0, 0)),
388+ (e.ABS_MT_PRESSURE, (0, 255, 0, 0)),
389+ (e.ABS_MT_SLOT, (0, 9, 0, 0)),
390+ ],
391+ e.EV_KEY: [
392+ touch_tool,
393+ ]
394+ }
395+ return events
396+
397+
398+def _get_system_resolution():
399+ from autopilot.display import Display
400+ display = Display.create()
401+ # TODO: This calculation needs to become part of the display module:
402+ l = r = t = b = 0
403+ for screen in range(display.get_num_screens()):
404+ geometry = display.get_screen_geometry(screen)
405+ if geometry[0] < l:
406+ l = geometry[0]
407+ if geometry[1] < t:
408+ t = geometry[1]
409+ if geometry[0] + geometry[2] > r:
410+ r = geometry[0] + geometry[2]
411+ if geometry[1] + geometry[3] > b:
412+ b = geometry[1] + geometry[3]
413+ res_x = r - l
414+ res_y = b - t
415+ return res_x, res_y
416+
417+
418+def _get_touch_tool():
419+ # android uses BTN_TOOL_FINGER, whereas desktop uses BTN_TOUCH. I have
420+ # no idea why...
421+ if autopilot.platform.model() == 'Desktop':
422+ touch_tool = e.BTN_TOUCH
423+ else:
424+ touch_tool = e.BTN_TOOL_FINGER
425+ return touch_tool
426+
427+
428+class _UInputTouchDevice(object):
429+ """Wrapper for the UInput Touch to execute its primitives."""
430+
431+ _device = None
432+ _touch_fingers_in_use = []
433+ _last_tracking_id = 0
434+
435+ def __init__(self, res_x=None, res_y=None, device_class=UInput):
436+ """Class constructor.
437+
438+ If res_x and res_y are not specified, they will be queried from the
439+ system.
440+
441+ """
442+ super(_UInputTouchDevice, self).__init__()
443+ if _UInputTouchDevice._device is None:
444+ _UInputTouchDevice._device = device_class(
445+ events=_get_touch_events(res_x, res_y),
446+ name='autopilot-finger',
447+ version=0x2, devnode=_get_devnode_path())
448+ self._touch_finger_slot = None
449+
450+ @property
451+ def pressed(self):
452+ return self._touch_finger_slot is not None
453+
454+ def finger_down(self, x, y):
455+ """Internal: moves finger "finger" down on the touchscreen.
456+
457+ :param x: The finger will be moved to this x coordinate.
458+ :param y: The finger will be moved to this y coordinate.
459+
460+ :raises RuntimeError: if the finger is already pressed.
461+ :raises RuntimeError: if no more touch slots are available.
462+
463+ """
464+ if self.pressed:
465+ raise RuntimeError("Cannot press finger: it's already pressed.")
466+ self._touch_finger_slot = self._get_free_touch_finger_slot()
467+
468+ self._device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger_slot)
469+ self._device.write(
470+ e.EV_ABS, e.ABS_MT_TRACKING_ID, self._get_next_tracking_id())
471+ press_value = 1
472+ self._device.write(e.EV_KEY, e.BTN_TOOL_FINGER, press_value)
473+ self._device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
474+ self._device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
475+ self._device.write(e.EV_ABS, e.ABS_MT_PRESSURE, 400)
476+ self._device.syn()
477+
478+ def _get_free_touch_finger_slot(self):
479+ """Return the id of a free touch finger.
480+
481+ :raises RuntimeError: if no more touch slots are available.
482+
483+ """
484+ max_number_of_fingers = 9
485+ for i in range(max_number_of_fingers):
486+ if i not in _UInputTouchDevice._touch_fingers_in_use:
487+ _UInputTouchDevice._touch_fingers_in_use.append(i)
488+ return i
489+ raise RuntimeError('All available fingers have been used already.')
490+
491+ def _get_next_tracking_id(self):
492+ _UInputTouchDevice._last_tracking_id += 1
493+ return _UInputTouchDevice._last_tracking_id
494+
495+ def finger_move(self, x, y):
496+ """Internal: moves finger "finger" on the touchscreen to pos (x,y)
497+
498+ NOTE: The finger has to be down for this to have any effect.
499+
500+ :raises RuntimeError: if the finger is not pressed.
501+
502+ """
503+ if not self.pressed:
504+ raise RuntimeError('Attempting to move without finger being down.')
505+ self._device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger_slot)
506+ self._device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
507+ self._device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
508+ self._device.syn()
509+
510+ def finger_up(self):
511+ """Internal: moves finger "finger" up from the touchscreen
512+
513+ :raises RuntimeError: if the finger is not pressed.
514+
515+ """
516+ if not self.pressed:
517+ raise RuntimeError("Cannot release finger: it's not pressed.")
518+ self._device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger_slot)
519+ lift_tracking_id = -1
520+ self._device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, lift_tracking_id)
521+ release_value = 0
522+ self._device.write(e.EV_KEY, e.BTN_TOOL_FINGER, release_value)
523+ self._device.syn()
524+ self._release_touch_finger()
525+
526+ def _release_touch_finger(self):
527+ """Release the touch finger.
528+
529+ :raises RuntimeError: if the finger was not claimed before or was
530+ already released.
531+
532+ """
533+ if (self._touch_finger_slot not in
534+ _UInputTouchDevice._touch_fingers_in_use):
535+ raise RuntimeError(
536+ "Finger %d was never claimed, or has already been released." %
537+ self._touch_finger_slot)
538+ _UInputTouchDevice._touch_fingers_in_use.remove(
539+ self._touch_finger_slot)
540+ self._touch_finger_slot = None
541
542
543 class Touch(TouchBase):
544 """Low level interface to generate single finger touch events."""
545
546- def __init__(self):
547+ def __init__(self, device_class=_UInputTouchDevice):
548 super(Touch, self).__init__()
549- self._touch_finger = None
550+ self._device = device_class()
551
552 @property
553 def pressed(self):
554- return self._touch_finger is not None
555+ return self._device.pressed
556
557 def tap(self, x, y):
558- """Click (or 'tap') at given x and y coordinates."""
559+ """Click (or 'tap') at given x and y coordinates.
560+
561+ :raises RuntimeError: if the finger is already pressed.
562+ :raises RuntimeError: if no more finger slots are available.
563+
564+ """
565 logger.debug("Tapping at: %d,%d", x, y)
566- self._finger_down(x, y)
567+ self._device.finger_down(x, y)
568 sleep(0.1)
569- self._finger_up()
570-
571- def tap_object(self, object):
572- """Click (or 'tap') a given object"""
573+ self._device.finger_up()
574+
575+ def tap_object(self, object_):
576+ """Click (or 'tap') a given object.
577+
578+ :raises RuntimeError: if the finger is already pressed.
579+ :raises RuntimeError: if no more finger slots are available.
580+ :raises ValueError: if `object_` doesn't have any recognised position
581+ attributes or if they are not of the correct type.
582+
583+ """
584 logger.debug("Tapping object: %r", object)
585- x, y = get_center_point(object)
586+ x, y = get_center_point(object_)
587 self.tap(x, y)
588
589 def press(self, x, y):
590- """Press and hold a given object or at the given coordinates
591- Call release() when the object has been pressed long enough"""
592+ """Press and hold a given object or at the given coordinates.
593+
594+ Call release() when the object has been pressed long enough.
595+
596+ :raises RuntimeError: if the finger is already pressed.
597+ :raises RuntimeError: if no more finger slots are available.
598+
599+ """
600 logger.debug("Pressing at: %d,%d", x, y)
601- self._finger_down(x, y)
602+ self._device.finger_down(x, y)
603
604 def release(self):
605- """Release a previously pressed finger"""
606+ """Release a previously pressed finger.
607+
608+ :raises RuntimeError: if the touch is not pressed.
609+
610+ """
611 logger.debug("Releasing")
612- self._finger_up()
613+ self._device.finger_up()
614
615 def move(self, x, y):
616 """Moves the pointing "finger" to pos(x,y).
617
618 NOTE: The finger has to be down for this to have any effect.
619
620+ :raises RuntimeError: if the finger is not pressed.
621+
622 """
623- if self._touch_finger is None:
624- raise RuntimeError("Attempting to move without finger being down.")
625- self._finger_move(x, y)
626+ self._device.finger_move(x, y)
627
628 def drag(self, x1, y1, x2, y2):
629- """Perform a drag gesture from (x1,y1) to (x2,y2)"""
630+ """Perform a drag gesture from (x1,y1) to (x2,y2).
631+
632+ :raises RuntimeError: if the finger is already pressed.
633+ :raises RuntimeError: if no more finger slots are available.
634+
635+ """
636 logger.debug("Dragging from %d,%d to %d,%d", x1, y1, x2, y2)
637- self._finger_down(x1, y1)
638+ self._device.finger_down(x1, y1)
639
640 # Let's drag in 100 steps for now...
641 dx = 1.0 * (x2 - x1) / 100
642@@ -372,52 +537,13 @@
643 cur_x = x1 + dx
644 cur_y = y1 + dy
645 for i in range(0, 100):
646- self._finger_move(int(cur_x), int(cur_y))
647+ self._device.finger_move(int(cur_x), int(cur_y))
648 sleep(0.002)
649 cur_x += dx
650 cur_y += dy
651 # Make sure we actually end up at target
652- self._finger_move(x2, y2)
653- self._finger_up()
654-
655- def _finger_down(self, x, y):
656- """Internal: moves finger "finger" down on the touchscreen.
657-
658- :param x: The finger will be moved to this x coordinate.
659- :param y: The finger will be moved to this y coordinate.
660-
661- """
662- if self._touch_finger is not None:
663- raise RuntimeError("Cannot press finger: it's already pressed.")
664- self._touch_finger = _get_touch_finger()
665-
666- _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
667- _touch_device.write(
668- e.EV_ABS, e.ABS_MT_TRACKING_ID, get_next_tracking_id())
669- _touch_device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 1)
670- _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
671- _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
672- _touch_device.write(e.EV_ABS, e.ABS_MT_PRESSURE, 400)
673- _touch_device.syn()
674-
675- def _finger_move(self, x, y):
676- """Internal: moves finger "finger" on the touchscreen to pos (x,y)
677- NOTE: The finger has to be down for this to have any effect."""
678- if self._touch_finger is not None:
679- _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
680- _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_X, int(x))
681- _touch_device.write(e.EV_ABS, e.ABS_MT_POSITION_Y, int(y))
682- _touch_device.syn()
683-
684- def _finger_up(self):
685- """Internal: moves finger "finger" up from the touchscreen"""
686- if self._touch_finger is None:
687- raise RuntimeError("Cannot release finger: it's not pressed.")
688- _touch_device.write(e.EV_ABS, e.ABS_MT_SLOT, self._touch_finger)
689- _touch_device.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, -1)
690- _touch_device.write(e.EV_KEY, e.BTN_TOOL_FINGER, 0)
691- _touch_device.syn()
692- self._touch_finger = _release_touch_finger(self._touch_finger)
693+ self._device.finger_move(x2, y2)
694+ self._device.finger_up()
695
696
697 # veebers: there should be a better way to handle this.
698
699=== modified file 'autopilot/tests/functional/test_input_stack.py'
700--- autopilot/tests/functional/test_input_stack.py 2013-12-16 00:20:40 +0000
701+++ autopilot/tests/functional/test_input_stack.py 2014-02-19 18:32:31 +0000
702@@ -1,7 +1,7 @@
703 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
704 #
705 # Autopilot Functional Test Tool
706-# Copyright (C) 2012-2013 Canonical
707+# Copyright (C) 2012, 2013, 2014 Canonical
708 #
709 # This program is free software: you can redistribute it and/or modify
710 # it under the terms of the GNU General Public License as published by
711@@ -150,8 +150,8 @@
712 from autopilot.input._X11 import _PRESSED_KEYS
713 return _PRESSED_KEYS
714 elif self.backend == 'UInput':
715- from autopilot.input._uinput import _PRESSED_KEYS
716- return _PRESSED_KEYS
717+ from autopilot.input import _uinput
718+ return _uinput.Keyboard._device._pressed_keys_ecodes
719 else:
720 self.fail("Don't know how to get pressed keys list for backend "
721 + self.backend
722@@ -551,8 +551,9 @@
723 test_result = FakeTestCase("test_press_key").run()
724
725 self.assertThat(test_result.wasSuccessful(), Equals(True))
726- from autopilot.input._uinput import _PRESSED_KEYS
727- self.assertThat(_PRESSED_KEYS, Equals([]))
728+ from autopilot.input import _uinput
729+ self.assertThat(
730+ _uinput.Keyboard._device._pressed_keys_ecodes, Equals([]))
731
732 @patch('autopilot.input._X11.fake_input', new=lambda *args: None, )
733 def test_mouse_button_released(self):
734
735=== modified file 'autopilot/tests/unit/test_input.py'
736--- autopilot/tests/unit/test_input.py 2013-12-10 03:10:11 +0000
737+++ autopilot/tests/unit/test_input.py 2014-02-19 18:32:31 +0000
738@@ -1,7 +1,7 @@
739 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
740 #
741 # Autopilot Functional Test Tool
742-# Copyright (C) 2013 Canonical
743+# Copyright (C) 2013, 2014 Canonical
744 #
745 # This program is free software: you can redistribute it and/or modify
746 # it under the terms of the GNU General Public License as published by
747@@ -17,11 +17,16 @@
748 # along with this program. If not, see <http://www.gnu.org/licenses/>.
749 #
750
751-from mock import patch
752+import testscenarios
753+from evdev import ecodes, uinput
754+from mock import ANY, call, patch, Mock
755+from six import StringIO
756 from testtools import TestCase
757-from testtools.matchers import raises
758+from testtools.matchers import Contains, raises
759
760+from autopilot.input import _uinput
761 from autopilot.input._common import get_center_point
762+from autopilot import utilities
763
764
765 class Empty(object):
766@@ -151,3 +156,578 @@
767
768 self.assertEqual(123, x)
769 self.assertEqual(345, y)
770+
771+
772+class UInputTestCase(TestCase):
773+ """Tests for the global methods of the uinput module."""
774+
775+ def test_create_touch_device_must_print_deprecation_message(self):
776+ with patch('sys.stderr', new=StringIO()) as stderr:
777+ with patch('autopilot.input._uinput.UInput'):
778+ _uinput.create_touch_device('dummy', 'dummy')
779+ self.assertThat(
780+ stderr.getvalue(),
781+ Contains(
782+ "This function is deprecated. Please use 'the Touch class to "
783+ "instantiate a device object' instead."
784+ )
785+ )
786+
787+
788+class UInputKeyboardDeviceTestCase(TestCase):
789+ """Test the integration with evdev.UInput for the keyboard."""
790+
791+ _PRESS_VALUE = 1
792+ _RELEASE_VALUE = 0
793+
794+ def get_keyboard_with_mocked_backend(self):
795+ keyboard = _uinput._UInputKeyboardDevice(device_class=Mock)
796+ keyboard._device.mock_add_spec(uinput.UInput, spec_set=True)
797+ return keyboard
798+
799+ def assert_key_press_emitted_write_and_syn(self, keyboard, key):
800+ self.assert_emitted_write_and_syn(keyboard, key, self._PRESS_VALUE)
801+
802+ def assert_key_release_emitted_write_and_syn(self, keyboard, key):
803+ self.assert_emitted_write_and_syn(keyboard, key, self._RELEASE_VALUE)
804+
805+ def assert_emitted_write_and_syn(self, keyboard, key, value):
806+ key_ecode = ecodes.ecodes.get(key)
807+ expected_calls = [
808+ call.write(ecodes.EV_KEY, key_ecode, value),
809+ call.syn()
810+ ]
811+
812+ self.assertEqual(expected_calls, keyboard._device.mock_calls)
813+
814+ def press_key_and_reset_mock(self, keyboard, key):
815+ keyboard.press(key)
816+ keyboard._device.reset_mock()
817+
818+ def test_press_key_must_emit_write_and_syn(self):
819+ keyboard = self.get_keyboard_with_mocked_backend()
820+ keyboard.press('KEY_A')
821+ self.assert_key_press_emitted_write_and_syn(keyboard, 'KEY_A')
822+
823+ def test_press_key_must_append_leading_string(self):
824+ keyboard = self.get_keyboard_with_mocked_backend()
825+ keyboard.press('A')
826+ self.assert_key_press_emitted_write_and_syn(keyboard, 'KEY_A')
827+
828+ def test_press_key_must_ignore_case(self):
829+ keyboard = self.get_keyboard_with_mocked_backend()
830+ keyboard.press('a')
831+ self.assert_key_press_emitted_write_and_syn(keyboard, 'KEY_A')
832+
833+ def test_press_unexisting_key_must_raise_error(self):
834+ keyboard = self.get_keyboard_with_mocked_backend()
835+ error = self.assertRaises(
836+ ValueError, keyboard.press, 'unexisting')
837+
838+ self.assertEqual('Unknown key name: unexisting.', str(error))
839+
840+ def test_release_not_pressed_key_must_raise_error(self):
841+ keyboard = self.get_keyboard_with_mocked_backend()
842+ error = self.assertRaises(
843+ ValueError, keyboard.release, 'A')
844+
845+ self.assertEqual("Key 'A' not pressed.", str(error))
846+
847+ def test_release_key_must_emit_write_and_syn(self):
848+ keyboard = self.get_keyboard_with_mocked_backend()
849+ self.press_key_and_reset_mock(keyboard, 'KEY_A')
850+
851+ keyboard.release('KEY_A')
852+ self.assert_key_release_emitted_write_and_syn(keyboard, 'KEY_A')
853+
854+ def test_release_key_must_append_leading_string(self):
855+ keyboard = self.get_keyboard_with_mocked_backend()
856+ self.press_key_and_reset_mock(keyboard, 'KEY_A')
857+
858+ keyboard.release('A')
859+ self.assert_key_release_emitted_write_and_syn(keyboard, 'KEY_A')
860+
861+ def test_release_key_must_ignore_case(self):
862+ keyboard = self.get_keyboard_with_mocked_backend()
863+ self.press_key_and_reset_mock(keyboard, 'KEY_A')
864+
865+ keyboard.release('a')
866+ self.assert_key_release_emitted_write_and_syn(keyboard, 'KEY_A')
867+
868+ def test_release_unexisting_key_must_raise_error(self):
869+ keyboard = self.get_keyboard_with_mocked_backend()
870+ error = self.assertRaises(
871+ ValueError, keyboard.release, 'unexisting')
872+
873+ self.assertEqual('Unknown key name: unexisting.', str(error))
874+
875+ def test_release_pressed_keys_without_pressed_keys_must_do_nothing(self):
876+ keyboard = self.get_keyboard_with_mocked_backend()
877+ keyboard.release_pressed_keys()
878+ self.assertEqual([], keyboard._device.mock_calls)
879+
880+ def test_release_pressed_keys_with_pressed_keys(self):
881+ expected_calls = [
882+ call.write(
883+ ecodes.EV_KEY, ecodes.ecodes.get('KEY_A'),
884+ self._RELEASE_VALUE),
885+ call.syn(),
886+ call.write(
887+ ecodes.EV_KEY, ecodes.ecodes.get('KEY_B'),
888+ self._RELEASE_VALUE),
889+ call.syn()
890+ ]
891+
892+ keyboard = self.get_keyboard_with_mocked_backend()
893+ self.press_key_and_reset_mock(keyboard, 'KEY_A')
894+ self.press_key_and_reset_mock(keyboard, 'KEY_B')
895+
896+ keyboard.release_pressed_keys()
897+
898+ self.assertEqual(expected_calls, keyboard._device.mock_calls)
899+
900+ def test_release_pressed_keys_already_released(self):
901+ expected_calls = []
902+ keyboard = self.get_keyboard_with_mocked_backend()
903+ keyboard.press('KEY_A')
904+ keyboard.release_pressed_keys()
905+ keyboard._device.reset_mock()
906+
907+ keyboard.release_pressed_keys()
908+ self.assertEqual(expected_calls, keyboard._device.mock_calls)
909+
910+
911+class UInputKeyboardTestCase(testscenarios.TestWithScenarios, TestCase):
912+ """Test UInput Keyboard helper for autopilot tests."""
913+
914+ scenarios = [
915+ ('single key', dict(keys='a', expected_calls_args=['a'])),
916+ ('upper-case letter', dict(
917+ keys='A', expected_calls_args=['KEY_LEFTSHIFT', 'A'])),
918+ ('key combination', dict(
919+ keys='a+b', expected_calls_args=['a', 'b']))
920+ ]
921+
922+ def setUp(self):
923+ super(UInputKeyboardTestCase, self).setUp()
924+ # Return to the original device after the test.
925+ self.addCleanup(self.set_keyboard_device, _uinput.Keyboard._device)
926+ # Mock the sleeps so we don't have to spend time actually sleeping.
927+ self.addCleanup(utilities.sleep.disable_mock)
928+ utilities.sleep.enable_mock()
929+
930+ def set_keyboard_device(self, device):
931+ _uinput.Keyboard._device = device
932+
933+ def get_keyboard_with_mocked_backend(self):
934+ _uinput.Keyboard._device = None
935+ keyboard = _uinput.Keyboard(device_class=Mock)
936+ keyboard._device.mock_add_spec(
937+ _uinput._UInputKeyboardDevice, spec_set=True)
938+ return keyboard
939+
940+ def test_press_must_put_press_device_keys(self):
941+ expected_calls = [
942+ call.press(arg) for arg in self.expected_calls_args]
943+ keyboard = self.get_keyboard_with_mocked_backend()
944+ keyboard.press(self.keys)
945+
946+ self.assertEqual(expected_calls, keyboard._device.mock_calls)
947+
948+ def test_release_must_release_device_keys(self):
949+ keyboard = self.get_keyboard_with_mocked_backend()
950+ keyboard.press(self.keys)
951+ keyboard._device.reset_mock()
952+
953+ expected_calls = [
954+ call.release(arg) for arg in
955+ reversed(self.expected_calls_args)]
956+ keyboard.release(self.keys)
957+
958+ self.assertEqual(
959+ expected_calls, keyboard._device.mock_calls)
960+
961+ def test_press_and_release_must_press_device_keys(self):
962+ expected_press_calls = [
963+ call.press(arg) for arg in self.expected_calls_args]
964+ ignored_calls = [
965+ ANY for arg in self.expected_calls_args]
966+
967+ keyboard = self.get_keyboard_with_mocked_backend()
968+ keyboard.press_and_release(self.keys)
969+
970+ self.assertEqual(
971+ expected_press_calls + ignored_calls,
972+ keyboard._device.mock_calls)
973+
974+ def test_press_and_release_must_release_device_keys_in_reverse_order(
975+ self):
976+ ignored_calls = [
977+ ANY for arg in self.expected_calls_args]
978+ expected_release_calls = [
979+ call.release(arg) for arg in
980+ reversed(self.expected_calls_args)]
981+
982+ keyboard = self.get_keyboard_with_mocked_backend()
983+ keyboard.press_and_release(self.keys)
984+
985+ self.assertEqual(
986+ ignored_calls + expected_release_calls,
987+ keyboard._device.mock_calls)
988+
989+ def test_on_test_end_without_device_must_do_nothing(self):
990+ _uinput.Keyboard._device = None
991+ # This will fail if it calls anything from the device, as it's None.
992+ _uinput.Keyboard.on_test_end(self)
993+
994+ def test_on_test_end_with_device_must_release_pressed_keys(self):
995+ keyboard = self.get_keyboard_with_mocked_backend()
996+ _uinput.Keyboard.on_test_end(self)
997+ self.assertEqual(
998+ [call.release_pressed_keys()], keyboard._device.mock_calls)
999+
1000+
1001+class TouchEventsTestCase(TestCase):
1002+
1003+ def assert_expected_ev_abs(self, res_x, res_y, actual_ev_abs):
1004+ expected_ev_abs = [
1005+ (ecodes.ABS_X, (0, res_x, 0, 0)),
1006+ (ecodes.ABS_Y, (0, res_y, 0, 0)),
1007+ (ecodes.ABS_PRESSURE, (0, 65535, 0, 0)),
1008+ (ecodes.ABS_MT_POSITION_X, (0, res_x, 0, 0)),
1009+ (ecodes.ABS_MT_POSITION_Y, (0, res_y, 0, 0)),
1010+ (ecodes.ABS_MT_TOUCH_MAJOR, (0, 30, 0, 0)),
1011+ (ecodes.ABS_MT_TRACKING_ID, (0, 65535, 0, 0)),
1012+ (ecodes.ABS_MT_PRESSURE, (0, 255, 0, 0)),
1013+ (ecodes.ABS_MT_SLOT, (0, 9, 0, 0))
1014+ ]
1015+ self.assertEqual(expected_ev_abs, actual_ev_abs)
1016+
1017+ def test_get_touch_events_without_args_must_use_system_resolution(self):
1018+ with patch.object(
1019+ _uinput, '_get_system_resolution', spec_set=True,
1020+ autospec=True) as mock_system_resolution:
1021+ mock_system_resolution.return_value = (
1022+ 'system_res_x', 'system_res_y')
1023+ events = _uinput._get_touch_events()
1024+
1025+ ev_abs = events.get(ecodes.EV_ABS)
1026+ self.assert_expected_ev_abs('system_res_x', 'system_res_y', ev_abs)
1027+
1028+ def test_get_touch_events_with_args_must_use_given_resulution(self):
1029+ events = _uinput._get_touch_events('given_res_x', 'given_res_y')
1030+ ev_abs = events.get(ecodes.EV_ABS)
1031+ self.assert_expected_ev_abs('given_res_x', 'given_res_y', ev_abs)
1032+
1033+
1034+class UInputTouchDeviceTestCase(TestCase):
1035+ """Test the integration with evdev.UInput for the touch device."""
1036+
1037+ def setUp(self):
1038+ super(UInputTouchDeviceTestCase, self).setUp()
1039+ self._number_of_slots = 9
1040+
1041+ # Return to the original device after the test.
1042+ self.addCleanup(
1043+ self.set_mouse_device,
1044+ _uinput._UInputTouchDevice._device,
1045+ _uinput._UInputTouchDevice._touch_fingers_in_use,
1046+ _uinput._UInputTouchDevice._last_tracking_id)
1047+
1048+ # Always start the tests without fingers in use.
1049+ _uinput._UInputTouchDevice._touch_fingers_in_use = []
1050+ _uinput._UInputTouchDevice._last_tracking_id = 0
1051+
1052+ def set_mouse_device(
1053+ self, device, touch_fingers_in_use, last_tracking_id):
1054+ _uinput._UInputTouchDevice._device = device
1055+ _uinput._UInputTouchDevice._touch_fingers_in_use = touch_fingers_in_use
1056+ _uinput._UInputTouchDevice._last_tracking_id = last_tracking_id
1057+
1058+ def get_touch_with_mocked_backend(self):
1059+ dummy_x_resolution = 100
1060+ dummy_y_resolution = 100
1061+
1062+ _uinput._UInputTouchDevice._device = None
1063+ touch = _uinput._UInputTouchDevice(
1064+ res_x=dummy_x_resolution, res_y=dummy_y_resolution,
1065+ device_class=Mock)
1066+ touch._device.mock_add_spec(uinput.UInput, spec_set=True)
1067+ return touch
1068+
1069+ def assert_finger_down_emitted_write_and_syn(
1070+ self, touch, slot, tracking_id, x, y):
1071+ press_value = 1
1072+ expected_calls = [
1073+ call.write(ecodes.EV_ABS, ecodes.ABS_MT_SLOT, slot),
1074+ call.write(
1075+ ecodes.EV_ABS, ecodes.ABS_MT_TRACKING_ID, tracking_id),
1076+ call.write(
1077+ ecodes.EV_KEY, ecodes.BTN_TOOL_FINGER, press_value),
1078+ call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_X, x),
1079+ call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_Y, y),
1080+ call.write(ecodes.EV_ABS, ecodes.ABS_MT_PRESSURE, 400),
1081+ call.syn()
1082+ ]
1083+ self.assertEqual(expected_calls, touch._device.mock_calls)
1084+
1085+ def assert_finger_move_emitted_write_and_syn(self, touch, slot, x, y):
1086+ expected_calls = [
1087+ call.write(ecodes.EV_ABS, ecodes.ABS_MT_SLOT, slot),
1088+ call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_X, x),
1089+ call.write(ecodes.EV_ABS, ecodes.ABS_MT_POSITION_Y, y),
1090+ call.syn()
1091+ ]
1092+ self.assertEqual(expected_calls, touch._device.mock_calls)
1093+
1094+ def assert_finger_up_emitted_write_and_syn(self, touch, slot):
1095+ lift_tracking_id = -1
1096+ release_value = 0
1097+ expected_calls = [
1098+ call.write(ecodes.EV_ABS, ecodes.ABS_MT_SLOT, slot),
1099+ call.write(
1100+ ecodes.EV_ABS, ecodes.ABS_MT_TRACKING_ID, lift_tracking_id),
1101+ call.write(
1102+ ecodes.EV_KEY, ecodes.BTN_TOOL_FINGER, release_value),
1103+ call.syn()
1104+ ]
1105+ self.assertEqual(expected_calls, touch._device.mock_calls)
1106+
1107+ def test_finger_down_must_use_free_slot(self):
1108+ for slot in range(self._number_of_slots):
1109+ touch = self.get_touch_with_mocked_backend()
1110+
1111+ touch.finger_down(0, 0)
1112+
1113+ self.assert_finger_down_emitted_write_and_syn(
1114+ touch, slot=slot, tracking_id=ANY, x=0, y=0)
1115+
1116+ def test_finger_down_without_free_slots_must_raise_error(self):
1117+ # Claim all the available slots.
1118+ for slot in range(self._number_of_slots):
1119+ touch = self.get_touch_with_mocked_backend()
1120+ touch.finger_down(0, 0)
1121+
1122+ touch = self.get_touch_with_mocked_backend()
1123+
1124+ # Try to use one more.
1125+ error = self.assertRaises(RuntimeError, touch.finger_down, 11, 11)
1126+ self.assertEqual(
1127+ 'All available fingers have been used already.', str(error))
1128+
1129+ def test_finger_down_must_use_unique_tracking_id(self):
1130+ for number in range(self._number_of_slots):
1131+ touch = self.get_touch_with_mocked_backend()
1132+ touch.finger_down(0, 0)
1133+
1134+ self.assert_finger_down_emitted_write_and_syn(
1135+ touch, slot=ANY, tracking_id=number + 1, x=0, y=0)
1136+
1137+ def test_finger_down_must_not_reuse_tracking_ids(self):
1138+ # Claim and release all the available slots once.
1139+ for number in range(self._number_of_slots):
1140+ touch = self.get_touch_with_mocked_backend()
1141+ touch.finger_down(0, 0)
1142+ touch.finger_up()
1143+
1144+ touch = self.get_touch_with_mocked_backend()
1145+
1146+ touch.finger_down(12, 12)
1147+ self.assert_finger_down_emitted_write_and_syn(
1148+ touch, slot=ANY, tracking_id=number + 2, x=12, y=12)
1149+
1150+ def test_finger_down_with_finger_pressed_must_raise_error(self):
1151+ touch = self.get_touch_with_mocked_backend()
1152+ touch.finger_down(0, 0)
1153+
1154+ error = self.assertRaises(RuntimeError, touch.finger_down, 0, 0)
1155+ self.assertEqual(
1156+ "Cannot press finger: it's already pressed.", str(error))
1157+
1158+ def test_finger_move_without_finger_pressed_must_raise_error(self):
1159+ touch = self.get_touch_with_mocked_backend()
1160+
1161+ error = self.assertRaises(RuntimeError, touch.finger_move, 10, 10)
1162+ self.assertEqual(
1163+ 'Attempting to move without finger being down.', str(error))
1164+
1165+ def test_finger_move_must_use_assigned_slot(self):
1166+ for slot in range(self._number_of_slots):
1167+ touch = self.get_touch_with_mocked_backend()
1168+ touch.finger_down(0, 0)
1169+ touch._device.reset_mock()
1170+
1171+ touch.finger_move(10, 10)
1172+
1173+ self.assert_finger_move_emitted_write_and_syn(
1174+ touch, slot=slot, x=10, y=10)
1175+
1176+ def test_finger_move_must_reuse_assigned_slot(self):
1177+ first_slot = 0
1178+ touch = self.get_touch_with_mocked_backend()
1179+ touch.finger_down(1, 1)
1180+ touch._device.reset_mock()
1181+
1182+ touch.finger_move(13, 13)
1183+ self.assert_finger_move_emitted_write_and_syn(
1184+ touch, slot=first_slot, x=13, y=13)
1185+ touch._device.reset_mock()
1186+
1187+ touch.finger_move(14, 14)
1188+ self.assert_finger_move_emitted_write_and_syn(
1189+ touch, slot=first_slot, x=14, y=14)
1190+
1191+ def test_finger_up_without_finger_pressed_must_raise_error(self):
1192+ touch = self.get_touch_with_mocked_backend()
1193+
1194+ error = self.assertRaises(RuntimeError, touch.finger_up)
1195+ self.assertEqual(
1196+ "Cannot release finger: it's not pressed.", str(error))
1197+
1198+ def test_finger_up_must_use_assigned_slot(self):
1199+ fingers = []
1200+ for slot in range(self._number_of_slots):
1201+ touch = self.get_touch_with_mocked_backend()
1202+ touch.finger_down(0, 0)
1203+ touch._device.reset_mock()
1204+ fingers.append(touch)
1205+
1206+ for slot, touch in enumerate(fingers):
1207+ touch.finger_up()
1208+
1209+ self.assert_finger_up_emitted_write_and_syn(touch, slot=slot)
1210+ touch._device.reset_mock()
1211+
1212+ def test_finger_up_must_release_slot(self):
1213+ fingers = []
1214+ # Claim all the available slots.
1215+ for slot in range(self._number_of_slots):
1216+ touch = self.get_touch_with_mocked_backend()
1217+ touch.finger_down(0, 0)
1218+ fingers.append(touch)
1219+
1220+ slot_to_reuse = 3
1221+ fingers[slot_to_reuse].finger_up()
1222+
1223+ touch = self.get_touch_with_mocked_backend()
1224+
1225+ # Try to use one more.
1226+ touch.finger_down(15, 15)
1227+ self.assert_finger_down_emitted_write_and_syn(
1228+ touch, slot=slot_to_reuse, tracking_id=ANY, x=15, y=15)
1229+
1230+ def test_device_with_finger_down_must_be_pressed(self):
1231+ touch = self.get_touch_with_mocked_backend()
1232+ touch.finger_down(0, 0)
1233+
1234+ self.assertTrue(touch.pressed)
1235+
1236+ def test_device_without_finger_down_must_not_be_pressed(self):
1237+ touch = self.get_touch_with_mocked_backend()
1238+ self.assertFalse(touch.pressed)
1239+
1240+ def test_device_after_finger_up_must_not_be_pressed(self):
1241+ touch = self.get_touch_with_mocked_backend()
1242+ touch.finger_down(0, 0)
1243+ touch.finger_up()
1244+
1245+ self.assertFalse(touch.pressed)
1246+
1247+ def test_press_other_device_must_not_press_all_of_them(self):
1248+ other_touch = self.get_touch_with_mocked_backend()
1249+ other_touch.finger_down(0, 0)
1250+
1251+ touch = self.get_touch_with_mocked_backend()
1252+ self.assertFalse(touch.pressed)
1253+
1254+
1255+class UInputTouchTestCase(TestCase):
1256+ """Test UInput Touch helper for autopilot tests."""
1257+
1258+ def setUp(self):
1259+ super(UInputTouchTestCase, self).setUp()
1260+ # Mock the sleeps so we don't have to spend time actually sleeping.
1261+ self.addCleanup(utilities.sleep.disable_mock)
1262+ utilities.sleep.enable_mock()
1263+
1264+ def get_touch_with_mocked_backend(self):
1265+ touch = _uinput.Touch(device_class=Mock)
1266+ touch._device.mock_add_spec(
1267+ _uinput._UInputTouchDevice, spec_set=True)
1268+ return touch
1269+
1270+ def test_tap_must_put_finger_down_and_then_up(self):
1271+ expected_calls = [
1272+ call.finger_down(0, 0),
1273+ call.finger_up()
1274+ ]
1275+
1276+ touch = self.get_touch_with_mocked_backend()
1277+ touch.tap(0, 0)
1278+ self.assertEqual(expected_calls, touch._device.mock_calls)
1279+
1280+ def test_tap_object_must_put_finger_down_and_then_up_on_the_center(self):
1281+ object_ = type('Dummy', (object,), {'globalRect': (0, 0, 10, 10)})
1282+ expected_calls = [
1283+ call.finger_down(5, 5),
1284+ call.finger_up()
1285+ ]
1286+
1287+ touch = self.get_touch_with_mocked_backend()
1288+ touch.tap_object(object_)
1289+ self.assertEqual(expected_calls, touch._device.mock_calls)
1290+
1291+ def test_press_must_put_finger_down(self):
1292+ expected_calls = [call.finger_down(0, 0)]
1293+
1294+ touch = self.get_touch_with_mocked_backend()
1295+ touch.press(0, 0)
1296+ self.assertEqual(expected_calls, touch._device.mock_calls)
1297+
1298+ def test_release_must_put_finger_up(self):
1299+ expected_calls = [call.finger_up()]
1300+
1301+ touch = self.get_touch_with_mocked_backend()
1302+ touch.release()
1303+ self.assertEqual(expected_calls, touch._device.mock_calls)
1304+
1305+ def test_move_must_move_finger(self):
1306+ expected_calls = [call.finger_move(10, 10)]
1307+
1308+ touch = self.get_touch_with_mocked_backend()
1309+ touch.move(10, 10)
1310+ self.assertEqual(expected_calls, touch._device.mock_calls)
1311+
1312+
1313+class MultipleUInputTouchBackend(_uinput._UInputTouchDevice):
1314+
1315+ def __init__(self, res_x=100, res_y=100, device_class=Mock):
1316+ super(MultipleUInputTouchBackend, self).__init__(
1317+ res_x, res_y, device_class)
1318+
1319+
1320+class MultipleUInputTouchTestCase(TestCase):
1321+
1322+ def setUp(self):
1323+ super(MultipleUInputTouchTestCase, self).setUp()
1324+ # Return to the original device after the test.
1325+ self.addCleanup(
1326+ self.set_mouse_device,
1327+ _uinput._UInputTouchDevice._device,
1328+ _uinput._UInputTouchDevice._touch_fingers_in_use,
1329+ _uinput._UInputTouchDevice._last_tracking_id)
1330+
1331+ def set_mouse_device(
1332+ self, device, touch_fingers_in_use, last_tracking_id):
1333+ _uinput._UInputTouchDevice._device = device
1334+ _uinput._UInputTouchDevice._touch_fingers_in_use = touch_fingers_in_use
1335+ _uinput._UInputTouchDevice._last_tracking_id = last_tracking_id
1336+
1337+ def test_press_other_device_must_not_press_all_of_them(self):
1338+ finger1 = _uinput.Touch(device_class=MultipleUInputTouchBackend)
1339+ finger2 = _uinput.Touch(device_class=MultipleUInputTouchBackend)
1340+
1341+ finger1.press(0, 0)
1342+ self.addCleanup(finger1.release)
1343+
1344+ self.assertFalse(finger2.pressed)
1345
1346=== modified file 'debian/control'
1347--- debian/control 2014-02-12 01:13:06 +0000
1348+++ debian/control 2014-02-19 18:32:31 +0000
1349@@ -16,6 +16,7 @@
1350 python-dbus,
1351 python-debian,
1352 python-dev,
1353+ python-evdev,
1354 python-fixtures,
1355 python-gi,
1356 python-junitxml,
1357@@ -30,6 +31,7 @@
1358 python-xlib,
1359 python3-all-dev (>= 3.3),
1360 python3-dbus,
1361+ python3-evdev,
1362 python3-fixtures,
1363 python3-gi,
1364 python3-junitxml,

Subscribers

People subscribed via source and target branches