OLD | NEW |
1 # Copyright 2014 The Chromium Authors. All rights reserved. | 1 # Copyright 2015 The Chromium Authors. All rights reserved. |
2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """ | 5 # pylint: disable=unused-wildcard-import |
6 A test facility to assert call sequences while mocking their behavior. | 6 # pylint: disable=wildcard-import |
7 """ | |
8 | 7 |
9 import os | 8 from devil.utils.mock_calls import * |
10 import sys | |
11 import unittest | |
12 | |
13 from pylib import constants | |
14 | |
15 sys.path.append(os.path.join( | |
16 constants.DIR_SOURCE_ROOT, 'third_party', 'pymock')) | |
17 import mock # pylint: disable=F0401 | |
18 | |
19 | |
20 class TestCase(unittest.TestCase): | |
21 """Adds assertCalls to TestCase objects.""" | |
22 class _AssertCalls(object): | |
23 def __init__(self, test_case, expected_calls, watched): | |
24 def call_action(pair): | |
25 if isinstance(pair, type(mock.call)): | |
26 return (pair, None) | |
27 else: | |
28 return pair | |
29 | |
30 def do_check(call): | |
31 def side_effect(*args, **kwargs): | |
32 received_call = call(*args, **kwargs) | |
33 self._test_case.assertTrue( | |
34 self._expected_calls, | |
35 msg=('Unexpected call: %s' % str(received_call))) | |
36 expected_call, action = self._expected_calls.pop(0) | |
37 self._test_case.assertTrue( | |
38 received_call == expected_call, | |
39 msg=('Expected call mismatch:\n' | |
40 ' expected: %s\n' | |
41 ' received: %s\n' | |
42 % (str(expected_call), str(received_call)))) | |
43 if callable(action): | |
44 return action(*args, **kwargs) | |
45 else: | |
46 return action | |
47 return side_effect | |
48 | |
49 self._test_case = test_case | |
50 self._expected_calls = [call_action(pair) for pair in expected_calls] | |
51 watched = watched.copy() # do not pollute the caller's dict | |
52 watched.update((call.parent.name, call.parent) | |
53 for call, _ in self._expected_calls) | |
54 self._patched = [test_case.patch_call(call, side_effect=do_check(call)) | |
55 for call in watched.itervalues()] | |
56 | |
57 def __enter__(self): | |
58 for patch in self._patched: | |
59 patch.__enter__() | |
60 return self | |
61 | |
62 def __exit__(self, exc_type, exc_val, exc_tb): | |
63 for patch in self._patched: | |
64 patch.__exit__(exc_type, exc_val, exc_tb) | |
65 if exc_type is None: | |
66 missing = ''.join(' expected: %s\n' % str(call) | |
67 for call, _ in self._expected_calls) | |
68 self._test_case.assertFalse( | |
69 missing, | |
70 msg='Expected calls not found:\n' + missing) | |
71 | |
72 def __init__(self, *args, **kwargs): | |
73 super(TestCase, self).__init__(*args, **kwargs) | |
74 self.call = mock.call.self | |
75 self._watched = {} | |
76 | |
77 def call_target(self, call): | |
78 """Resolve a self.call instance to the target it represents. | |
79 | |
80 Args: | |
81 call: a self.call instance, e.g. self.call.adb.Shell | |
82 | |
83 Returns: | |
84 The target object represented by the call, e.g. self.adb.Shell | |
85 | |
86 Raises: | |
87 ValueError if the path of the call does not start with "self", i.e. the | |
88 target of the call is external to the self object. | |
89 AttributeError if the path of the call does not specify a valid | |
90 chain of attributes (without any calls) starting from "self". | |
91 """ | |
92 path = call.name.split('.') | |
93 if path.pop(0) != 'self': | |
94 raise ValueError("Target %r outside of 'self' object" % call.name) | |
95 target = self | |
96 for attr in path: | |
97 target = getattr(target, attr) | |
98 return target | |
99 | |
100 def patch_call(self, call, **kwargs): | |
101 """Patch the target of a mock.call instance. | |
102 | |
103 Args: | |
104 call: a mock.call instance identifying a target to patch | |
105 Extra keyword arguments are processed by mock.patch | |
106 | |
107 Returns: | |
108 A context manager to mock/unmock the target of the call | |
109 """ | |
110 if call.name.startswith('self.'): | |
111 target = self.call_target(call.parent) | |
112 _, attribute = call.name.rsplit('.', 1) | |
113 if (hasattr(type(target), attribute) | |
114 and isinstance(getattr(type(target), attribute), property)): | |
115 return mock.patch.object( | |
116 type(target), attribute, new_callable=mock.PropertyMock, **kwargs) | |
117 else: | |
118 return mock.patch.object(target, attribute, **kwargs) | |
119 else: | |
120 return mock.patch(call.name, **kwargs) | |
121 | |
122 def watchCalls(self, calls): | |
123 """Add calls to the set of watched calls. | |
124 | |
125 Args: | |
126 calls: a sequence of mock.call instances identifying targets to watch | |
127 """ | |
128 self._watched.update((call.name, call) for call in calls) | |
129 | |
130 def watchMethodCalls(self, call, ignore=None): | |
131 """Watch all public methods of the target identified by a self.call. | |
132 | |
133 Args: | |
134 call: a self.call instance indetifying an object | |
135 ignore: a list of public methods to ignore when watching for calls | |
136 """ | |
137 target = self.call_target(call) | |
138 if ignore is None: | |
139 ignore = [] | |
140 self.watchCalls(getattr(call, method) | |
141 for method in dir(target.__class__) | |
142 if not method.startswith('_') and not method in ignore) | |
143 | |
144 def clearWatched(self): | |
145 """Clear the set of watched calls.""" | |
146 self._watched = {} | |
147 | |
148 def assertCalls(self, *calls): | |
149 """A context manager to assert that a sequence of calls is made. | |
150 | |
151 During the assertion, a number of functions and methods will be "watched", | |
152 and any calls made to them is expected to appear---in the exact same order, | |
153 and with the exact same arguments---as specified by the argument |calls|. | |
154 | |
155 By default, the targets of all expected calls are watched. Further targets | |
156 to watch may be added using watchCalls and watchMethodCalls. | |
157 | |
158 Optionaly, each call may be accompanied by an action. If the action is a | |
159 (non-callable) value, this value will be used as the return value given to | |
160 the caller when the matching call is found. Alternatively, if the action is | |
161 a callable, the action will be then called with the same arguments as the | |
162 intercepted call, so that it can provide a return value or perform other | |
163 side effects. If the action is missing, a return value of None is assumed. | |
164 | |
165 Note that mock.Mock objects are often convenient to use as a callable | |
166 action, e.g. to raise exceptions or return other objects which are | |
167 themselves callable. | |
168 | |
169 Args: | |
170 calls: each argument is either a pair (expected_call, action) or just an | |
171 expected_call, where expected_call is a mock.call instance. | |
172 | |
173 Raises: | |
174 AssertionError if the watched targets do not receive the exact sequence | |
175 of calls specified. Missing calls, extra calls, and calls with | |
176 mismatching arguments, all cause the assertion to fail. | |
177 """ | |
178 return self._AssertCalls(self, calls, self._watched) | |
179 | |
180 def assertCall(self, call, action=None): | |
181 return self.assertCalls((call, action)) | |
182 | |
OLD | NEW |