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