Merge lp:~nuclearbob/autopilot/pep257-b into lp:autopilot
- pep257-b
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Christopher Lee |
Approved revision: | 452 |
Merged at revision: | 463 |
Proposed branch: | lp:~nuclearbob/autopilot/pep257-b |
Merge into: | lp:autopilot |
Diff against target: |
2832 lines (+1213/-660) 27 files modified
autopilot/__init__.py (+1/-0) autopilot/application/__init__.py (+4/-2) autopilot/application/_environment.py (+4/-32) autopilot/application/_launcher.py (+116/-111) autopilot/dbus_handler.py (+3/-3) autopilot/display/_X11.py (+6/-2) autopilot/display/__init__.py (+6/-4) autopilot/globals.py (+3/-1) autopilot/introspection/dbus.py (+95/-20) autopilot/keybindings.py (+22/-11) autopilot/platform.py (+2/-0) autopilot/run.py (+8/-7) autopilot/testcase.py (+58/-7) autopilot/testresult.py (+9/-4) autopilot/tests/functional/__init__.py (+1/-86) autopilot/tests/functional/fixtures.py (+170/-0) autopilot/tests/functional/test_ap_apps.py (+60/-71) autopilot/tests/functional/test_autopilot_functional.py (+20/-13) autopilot/tests/functional/test_input_stack.py (+1/-1) autopilot/tests/functional/test_introspection_features.py (+1/-1) autopilot/tests/functional/test_process_emulator.py (+30/-5) autopilot/tests/unit/test_application_environment.py (+1/-57) autopilot/tests/unit/test_application_launcher.py (+310/-199) autopilot/tests/unit/test_introspection_features.py (+197/-0) autopilot/tests/unit/test_test_fixtures.py (+64/-18) autopilot/utilities.py (+18/-5) debian/control (+3/-0) |
To merge this branch: | bzr merge lp:~nuclearbob/autopilot/pep257-b |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Christopher Lee (community) | Approve | ||
PS Jenkins bot | continuous-integration | Needs Fixing | |
Review via email: mp+210633@code.launchpad.net |
Commit message
Additional pep257 changes.
Description of the change
Additional pep257 changes. I'm going to set this back to WIP and look at the diff.
Max Brustkern (nuclearbob) wrote : | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:451
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 452. By Max Brustkern
-
Fixed flake8 issue
- 453. By Max Brustkern
-
Merged trunk
- 454. By Max Brustkern
-
Resolved conflict with other branch
- 455. By Max Brustkern
-
Merged proposed release
Preview Diff
1 | === modified file 'autopilot/__init__.py' |
2 | --- autopilot/__init__.py 2014-02-21 16:36:39 +0000 |
3 | +++ autopilot/__init__.py 2014-03-20 12:37:25 +0000 |
4 | @@ -181,6 +181,7 @@ |
5 | |
6 | |
7 | def get_version_string(): |
8 | + """Return the autopilot source and package versions.""" |
9 | version_string = "Autopilot Source Version: " + _get_source_version() |
10 | pkg_version = _get_package_version() |
11 | if pkg_version: |
12 | |
13 | === modified file 'autopilot/application/__init__.py' |
14 | --- autopilot/application/__init__.py 2013-12-13 00:45:13 +0000 |
15 | +++ autopilot/application/__init__.py 2014-03-20 12:37:25 +0000 |
16 | @@ -21,12 +21,14 @@ |
17 | |
18 | from autopilot.application._launcher import ( |
19 | ClickApplicationLauncher, |
20 | + get_application_launcher_wrapper, |
21 | NormalApplicationLauncher, |
22 | - get_application_launcher_wrapper, |
23 | + UpstartApplicationLauncher, |
24 | ) |
25 | |
26 | __all__ = [ |
27 | 'ClickApplicationLauncher', |
28 | + 'get_application_launcher_wrapper', |
29 | 'NormalApplicationLauncher', |
30 | - 'get_application_launcher_wrapper', |
31 | + 'UpstartApplicationLauncher', |
32 | ] |
33 | |
34 | === modified file 'autopilot/application/_environment.py' |
35 | --- autopilot/application/_environment.py 2014-01-29 21:28:12 +0000 |
36 | +++ autopilot/application/_environment.py 2014-03-20 12:37:25 +0000 |
37 | @@ -21,7 +21,6 @@ |
38 | |
39 | import fixtures |
40 | import os |
41 | -import subprocess |
42 | |
43 | |
44 | class ApplicationEnvironment(fixtures.Fixture): |
45 | @@ -43,6 +42,8 @@ |
46 | """Prepare the application, or environment to launch with |
47 | autopilot-support. |
48 | |
49 | + :returns: unmodified app_path and arguments |
50 | + |
51 | """ |
52 | modules = os.getenv('GTK_MODULES', '').split(':') |
53 | if 'autopilot' not in modules: |
54 | @@ -58,6 +59,8 @@ |
55 | """Prepare the application, or environment to launch with |
56 | autopilot-support. |
57 | |
58 | + :returns: unmodified app_path and arguments |
59 | + |
60 | """ |
61 | if '-testability' not in arguments: |
62 | insert_pos = 0 |
63 | @@ -68,34 +71,3 @@ |
64 | arguments.insert(insert_pos, '-testability') |
65 | |
66 | return app_path, arguments |
67 | - |
68 | - |
69 | -class UpstartApplicationEnvironment(ApplicationEnvironment): |
70 | - |
71 | - def prepare_environment(self, app_path, arguments): |
72 | - """Prepare the application, or environment to launch with |
73 | - autopilot-support. |
74 | - |
75 | - """ |
76 | - _set_upstart_env("QT_LOAD_TESTABILITY", 1) |
77 | - self.addCleanup(_unset_upstart_env, "QT_LOAD_TESTABILITY") |
78 | - |
79 | - return app_path, arguments |
80 | - |
81 | - |
82 | -def _set_upstart_env(key, value): |
83 | - _call_upstart_with_args( |
84 | - "set-env", |
85 | - "{key}={value}".format(key=key, value=value) |
86 | - ) |
87 | - |
88 | - |
89 | -def _unset_upstart_env(key): |
90 | - _call_upstart_with_args("unset-env", key) |
91 | - |
92 | - |
93 | -def _call_upstart_with_args(*arguments): |
94 | - return subprocess.check_output( |
95 | - ["/sbin/initctl"] + list(arguments), |
96 | - universal_newlines=True |
97 | - ) |
98 | |
99 | === modified file 'autopilot/application/_launcher.py' |
100 | --- autopilot/application/_launcher.py 2014-03-09 20:10:51 +0000 |
101 | +++ autopilot/application/_launcher.py 2014-03-20 12:37:25 +0000 |
102 | @@ -20,6 +20,7 @@ |
103 | """Base module for application launchers.""" |
104 | |
105 | import fixtures |
106 | +from gi.repository import GLib, UpstartAppLaunch |
107 | import json |
108 | import logging |
109 | import os |
110 | @@ -32,10 +33,8 @@ |
111 | from autopilot._timeout import Timeout |
112 | from autopilot.utilities import _raise_on_unknown_kwargs |
113 | from autopilot.application._environment import ( |
114 | - _call_upstart_with_args, |
115 | GtkApplicationEnvironment, |
116 | QtApplicationEnvironment, |
117 | - UpstartApplicationEnvironment, |
118 | ) |
119 | |
120 | |
121 | @@ -43,10 +42,12 @@ |
122 | |
123 | |
124 | class ApplicationLauncher(fixtures.Fixture): |
125 | + |
126 | """A class that knows how to launch an application with a certain type of |
127 | introspection enabled. |
128 | |
129 | """ |
130 | + |
131 | def __init__(self, case_addDetail): |
132 | self.case_addDetail = case_addDetail |
133 | super(ApplicationLauncher, self).__init__() |
134 | @@ -55,9 +56,16 @@ |
135 | raise NotImplementedError("Sub-classes must implement this method.") |
136 | |
137 | |
138 | -class ClickApplicationLauncher(ApplicationLauncher): |
139 | +class UpstartApplicationLauncher(ApplicationLauncher): |
140 | + |
141 | + """A launcher class that launched applicaitons with UpstartAppLaunch.""" |
142 | + |
143 | + Timeout = object() |
144 | + Failed = object() |
145 | + Started = object() |
146 | + |
147 | def __init__(self, case_addDetail, **kwargs): |
148 | - super(ClickApplicationLauncher, self).__init__(case_addDetail) |
149 | + super(UpstartApplicationLauncher, self).__init__(case_addDetail) |
150 | |
151 | self.emulator_base = kwargs.pop('emulator_base', None) |
152 | self.dbus_bus = kwargs.pop('dbus_bus', 'session') |
153 | @@ -65,35 +73,100 @@ |
154 | |
155 | _raise_on_unknown_kwargs(kwargs) |
156 | |
157 | + def launch(self, app_id, app_uris=[]): |
158 | + state = {} |
159 | + state['loop'] = self._get_glib_loop() |
160 | + state['expected_app_id'] = app_id |
161 | + state['message'] = '' |
162 | + |
163 | + UpstartAppLaunch.observer_add_app_failed(self._on_failed, state) |
164 | + UpstartAppLaunch.observer_add_app_started(self._on_started, state) |
165 | + GLib.timeout_add_seconds(10.0, self._on_timeout, state) |
166 | + |
167 | + self._launch_app(app_id, app_uris) |
168 | + state['loop'].run() |
169 | + UpstartAppLaunch.observer_delete_app_failed(self._on_failed) |
170 | + UpstartAppLaunch.observer_delete_app_started(self._on_started) |
171 | + self._maybe_add_application_cleanups(state) |
172 | + self._check_status_error( |
173 | + state.get('status', None), |
174 | + state.get('message', '') |
175 | + ) |
176 | + return self._get_pid_for_launched_app(app_id) |
177 | + |
178 | + @staticmethod |
179 | + def _on_failed(launched_app_id, failure_type, state): |
180 | + if launched_app_id == state['expected_app_id']: |
181 | + if failure_type == UpstartAppLaunch.AppFailed.CRASH: |
182 | + state['message'] = 'Application crashed.' |
183 | + elif failure_type == UpstartAppLaunch.AppFailed.START_FAILURE: |
184 | + state['message'] = 'Application failed to start.' |
185 | + state['status'] = UpstartApplicationLauncher.Failed |
186 | + state['loop'].quit() |
187 | + |
188 | + @staticmethod |
189 | + def _on_started(launched_app_id, state): |
190 | + if launched_app_id == state['expected_app_id']: |
191 | + state['status'] = UpstartApplicationLauncher.Started |
192 | + state['loop'].quit() |
193 | + |
194 | + @staticmethod |
195 | + def _on_timeout(state): |
196 | + state['status'] = UpstartApplicationLauncher.Timeout |
197 | + state['loop'].quit() |
198 | + |
199 | + def _maybe_add_application_cleanups(self, state): |
200 | + if state.get('status', None) == UpstartApplicationLauncher.Started: |
201 | + app_id = state['expected_app_id'] |
202 | + self.addCleanup(self._stop_application, app_id) |
203 | + self.addCleanup(self._attach_application_log, app_id) |
204 | + |
205 | + def _attach_application_log(self, app_id): |
206 | + log_path = UpstartAppLaunch.application_log_path(app_id) |
207 | + if log_path: |
208 | + self.case_addDetail( |
209 | + "Application Log", |
210 | + content_from_file(log_path) |
211 | + ) |
212 | + |
213 | + def _stop_application(self, app_id): |
214 | + UpstartAppLaunch.stop_application(app_id) |
215 | + |
216 | + @staticmethod |
217 | + def _get_glib_loop(): |
218 | + return GLib.MainLoop() |
219 | + |
220 | + @staticmethod |
221 | + def _get_pid_for_launched_app(app_id): |
222 | + return UpstartAppLaunch.get_primary_pid(app_id) |
223 | + |
224 | + @staticmethod |
225 | + def _launch_app(app_name, app_uris): |
226 | + UpstartAppLaunch.start_application_test(app_name, app_uris) |
227 | + |
228 | + @staticmethod |
229 | + def _check_status_error(status, extra_message=''): |
230 | + message_parts = [] |
231 | + if status == UpstartApplicationLauncher.Timeout: |
232 | + message_parts.append( |
233 | + "Timed out while waiting for application to launch" |
234 | + ) |
235 | + elif status == UpstartApplicationLauncher.Failed: |
236 | + message_parts.append("Application Launch Failed") |
237 | + if message_parts and extra_message: |
238 | + message_parts.append(extra_message) |
239 | + if message_parts: |
240 | + raise RuntimeError(': '.join(message_parts)) |
241 | + |
242 | + |
243 | +class ClickApplicationLauncher(UpstartApplicationLauncher): |
244 | + |
245 | def launch(self, package_id, app_name, app_uris): |
246 | app_id = _get_click_app_id(package_id, app_name) |
247 | - |
248 | - _app_env = self.useFixture(UpstartApplicationEnvironment()) |
249 | - _app_env.prepare_environment(app_id, app_name) |
250 | - |
251 | - return self._launch_click_app(app_id, app_uris) |
252 | - |
253 | - def _launch_click_app(self, app_id, app_uris): |
254 | - pid = _launch_click_app(app_id, app_uris) |
255 | - self._add_click_launch_cleanup(app_id, pid) |
256 | - |
257 | - logger.info( |
258 | - "Click package %s has been launched with PID %d", |
259 | - app_id, |
260 | - pid |
261 | - ) |
262 | - |
263 | - return pid |
264 | - |
265 | - def _add_click_launch_cleanup(self, app_id, pid): |
266 | - self.addCleanup(_kill_pid, pid) |
267 | - self.addCleanup(self._add_log_cleanup, app_id) |
268 | - |
269 | - def _add_log_cleanup(self, app_id): |
270 | - self.case_addDetail( |
271 | - "Application Log", |
272 | - _get_click_application_log_content_object(app_id) |
273 | - ) |
274 | + return self._do_upstart_launch(app_id, app_uris) |
275 | + |
276 | + def _do_upstart_launch(self, app_id, app_uris): |
277 | + return super(ClickApplicationLauncher, self).launch(app_id, app_uris) |
278 | |
279 | |
280 | class NormalApplicationLauncher(ApplicationLauncher): |
281 | @@ -173,45 +246,6 @@ |
282 | return process |
283 | |
284 | |
285 | -def _is_process_running(pid): |
286 | - return psutil.pid_exists(pid) |
287 | - |
288 | - |
289 | -def _launch_click_app(app_id, app_uris): |
290 | - subprocess.check_output([ |
291 | - "/sbin/start", |
292 | - "application", |
293 | - "APP_ID={}".format(app_id), |
294 | - "APP_URIS='{}'".format(app_uris), |
295 | - ]) |
296 | - |
297 | - return _get_click_app_pid(app_id) |
298 | - |
299 | - |
300 | -def _get_click_app_status(app_id): |
301 | - return _call_upstart_with_args( |
302 | - "status", |
303 | - "application-click", |
304 | - "APP_ID={}".format(app_id) |
305 | - ) |
306 | - |
307 | - |
308 | -def _get_click_application_log_content_object(app_id): |
309 | - try: |
310 | - return content_from_file( |
311 | - _get_click_application_log_path(app_id), |
312 | - buffer_now=True |
313 | - ) |
314 | - except IOError as e: |
315 | - return text_content(u'Unable to open application log: %s' % (e,)) |
316 | - |
317 | - |
318 | -def _get_click_application_log_path(app_id): |
319 | - log_dir = os.path.expanduser('~/.cache/upstart/') |
320 | - log_name = 'application-click-{}.log'.format(app_id) |
321 | - return os.path.join(log_dir, log_name) |
322 | - |
323 | - |
324 | def _get_click_app_id(package_id, app_name=None): |
325 | for pkg in _get_click_manifest(): |
326 | if pkg['name'] == package_id: |
327 | @@ -241,47 +275,6 @@ |
328 | return json.loads(click_manifest_str) |
329 | |
330 | |
331 | -def _get_click_app_pid(app_id): |
332 | - for _ in Timeout.default(): |
333 | - try: |
334 | - list_output = _get_click_app_status(app_id) |
335 | - except subprocess.CalledProcessError: |
336 | - # application not started yet. |
337 | - pass |
338 | - else: |
339 | - for line in list_output.split('\n'): |
340 | - if app_id in line and "start/running" in line: |
341 | - return int(line.split()[-1]) |
342 | - else: |
343 | - raise RuntimeError( |
344 | - "Could not find autopilot interface for click package" |
345 | - " '{}' after 10 seconds.".format(app_id) |
346 | - ) |
347 | - |
348 | - |
349 | -def _kill_pid(pid): |
350 | - """Kill the process with the specified pid.""" |
351 | - logger.info("waiting for process to exit.") |
352 | - _attempt_kill_pid(pid) |
353 | - for _ in Timeout.default(): |
354 | - if not _is_process_running(pid): |
355 | - break |
356 | - else: |
357 | - logger.info( |
358 | - "Killing process group, since it hasn't exited after the default" |
359 | - "timeout." |
360 | - ) |
361 | - _attempt_kill_pid(pid, signal.SIGKILL) |
362 | - |
363 | - |
364 | -def _attempt_kill_pid(pid, sig=signal.SIGTERM): |
365 | - try: |
366 | - logger.info("Killing process %d", pid) |
367 | - os.killpg(pid, sig) |
368 | - except OSError: |
369 | - logger.info("Appears process has already exited.") |
370 | - |
371 | - |
372 | def _get_application_environment(app_type=None, app_path=None): |
373 | if app_type is None and app_path is None: |
374 | raise ValueError("Must specify either app_type or app_path.") |
375 | @@ -368,3 +361,15 @@ |
376 | ) |
377 | _attempt_kill_pid(process.pid, signal.SIGKILL) |
378 | return u''.join(stdout_parts), u''.join(stderr_parts), process.returncode |
379 | + |
380 | + |
381 | +def _attempt_kill_pid(pid, sig=signal.SIGTERM): |
382 | + try: |
383 | + logger.info("Killing process %d", pid) |
384 | + os.killpg(pid, sig) |
385 | + except OSError: |
386 | + logger.info("Appears process has already exited.") |
387 | + |
388 | + |
389 | +def _is_process_running(pid): |
390 | + return psutil.pid_exists(pid) |
391 | |
392 | === modified file 'autopilot/dbus_handler.py' |
393 | --- autopilot/dbus_handler.py 2013-07-22 04:36:50 +0000 |
394 | +++ autopilot/dbus_handler.py 2014-03-20 12:37:25 +0000 |
395 | @@ -42,7 +42,7 @@ |
396 | |
397 | |
398 | def get_session_bus(): |
399 | - """This function returns a session bus that has had the DBus GLib main loop |
400 | + """Return a session bus that has had the DBus GLib main loop |
401 | initialised. |
402 | |
403 | """ |
404 | @@ -51,7 +51,7 @@ |
405 | |
406 | |
407 | def get_system_bus(): |
408 | - """This function returns a system bus that has had the DBus GLib main loop |
409 | + """Return a system bus that has had the DBus GLib main loop |
410 | initialised. |
411 | |
412 | """ |
413 | @@ -60,7 +60,7 @@ |
414 | |
415 | |
416 | def get_custom_bus(bus_address): |
417 | - """This function returns a custom bus that has had the DBus GLib main loop |
418 | + """Return a custom bus that has had the DBus GLib main loop |
419 | initialised. |
420 | |
421 | """ |
422 | |
423 | === modified file 'autopilot/display/_X11.py' |
424 | --- autopilot/display/_X11.py 2013-07-19 07:03:40 +0000 |
425 | +++ autopilot/display/_X11.py 2014-03-20 12:37:25 +0000 |
426 | @@ -44,11 +44,15 @@ |
427 | self._blacklisted_drivers = ["NVIDIA"] |
428 | |
429 | def get_num_screens(self): |
430 | - """Get the number of screens attached to the PC.""" |
431 | + """Get the number of screens attached to the PC. |
432 | + |
433 | + :returns: int indicating number of screens attached. |
434 | + |
435 | + """ |
436 | return self._default_screen.get_n_monitors() |
437 | |
438 | def get_primary_screen(self): |
439 | - """Returns an integer of which screen is considered the primary""" |
440 | + """Return an integer of which screen is considered the primary.""" |
441 | return self._default_screen.get_primary_monitor() |
442 | |
443 | def get_screen_width(self, screen_number=0): |
444 | |
445 | === modified file 'autopilot/display/__init__.py' |
446 | --- autopilot/display/__init__.py 2014-02-12 01:07:08 +0000 |
447 | +++ autopilot/display/__init__.py 2014-03-20 12:37:25 +0000 |
448 | @@ -26,7 +26,7 @@ |
449 | |
450 | |
451 | def is_rect_on_screen(screen_number, rect): |
452 | - """Returns True if *rect* is **entirely** on the specified screen, with no |
453 | + """Return True if *rect* is **entirely** on the specified screen, with no |
454 | overlap.""" |
455 | (x, y, w, h) = rect |
456 | (mx, my, mw, mh) = Display.create().get_screen_geometry(screen_number) |
457 | @@ -34,7 +34,7 @@ |
458 | |
459 | |
460 | def is_point_on_screen(screen_number, point): |
461 | - """Returns True if *point* is on the specified screen. |
462 | + """Return True if *point* is on the specified screen. |
463 | |
464 | *point* must be an iterable type with two elements: (x, y) |
465 | |
466 | @@ -45,7 +45,7 @@ |
467 | |
468 | |
469 | def is_point_on_any_screen(point): |
470 | - """Returns true if *point* is on any currently configured screen.""" |
471 | + """Return true if *point* is on any currently configured screen.""" |
472 | return any([is_point_on_screen(m, point) for m in |
473 | range(Display.create().get_num_screens())]) |
474 | |
475 | @@ -99,7 +99,8 @@ |
476 | |
477 | |
478 | class Display(object): |
479 | - """The base class/inteface for the display devices""" |
480 | + |
481 | + """The base class/inteface for the display devices.""" |
482 | |
483 | @staticmethod |
484 | def create(preferred_backend=''): |
485 | @@ -121,6 +122,7 @@ |
486 | one of the possible backends for this device class. |
487 | :raises: :class:`~autopilot.BackendException` if the preferred_backend |
488 | is set, but that backend could not be instantiated. |
489 | + :returns: Instance of Display with appropriate backend. |
490 | |
491 | """ |
492 | def get_x11_display(): |
493 | |
494 | === modified file 'autopilot/globals.py' |
495 | --- autopilot/globals.py 2014-01-14 01:50:17 +0000 |
496 | +++ autopilot/globals.py 2014-03-20 12:37:25 +0000 |
497 | @@ -39,11 +39,12 @@ |
498 | |
499 | |
500 | def get_log_verbose(): |
501 | - """Returns true if the user asked for verbose logging.""" |
502 | + """Return true if the user asked for verbose logging.""" |
503 | return _test_logger._log_verbose |
504 | |
505 | |
506 | class _TestLogger(CleanupRegistered): |
507 | + |
508 | """A class that handles adding test logs as test result content.""" |
509 | |
510 | def __init__(self): |
511 | @@ -98,6 +99,7 @@ |
512 | |
513 | |
514 | class _VideoLogger(CleanupRegistered): |
515 | + |
516 | """Video capture autopilot tests, saving the results if the test failed.""" |
517 | |
518 | _recording_app = '/usr/bin/recordmydesktop' |
519 | |
520 | === modified file 'autopilot/introspection/dbus.py' |
521 | --- autopilot/introspection/dbus.py 2014-03-11 16:39:25 +0000 |
522 | +++ autopilot/introspection/dbus.py 2014-03-20 12:37:25 +0000 |
523 | @@ -413,7 +413,7 @@ |
524 | of the appropriate type (the latter case is for overridden emulator |
525 | classes). |
526 | |
527 | - :raises: **TypeError** if neither *type_name* or keyword filters are |
528 | + :raises TypeError: if neither *type_name* or keyword filters are |
529 | provided. |
530 | |
531 | .. seealso:: |
532 | @@ -466,7 +466,7 @@ |
533 | automatically retrieves new state every time this object's attributes |
534 | are read. |
535 | |
536 | - :raises: **StateNotFound** if the object in the application under test |
537 | + :raises StateNotFound: if the object in the application under test |
538 | has been destroyed. |
539 | |
540 | """ |
541 | @@ -530,7 +530,7 @@ |
542 | |
543 | :param piece: an XPath-like query that specifies which bit of the tree |
544 | you want to look at. |
545 | - :raises: **TypeError** on invalid *piece* parameter. |
546 | + :raises TypeError: on invalid *piece* parameter. |
547 | |
548 | """ |
549 | if not isinstance(piece, six.string_types): |
550 | @@ -598,25 +598,16 @@ |
551 | (path, state_dict). |
552 | |
553 | This only works for classes that derive from DBusIntrospectionObject. |
554 | + |
555 | + :returns: A proxy object that derives from DBusIntrospectionObject |
556 | + :raises ValueError: if more than one class is appropriate for this |
557 | + dbus_tuple |
558 | + |
559 | """ |
560 | path, state = dbus_tuple |
561 | - name = get_classname_from_path(path) |
562 | - try: |
563 | - class_type = _object_registry[self._id][name] |
564 | - except KeyError: |
565 | - get_debug_logger().warning( |
566 | - "Generating introspection instance for type '%s' based on " |
567 | - "generic class.", name) |
568 | - # we want the object to inherit from the custom emulator base, not |
569 | - # the object class that is doing the selecting |
570 | - for base in type(self).__bases__: |
571 | - if issubclass(base, CustomEmulatorBase): |
572 | - base_class = base |
573 | - break |
574 | - else: |
575 | - base_class = type(self) |
576 | - class_type = type(str(name), (base_class,), {}) |
577 | - return class_type(state, path, self._backend) |
578 | + class_object = _get_proxy_object_class( |
579 | + _object_registry[self._id], type(self), path, state) |
580 | + return class_object(state, path, self._backend) |
581 | |
582 | def print_tree(self, output=None, maxdepth=None, _curdepth=0): |
583 | """Print properties of the object and its children to a stream. |
584 | @@ -678,6 +669,24 @@ |
585 | finally: |
586 | self.__refresh_on_attribute = True |
587 | |
588 | + @classmethod |
589 | + def validate_dbus_object(cls, path, _state): |
590 | + """Return whether this class is the appropriate proxy object class for |
591 | + a given dbus path and state. |
592 | + |
593 | + The default version matches the name of the dbus object and the class. |
594 | + Subclasses of CustomProxyObject can override it to define a different |
595 | + validation method. |
596 | + |
597 | + :param path: The dbus path of the object to check |
598 | + :param state: The dbus state dict of the object to check |
599 | + (ignored in default implementation) |
600 | + :returns: Whether this class is appropriate for the dbus object |
601 | + |
602 | + """ |
603 | + name = get_classname_from_path(path) |
604 | + return cls.__name__ == name |
605 | + |
606 | |
607 | def _is_valid_server_side_filter_param(key, value): |
608 | """Return True if the key and value parameters are valid for server-side |
609 | @@ -741,6 +750,72 @@ |
610 | raise ValueError("Unsupported value type: {}".format(type(value))) |
611 | |
612 | |
613 | +def _get_proxy_object_class(proxy_class_dict, default_class, path, state): |
614 | + """Return a custom proxy class, either from the list or the default. |
615 | + |
616 | + Use helper functions to check the class list or return the default. |
617 | + :param proxy_class_dict: dict of proxy classes to try |
618 | + :param default_class: default class to use if nothing in dict matches |
619 | + :param path: dbus path |
620 | + :param state: dbus state |
621 | + :returns: appropriate custom proxy class |
622 | + :raises ValueError: if more than one class in the dict matches |
623 | + |
624 | + """ |
625 | + class_type = _try_custom_proxy_classes(proxy_class_dict, path, state) |
626 | + if class_type: |
627 | + return class_type |
628 | + return _get_default_proxy_class(default_class, |
629 | + get_classname_from_path(path)) |
630 | + |
631 | + |
632 | +def _try_custom_proxy_classes(proxy_class_dict, path, state): |
633 | + """Identify which custom proxy class matches the dbus path and state. |
634 | + |
635 | + If more than one class in proxy_class_dict matches, raise an exception. |
636 | + :param proxy_class_dict: dict of proxy classes to try |
637 | + :param path: dbus path |
638 | + :param state: dbus state dict |
639 | + :returns: matching custom proxy class |
640 | + :raises ValueError: if more than one class matches |
641 | + |
642 | + """ |
643 | + possible_classes = [c for c in proxy_class_dict.values() if |
644 | + c.validate_dbus_object(path, state)] |
645 | + if len(possible_classes) > 1: |
646 | + raise ValueError( |
647 | + 'More than one custom proxy class matches this object: ' |
648 | + 'Matching classes are: %s. State is %s. Path is %s.' |
649 | + ','.join([repr(c) for c in possible_classes]), |
650 | + repr(state), |
651 | + path) |
652 | + if len(possible_classes) == 1: |
653 | + return possible_classes[0] |
654 | + return None |
655 | + |
656 | + |
657 | +def _get_default_proxy_class(default_class, name): |
658 | + """Return a custom proxy object class of the default or a base class. |
659 | + |
660 | + We want the object to inherit from CustomEmulatorBase, not the object |
661 | + class that is doing the selecting. |
662 | + :param default_class: default class to use if no bases match |
663 | + :param name: name of new class |
664 | + :returns: custom proxy object class |
665 | + |
666 | + """ |
667 | + get_debug_logger().warning( |
668 | + "Generating introspection instance for type '%s' based on generic " |
669 | + "class.", name) |
670 | + for base in default_class.__bases__: |
671 | + if issubclass(base, CustomEmulatorBase): |
672 | + base_class = base |
673 | + break |
674 | + else: |
675 | + base_class = default_class |
676 | + return type(str(name), (base_class,), {}) |
677 | + |
678 | + |
679 | class _CustomEmulatorMeta(IntrospectableObjectMetaclass): |
680 | |
681 | def __new__(cls, name, bases, d): |
682 | |
683 | === modified file 'autopilot/keybindings.py' |
684 | --- autopilot/keybindings.py 2013-10-13 21:49:49 +0000 |
685 | +++ autopilot/keybindings.py 2014-03-20 12:37:25 +0000 |
686 | @@ -137,8 +137,10 @@ |
687 | """Get a keybinding, given its well-known name. |
688 | |
689 | :param string binding_name: |
690 | - :raises: **TypeError** if binding_name cannot be found in the bindings |
691 | - dictionaries. |
692 | + :raises TypeError: if binding_name is not a string |
693 | + :raises ValueError: if binding_name cannot be found in the bindings |
694 | + dictionaries. |
695 | + :returns: string for keybinding |
696 | |
697 | """ |
698 | if not isinstance(binding_name, six.string_types): |
699 | @@ -153,12 +155,12 @@ |
700 | |
701 | |
702 | def get_hold_part(binding_name): |
703 | - """Returns the part of a keybinding that must be held permanently. |
704 | + """Return the part of a keybinding that must be held permanently. |
705 | |
706 | Use this function to split bindings like "Alt+Tab" into the part that must |
707 | be held down. See :meth:`get_tap_part` for the part that must be tapped. |
708 | |
709 | - :raises: **ValueError** if the binding specified does not have multiple |
710 | + :raises ValueError: if the binding specified does not have multiple |
711 | parts. |
712 | |
713 | """ |
714 | @@ -172,13 +174,13 @@ |
715 | |
716 | |
717 | def get_tap_part(binding_name): |
718 | - """Returns the part of a keybinding that must be tapped. |
719 | + """Return the part of a keybinding that must be tapped. |
720 | |
721 | Use this function to split bindings like "Alt+Tab" into the part that must |
722 | be held tapped. See :meth:`get_hold_part` for the part that must be held |
723 | down. |
724 | |
725 | - :Raises: **ValueError** if the binding specified does not have multiple |
726 | + :raises ValueError: if the binding specified does not have multiple |
727 | parts. |
728 | |
729 | """ |
730 | @@ -195,9 +197,10 @@ |
731 | """Given a keybinding name, get the keybinding string from the compiz |
732 | option. |
733 | |
734 | - :raises: **ValueError** if the compiz setting described does not hold a |
735 | + :raises ValueError: if the compiz setting described does not hold a |
736 | keybinding. |
737 | - :raises: **RuntimeError** if the compiz keybinding has been disabled. |
738 | + :raises RuntimeError: if the compiz keybinding has been disabled. |
739 | + :returns: compiz keybinding |
740 | |
741 | """ |
742 | plugin_name, setting_name = compiz_tuple |
743 | @@ -249,6 +252,7 @@ |
744 | |
745 | |
746 | class KeybindingsHelper(object): |
747 | + |
748 | """A helper class that makes it easier to use Unity keybindings.""" |
749 | |
750 | @property |
751 | @@ -294,7 +298,12 @@ |
752 | |
753 | |
754 | def _get_global_compiz_context(): |
755 | - """Get the compizconfig global context object.""" |
756 | + """Get the compizconfig global context object. |
757 | + |
758 | + :returns: global compiz context, either already defined or from compiz |
759 | + config |
760 | + |
761 | + """ |
762 | global _global_compiz_context |
763 | if _global_compiz_context is None: |
764 | with Silence(): |
765 | @@ -306,7 +315,8 @@ |
766 | def _get_compiz_plugin(plugin_name): |
767 | """Get a compizconfig plugin with the specified name. |
768 | |
769 | - Raises KeyError of the plugin named does not exist. |
770 | + :raises KeyError: if the plugin named does not exist. |
771 | + :returns: compizconfig plugin |
772 | |
773 | """ |
774 | ctx = _get_global_compiz_context() |
775 | @@ -321,7 +331,8 @@ |
776 | def _get_compiz_setting(plugin_name, setting_name): |
777 | """Get a compizconfig setting object, given a plugin name and setting name. |
778 | |
779 | - Raises KeyError if the plugin or setting is not found. |
780 | + :raises KeyError: if the plugin or setting is not found. |
781 | + :returns: compiz setting object |
782 | |
783 | """ |
784 | plugin = _get_compiz_plugin(plugin_name) |
785 | |
786 | === modified file 'autopilot/platform.py' |
787 | --- autopilot/platform.py 2014-01-27 22:23:58 +0000 |
788 | +++ autopilot/platform.py 2014-03-20 12:37:25 +0000 |
789 | @@ -130,6 +130,8 @@ |
790 | |
791 | ... True |
792 | |
793 | + :returns: boolean indicating whether this is a tablet |
794 | + |
795 | """ |
796 | return _PlatformDetector.create().is_tablet |
797 | |
798 | |
799 | === modified file 'autopilot/run.py' |
800 | --- autopilot/run.py 2014-03-09 23:01:45 +0000 |
801 | +++ autopilot/run.py 2014-03-20 12:37:25 +0000 |
802 | @@ -53,7 +53,7 @@ |
803 | |
804 | |
805 | def setup_logging(verbose): |
806 | - """Configure the root logger and verbose logging to stderr""" |
807 | + """Configure the root logger and verbose logging to stderr.""" |
808 | root_logger = get_root_logger() |
809 | root_logger.setLevel(logging.DEBUG) |
810 | if verbose == 0: |
811 | @@ -173,6 +173,7 @@ |
812 | """Get the on-disk location of a package from a test id name. |
813 | |
814 | :raises ImportError: if the name could not be found. |
815 | + :returns: path as a string |
816 | """ |
817 | top_level_pkg = import_name.split('.')[0] |
818 | _, path, _ = find_module(top_level_pkg, [os.getcwd()] + sys.path) |
819 | @@ -201,9 +202,9 @@ |
820 | |
821 | |
822 | def _discover_test(test_name): |
823 | - """Returns tuple of TestSuite of found test , top_level_dir of test |
824 | + """Return tuple of (TestSuite of found test, top_level_dir of test). |
825 | |
826 | - raises ImportError if test_name isn't a valid module or test name |
827 | + :raises ImportError: if test_name isn't a valid module or test name |
828 | |
829 | """ |
830 | loader = TestLoader() |
831 | @@ -223,7 +224,7 @@ |
832 | |
833 | |
834 | def _discover_requested_tests(test_names): |
835 | - """Returns a collection of tests that are under test_names. |
836 | + """Return a collection of tests that are under test_names. |
837 | |
838 | returns a tuple containig a TestSuite of tests found and a boolean |
839 | depicting wherether any difficulties were encountered while searching |
840 | @@ -252,7 +253,7 @@ |
841 | |
842 | |
843 | def _filter_tests(all_tests, test_names): |
844 | - """Filters a given TestSuite for tests starting with any name contained |
845 | + """Filter a given TestSuite for tests starting with any name contained |
846 | within test_names. |
847 | |
848 | """ |
849 | @@ -278,7 +279,7 @@ |
850 | |
851 | |
852 | def load_test_suite_from_name(test_names): |
853 | - """Returns a test suite object given a dotted test names. |
854 | + """Return a test suite object given a dotted test names. |
855 | |
856 | Returns a tuple containing the TestSuite and a boolean indicating wherever |
857 | any issues where encountered during the loading process. |
858 | @@ -383,7 +384,7 @@ |
859 | |
860 | |
861 | def _get_app_name_and_args(argument_list): |
862 | - """Returns a tuple of (app_name, [app_args]).""" |
863 | + """Return a tuple of (app_name, [app_args]).""" |
864 | return argument_list[0], argument_list[1:] |
865 | |
866 | |
867 | |
868 | === modified file 'autopilot/testcase.py' |
869 | --- autopilot/testcase.py 2014-02-12 01:12:55 +0000 |
870 | +++ autopilot/testcase.py 2014-03-20 12:37:25 +0000 |
871 | @@ -50,6 +50,7 @@ |
872 | |
873 | import logging |
874 | import os |
875 | +import six |
876 | |
877 | from testscenarios import TestWithScenarios |
878 | from testtools import TestCase |
879 | @@ -59,6 +60,7 @@ |
880 | ClickApplicationLauncher, |
881 | get_application_launcher_wrapper, |
882 | NormalApplicationLauncher, |
883 | + UpstartApplicationLauncher, |
884 | ) |
885 | from autopilot.display import Display |
886 | from autopilot.globals import get_debug_profile_fixture |
887 | @@ -122,6 +124,7 @@ |
888 | |
889 | |
890 | class AutopilotTestCase(TestWithScenarios, TestCase, KeybindingsHelper): |
891 | + |
892 | """Wrapper around testtools.TestCase that adds significant functionality. |
893 | |
894 | This class should be the base class for all autopilot test case classes. |
895 | @@ -241,18 +244,24 @@ |
896 | :keyword emulator_base: If set, specifies the base class to be used for |
897 | all emulators for this loaded application. |
898 | |
899 | - :raises: **ValueError** if unknown keyword arguments are passed. |
900 | + :raises ValueError: if unknown keyword arguments are passed. |
901 | :return: A proxy object that represents the application. Introspection |
902 | data is retrievable via this object. |
903 | |
904 | """ |
905 | + logger.info( |
906 | + "Attempting to launch application '%s' with arguments '%s' as a " |
907 | + "normal process", |
908 | + application, |
909 | + ' '.join(arguments) |
910 | + ) |
911 | launcher = self.useFixture( |
912 | NormalApplicationLauncher(self.addDetail, **kwargs) |
913 | ) |
914 | |
915 | return self._launch_test_application(launcher, application, *arguments) |
916 | |
917 | - def launch_click_package(self, package_id, app_name=None, app_uris="", |
918 | + def launch_click_package(self, package_id, app_name=None, app_uris=[], |
919 | **kwargs): |
920 | """Launch a click package application with introspection enabled. |
921 | |
922 | @@ -283,13 +292,55 @@ |
923 | :raises RuntimeError: If the specified app_name cannot be found within |
924 | the specified click package. |
925 | |
926 | + :returns: proxy object for the launched package application |
927 | + |
928 | """ |
929 | + if isinstance(app_uris, (six.text_type, six.binary_type)): |
930 | + app_uris = [app_uris] |
931 | + logger.info( |
932 | + "Attempting to launch click application '%s' from click package " |
933 | + " '%s' and URIs '%s'", |
934 | + app_name if app_name is not None else "(default)", |
935 | + package_id, |
936 | + ','.join(app_uris) |
937 | + ) |
938 | launcher = self.useFixture( |
939 | ClickApplicationLauncher(self.addDetail, **kwargs) |
940 | ) |
941 | return self._launch_test_application(launcher, package_id, app_name, |
942 | app_uris) |
943 | |
944 | + def launch_upstart_application(self, application_name, uris=[], **kwargs): |
945 | + """Launch an application with upstart. |
946 | + |
947 | + This method launched an application via the ``upstart-app-launch`` |
948 | + library, on platforms that support it. |
949 | + |
950 | + Usage is similar to the |
951 | + :py:meth:`AutopilotTestCase.launch_test_application`:: |
952 | + |
953 | + app_proxy = self.launch_upstart_application("gallery-app") |
954 | + |
955 | + :param application_name: The name of the application to launch. |
956 | + :keyword emulator_base: If set, specifies the base class to be used for |
957 | + all emulators for this loaded application. |
958 | + |
959 | + :raises RuntimeError: If the specified application cannot be launched. |
960 | + :raises ValueError: If unknown keyword arguments are specified. |
961 | + """ |
962 | + if isinstance(uris, (six.text_type, six.binary_type)): |
963 | + uris = [uris] |
964 | + logger.info( |
965 | + "Attempting to launch application '%s' with URIs '%s' via " |
966 | + "upstart-app-launch", |
967 | + application_name, |
968 | + ','.join(uris) |
969 | + ) |
970 | + launcher = self.useFixture( |
971 | + UpstartApplicationLauncher(self.addDetail, **kwargs) |
972 | + ) |
973 | + return self._launch_test_application(launcher, application_name, uris) |
974 | + |
975 | # Wrapper function tying the newer ApplicationLauncher behaviour with the |
976 | # previous (to be depreciated) behaviour |
977 | def _launch_test_application(self, launcher_instance, application, *args): |
978 | @@ -397,7 +448,7 @@ |
979 | |
980 | :param stack_start: An iterable of |
981 | :class:`~autopilot.process.Window` instances. |
982 | - :raises: **AssertionError** if the top of the window stack does not |
983 | + :raises AssertionError: if the top of the window stack does not |
984 | match the contents of the stack_start parameter. |
985 | |
986 | """ |
987 | @@ -427,9 +478,9 @@ |
988 | :param obj: The object to test. |
989 | :param kwargs: One or more keyword arguments to match against the |
990 | attributes of the *obj* parameter. |
991 | - :raises: **ValueError** if no keyword arguments were given. |
992 | - :raises: **ValueError** if a named attribute is a callable object. |
993 | - :raises: **AssertionError** if any of the attribute/value pairs in |
994 | + :raises ValueError: if no keyword arguments were given. |
995 | + :raises ValueError: if a named attribute is a callable object. |
996 | + :raises AssertionError: if any of the attribute/value pairs in |
997 | kwargs do not match the attributes on the object passed in. |
998 | |
999 | """ |
1000 | @@ -485,7 +536,7 @@ |
1001 | |
1002 | |
1003 | def _compare_system_with_process_snapshot(snapshot_fn, old_snapshot): |
1004 | - """Compares an existing process snapshot with current running processes. |
1005 | + """Compare an existing process snapshot with current running processes. |
1006 | |
1007 | :param snapshot_fn: A callable that returns the current running process |
1008 | list. |
1009 | |
1010 | === modified file 'autopilot/testresult.py' |
1011 | --- autopilot/testresult.py 2014-02-26 01:23:18 +0000 |
1012 | +++ autopilot/testresult.py 2014-03-20 12:37:25 +0000 |
1013 | @@ -37,15 +37,16 @@ |
1014 | |
1015 | |
1016 | class LoggedTestResultDecorator(TestResultDecorator): |
1017 | + |
1018 | """A decorator that logs messages to python's logging system.""" |
1019 | |
1020 | def _log(self, level, message): |
1021 | - """Performs the actual message logging""" |
1022 | + """Perform the actual message logging.""" |
1023 | if get_log_verbose(): |
1024 | logging.getLogger().log(level, message) |
1025 | |
1026 | def _log_details(self, level, details): |
1027 | - """Logs the relavent test details""" |
1028 | + """Log the relavent test details.""" |
1029 | for detail in details: |
1030 | # Skip the test-log as it was logged while the test executed |
1031 | if detail == "test-log": |
1032 | @@ -64,7 +65,7 @@ |
1033 | return super(type(self), self).addError(test, err, details) |
1034 | |
1035 | def addFailure(self, test, err=None, details=None): |
1036 | - """Called for a test which failed an assert""" |
1037 | + """Called for a test which failed an assert.""" |
1038 | self._log(logging.ERROR, "FAIL: %s" % (test.id())) |
1039 | if hasattr(test, "getDetails"): |
1040 | self._log_details(logging.ERROR, test.getDetails()) |
1041 | @@ -72,7 +73,11 @@ |
1042 | |
1043 | |
1044 | def get_output_formats(): |
1045 | - """Get information regarding the different output formats supported.""" |
1046 | + """Get information regarding the different output formats supported. |
1047 | + |
1048 | + :returns: dict of supported formats and appropriate construct functions |
1049 | + |
1050 | + """ |
1051 | supported_formats = {} |
1052 | |
1053 | supported_formats['text'] = _construct_text |
1054 | |
1055 | === modified file 'autopilot/tests/functional/__init__.py' |
1056 | --- autopilot/tests/functional/__init__.py 2014-03-10 06:02:13 +0000 |
1057 | +++ autopilot/tests/functional/__init__.py 2014-03-20 12:37:25 +0000 |
1058 | @@ -21,16 +21,14 @@ |
1059 | from __future__ import absolute_import |
1060 | |
1061 | from codecs import open |
1062 | -import fixtures |
1063 | import os |
1064 | import os.path |
1065 | import sys |
1066 | import logging |
1067 | from shutil import rmtree |
1068 | import subprocess |
1069 | -from tempfile import mkdtemp, mkstemp |
1070 | +from tempfile import mkdtemp |
1071 | from testtools.content import text_content |
1072 | -from textwrap import dedent |
1073 | |
1074 | from autopilot.testcase import AutopilotTestCase |
1075 | |
1076 | @@ -150,89 +148,6 @@ |
1077 | return script_file |
1078 | |
1079 | |
1080 | -class TempDesktopFile(fixtures.Fixture): |
1081 | - def setUp(self): |
1082 | - super(TempDesktopFile, self).setUp() |
1083 | - path_created = TempDesktopFile._ensure_desktop_dir_exists() |
1084 | - self._desktop_file_path = self._create_desktop_file() |
1085 | - |
1086 | - self.addCleanup( |
1087 | - TempDesktopFile._remove_desktop_file_components, |
1088 | - path_created, |
1089 | - self._desktop_file_path, |
1090 | - ) |
1091 | - |
1092 | - def get_desktop_file_path(self): |
1093 | - return self._desktop_file_path |
1094 | - |
1095 | - @staticmethod |
1096 | - def _ensure_desktop_dir_exists(): |
1097 | - desktop_file_dir = TempDesktopFile._desktop_file_dir() |
1098 | - if not os.path.exists(desktop_file_dir): |
1099 | - return TempDesktopFile._create_desktop_file_dir(desktop_file_dir) |
1100 | - return '' |
1101 | - |
1102 | - @staticmethod |
1103 | - def _desktop_file_dir(): |
1104 | - return os.path.join( |
1105 | - os.getenv('HOME'), |
1106 | - '.local', |
1107 | - 'share', |
1108 | - 'applications' |
1109 | - ) |
1110 | - |
1111 | - @staticmethod |
1112 | - def _create_desktop_file_dir(desktop_file_dir): |
1113 | - """Create the directory specified. |
1114 | - |
1115 | - Returns the component of the path that did not exist, or the empty |
1116 | - string if the entire path already existed. |
1117 | - |
1118 | - """ |
1119 | - # We might be creating more than just the leaf directory, so we need to |
1120 | - # keep track of what doesn't already exist and remove it when we're |
1121 | - # done. Defaults to removing the full path |
1122 | - path_to_delete = "" |
1123 | - if not os.path.exists(desktop_file_dir): |
1124 | - path_to_delete = desktop_file_dir |
1125 | - full_path, leaf = os.path.split(desktop_file_dir) |
1126 | - while leaf != "": |
1127 | - if not os.path.exists(full_path): |
1128 | - path_to_delete = full_path |
1129 | - full_path, leaf = os.path.split(full_path) |
1130 | - |
1131 | - try: |
1132 | - os.makedirs(desktop_file_dir) |
1133 | - except OSError: |
1134 | - logger.warning("Directory already exists: %s" % desktop_file_dir) |
1135 | - return path_to_delete |
1136 | - |
1137 | - @staticmethod |
1138 | - def _remove_desktop_file_components(created_path, created_file): |
1139 | - if created_path != "": |
1140 | - rmtree(created_path) |
1141 | - else: |
1142 | - os.remove(created_file) |
1143 | - |
1144 | - @staticmethod |
1145 | - def _create_desktop_file(): |
1146 | - _, tmp_file_path = mkstemp( |
1147 | - suffix='.desktop', |
1148 | - dir=TempDesktopFile._desktop_file_dir() |
1149 | - ) |
1150 | - with open(tmp_file_path, 'w') as desktop_file: |
1151 | - desktop_file.write( |
1152 | - dedent("""\ |
1153 | - [Desktop Entry] |
1154 | - Type=Application |
1155 | - Exec=Not important |
1156 | - Path=Not important |
1157 | - Name=Test app |
1158 | - Icon=Not important""") |
1159 | - ) |
1160 | - return tmp_file_path |
1161 | - |
1162 | - |
1163 | def _get_environment_patch(pythonpath): |
1164 | environment_patch = dict(DISPLAY=':0') |
1165 | |
1166 | |
1167 | === added file 'autopilot/tests/functional/fixtures.py' |
1168 | --- autopilot/tests/functional/fixtures.py 1970-01-01 00:00:00 +0000 |
1169 | +++ autopilot/tests/functional/fixtures.py 2014-03-20 12:37:25 +0000 |
1170 | @@ -0,0 +1,170 @@ |
1171 | +# -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*- |
1172 | +# |
1173 | +# Autopilot Functional Test Tool |
1174 | +# Copyright (C) 2014 Canonical |
1175 | +# |
1176 | +# This program is free software: you can redistribute it and/or modify |
1177 | +# it under the terms of the GNU General Public License as published by |
1178 | +# the Free Software Foundation, either version 3 of the License, or |
1179 | +# (at your option) any later version. |
1180 | +# |
1181 | +# This program is distributed in the hope that it will be useful, |
1182 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
1183 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
1184 | +# GNU General Public License for more details. |
1185 | +# |
1186 | +# You should have received a copy of the GNU General Public License |
1187 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
1188 | +# |
1189 | + |
1190 | +"""Fixtures for the autopilot functional test suite.""" |
1191 | + |
1192 | +from __future__ import absolute_import |
1193 | + |
1194 | +import logging |
1195 | +import os |
1196 | +import stat |
1197 | +from shutil import rmtree |
1198 | +import tempfile |
1199 | +from textwrap import dedent |
1200 | + |
1201 | +from fixtures import Fixture |
1202 | + |
1203 | + |
1204 | +logger = logging.getLogger(__name__) |
1205 | + |
1206 | + |
1207 | +class ExecutableScript(Fixture): |
1208 | + """Write some text to a file on disk and make it executable.""" |
1209 | + |
1210 | + def __init__(self, script, extension=".py"): |
1211 | + """Initialise the fixture. |
1212 | + |
1213 | + :param script: The contents of the script file. |
1214 | + :param extension: The desired extension on the script file. |
1215 | + |
1216 | + """ |
1217 | + super(ExecutableScript, self).__init__() |
1218 | + self._script = script |
1219 | + self._extension = extension |
1220 | + |
1221 | + def setUp(self): |
1222 | + super(ExecutableScript, self).setUp() |
1223 | + with tempfile.NamedTemporaryFile( |
1224 | + suffix=self._extension, |
1225 | + mode='w', |
1226 | + delete=False |
1227 | + ) as f: |
1228 | + f.write(self._script) |
1229 | + self.path = f.name |
1230 | + self.addCleanup(os.unlink, self.path) |
1231 | + |
1232 | + os.chmod(self.path, os.stat(self.path).st_mode | stat.S_IXUSR) |
1233 | + |
1234 | + |
1235 | +class TempDesktopFile(Fixture): |
1236 | + |
1237 | + def __init__(self, type=None, exec_=None, name=None, icon=None): |
1238 | + """Create a TempDesktopFile instance. |
1239 | + |
1240 | + Parameters control the contents of the created desktop file. Default |
1241 | + values will create a desktop file with bogus contents. |
1242 | + |
1243 | + :param type: The type field in the created file. Defaults to |
1244 | + 'Application'. |
1245 | + :param exec_: The path to the file to execute. |
1246 | + :param name: The name of the application being launched. Defaults to |
1247 | + "Test App". |
1248 | + """ |
1249 | + super(TempDesktopFile, self).__init__() |
1250 | + type_line = type if type is not None else "Application" |
1251 | + exec_line = exec_ if exec_ is not None else "Not Important" |
1252 | + name_line = name if name is not None else "Test App" |
1253 | + icon_line = icon if icon is not None else "Not Important" |
1254 | + self._file_contents = dedent( |
1255 | + """\ |
1256 | + [Desktop Entry] |
1257 | + Type={} |
1258 | + Exec={} |
1259 | + Name={} |
1260 | + Icon={} |
1261 | + """.format(type_line, exec_line, name_line, icon_line) |
1262 | + ) |
1263 | + |
1264 | + def setUp(self): |
1265 | + super(TempDesktopFile, self).setUp() |
1266 | + path_created = TempDesktopFile._ensure_desktop_dir_exists() |
1267 | + self._desktop_file_path = self._create_desktop_file( |
1268 | + self._file_contents, |
1269 | + ) |
1270 | + |
1271 | + self.addCleanup( |
1272 | + TempDesktopFile._remove_desktop_file_components, |
1273 | + path_created, |
1274 | + self._desktop_file_path, |
1275 | + ) |
1276 | + |
1277 | + def get_desktop_file_path(self): |
1278 | + return self._desktop_file_path |
1279 | + |
1280 | + def get_desktop_file_id(self): |
1281 | + return os.path.splitext(os.path.basename(self._desktop_file_path))[0] |
1282 | + |
1283 | + @staticmethod |
1284 | + def _ensure_desktop_dir_exists(): |
1285 | + desktop_file_dir = TempDesktopFile._desktop_file_dir() |
1286 | + if not os.path.exists(desktop_file_dir): |
1287 | + return TempDesktopFile._create_desktop_file_dir(desktop_file_dir) |
1288 | + return '' |
1289 | + |
1290 | + @staticmethod |
1291 | + def _desktop_file_dir(): |
1292 | + return os.path.join( |
1293 | + os.getenv('HOME'), |
1294 | + '.local', |
1295 | + 'share', |
1296 | + 'applications' |
1297 | + ) |
1298 | + |
1299 | + @staticmethod |
1300 | + def _create_desktop_file_dir(desktop_file_dir): |
1301 | + """Create the directory specified. |
1302 | + |
1303 | + Returns the component of the path that did not exist, or the empty |
1304 | + string if the entire path already existed. |
1305 | + |
1306 | + """ |
1307 | + # We might be creating more than just the leaf directory, so we need to |
1308 | + # keep track of what doesn't already exist and remove it when we're |
1309 | + # done. Defaults to removing the full path |
1310 | + path_to_delete = "" |
1311 | + if not os.path.exists(desktop_file_dir): |
1312 | + path_to_delete = desktop_file_dir |
1313 | + full_path, leaf = os.path.split(desktop_file_dir) |
1314 | + while leaf != "": |
1315 | + if not os.path.exists(full_path): |
1316 | + path_to_delete = full_path |
1317 | + full_path, leaf = os.path.split(full_path) |
1318 | + |
1319 | + try: |
1320 | + os.makedirs(desktop_file_dir) |
1321 | + except OSError: |
1322 | + logger.warning("Directory already exists: %s" % desktop_file_dir) |
1323 | + return path_to_delete |
1324 | + |
1325 | + @staticmethod |
1326 | + def _remove_desktop_file_components(created_path, created_file): |
1327 | + if created_path != "": |
1328 | + rmtree(created_path) |
1329 | + else: |
1330 | + os.remove(created_file) |
1331 | + |
1332 | + @staticmethod |
1333 | + def _create_desktop_file(file_contents): |
1334 | + _, tmp_file_path = tempfile.mkstemp( |
1335 | + suffix='.desktop', |
1336 | + dir=TempDesktopFile._desktop_file_dir() |
1337 | + ) |
1338 | + with open(tmp_file_path, 'w') as desktop_file: |
1339 | + desktop_file.write(file_contents) |
1340 | + return tmp_file_path |
1341 | |
1342 | === modified file 'autopilot/tests/functional/test_ap_apps.py' |
1343 | --- autopilot/tests/functional/test_ap_apps.py 2014-03-10 01:54:54 +0000 |
1344 | +++ autopilot/tests/functional/test_ap_apps.py 2014-03-20 12:37:25 +0000 |
1345 | @@ -20,13 +20,11 @@ |
1346 | |
1347 | import datetime |
1348 | import os |
1349 | -import stat |
1350 | import subprocess |
1351 | import logging |
1352 | import sys |
1353 | import six |
1354 | from mock import patch |
1355 | -from tempfile import mktemp |
1356 | from testtools import skipIf |
1357 | from testtools.matchers import ( |
1358 | Equals, |
1359 | @@ -40,7 +38,10 @@ |
1360 | from autopilot.process import ProcessManager |
1361 | from autopilot.platform import model |
1362 | from autopilot.testcase import AutopilotTestCase |
1363 | -from autopilot.tests.functional import TempDesktopFile |
1364 | +from autopilot.tests.functional.fixtures import ( |
1365 | + ExecutableScript, |
1366 | + TempDesktopFile, |
1367 | +) |
1368 | from autopilot.introspection import ( |
1369 | get_proxy_object_for_existing_process, |
1370 | ProcessSearchError, |
1371 | @@ -86,15 +87,7 @@ |
1372 | and return the path to the script file. |
1373 | |
1374 | """ |
1375 | - path = mktemp(extension) |
1376 | - if six.PY3: |
1377 | - open(path, 'w', encoding='utf-8').write(content) |
1378 | - else: |
1379 | - open(path, 'w').write(content) |
1380 | - self.addCleanup(os.unlink, path) |
1381 | - |
1382 | - os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR) |
1383 | - return path |
1384 | + return self.useFixture(ExecutableScript(content, extension)).path |
1385 | |
1386 | |
1387 | class ApplicationLaunchTests(ApplicationTests): |
1388 | @@ -238,7 +231,39 @@ |
1389 | ) |
1390 | |
1391 | |
1392 | -class QtTests(ApplicationTests): |
1393 | +class QmlTestMixin(object): |
1394 | + |
1395 | + def get_qml_viewer_app_path(self): |
1396 | + try: |
1397 | + qtversions = subprocess.check_output( |
1398 | + ['qtchooser', '-list-versions'], |
1399 | + universal_newlines=True |
1400 | + ).split('\n') |
1401 | + check_func = self._find_qt_binary_chooser |
1402 | + except OSError: |
1403 | + # This means no qtchooser is installed, so let's check for |
1404 | + # qmlviewer and qmlscene manually, the old way |
1405 | + qtversions = ['qt4', 'qt5'] |
1406 | + check_func = self._find_qt_binary_old |
1407 | + |
1408 | + not_found = True |
1409 | + if 'qt4' in qtversions: |
1410 | + path = check_func('qt4', 'qmlviewer') |
1411 | + if path: |
1412 | + not_found = False |
1413 | + self.qml_viewer_app_path = path |
1414 | + self.patch_environment("QT_SELECT", "qt4") |
1415 | + |
1416 | + if 'qt5' in qtversions: |
1417 | + path = check_func('qt5', 'qmlscene') |
1418 | + if path: |
1419 | + not_found = False |
1420 | + self.qml_viewer_app_path = path |
1421 | + self.patch_environment("QT_SELECT", "qt5") |
1422 | + |
1423 | + if not_found: |
1424 | + self.skip("Neither qmlviewer nor qmlscene is installed") |
1425 | + return self.qml_viewer_app_path |
1426 | |
1427 | def _find_qt_binary_chooser(self, version, name): |
1428 | # Check for existence of the binary when qtchooser is installed |
1429 | @@ -264,59 +289,21 @@ |
1430 | path = None |
1431 | return path |
1432 | |
1433 | - def setUp(self): |
1434 | - super(QtTests, self).setUp() |
1435 | - |
1436 | - try: |
1437 | - qtversions = subprocess.check_output( |
1438 | - ['qtchooser', '-list-versions'], |
1439 | - universal_newlines=True |
1440 | - ).split('\n') |
1441 | - check_func = self._find_qt_binary_chooser |
1442 | - except OSError: |
1443 | - # This means no qtchooser is installed, so let's check for |
1444 | - # qmlviewer and qmlscene manually, the old way |
1445 | - qtversions = ['qt4', 'qt5'] |
1446 | - check_func = self._find_qt_binary_old |
1447 | - |
1448 | - not_found = True |
1449 | - if 'qt4' in qtversions: |
1450 | - path = check_func('qt4', 'qmlviewer') |
1451 | - if path: |
1452 | - not_found = False |
1453 | - self.app_path = path |
1454 | - self.patch_environment("QT_SELECT", "qt4") |
1455 | - |
1456 | - if 'qt5' in qtversions: |
1457 | - path = check_func('qt5', 'qmlscene') |
1458 | - if path: |
1459 | - not_found = False |
1460 | - self.app_path = path |
1461 | - self.patch_environment("QT_SELECT", "qt5") |
1462 | - |
1463 | - if not_found: |
1464 | - self.skip("Neither qmlviewer nor qmlscene is installed") |
1465 | - |
1466 | - def test_can_launch_qt_app(self): |
1467 | - extra_args = '' |
1468 | - if model() != "Desktop": |
1469 | - # We need to add the desktop-file-hint |
1470 | - desktop_file = self.useFixture( |
1471 | - TempDesktopFile() |
1472 | - ).get_desktop_file_path() |
1473 | - extra_args = '--desktop_file_hint={hint_file}'.format( |
1474 | - hint_file=desktop_file |
1475 | - ) |
1476 | - |
1477 | - app_proxy = self.launch_test_application( |
1478 | - self.app_path, |
1479 | - extra_args, |
1480 | - app_type='qt' |
1481 | - ) |
1482 | + |
1483 | +class QtTests(ApplicationTests, QmlTestMixin): |
1484 | + |
1485 | + def test_can_launch_normal_app(self): |
1486 | + path = self.get_qml_viewer_app_path() |
1487 | + app_proxy = self.launch_test_application(path, app_type='qt') |
1488 | self.assertTrue(app_proxy is not None) |
1489 | |
1490 | + def test_can_launch_upstart_app(self): |
1491 | + path = self.get_qml_viewer_app_path() |
1492 | + fixture = self.useFixture(TempDesktopFile(exec_=path,)) |
1493 | + self.launch_upstart_application(fixture.get_desktop_file_id()) |
1494 | + |
1495 | @skipIf(model() != "Desktop", "Only suitable on Desktop (Qt4)") |
1496 | - def test_can_launch_qt_script(self): |
1497 | + def test_can_launch_normal_qt_script(self): |
1498 | path = self.write_script(dedent("""\ |
1499 | #!%s |
1500 | from PyQt4.QtGui import QMainWindow, QApplication |
1501 | @@ -330,6 +317,7 @@ |
1502 | app_proxy = self.launch_test_application(path, app_type='qt') |
1503 | self.assertTrue(app_proxy is not None) |
1504 | |
1505 | + # TODO: move this into a test module that tests bamf. |
1506 | @skipIf(model() != 'Desktop', "Bamf only available on desktop (Qt4)") |
1507 | def test_bamf_geometry_gives_reliable_results(self): |
1508 | path = self.write_script(dedent("""\ |
1509 | @@ -413,17 +401,18 @@ |
1510 | |
1511 | class GtkTests(ApplicationTests): |
1512 | |
1513 | - def setUp(self): |
1514 | - super(GtkTests, self).setUp() |
1515 | - |
1516 | + def _get_mahjongg_path(self): |
1517 | try: |
1518 | - self.app_path = subprocess.check_output( |
1519 | + return subprocess.check_output( |
1520 | ['which', 'gnome-mahjongg'], universal_newlines=True).strip() |
1521 | - except subprocess.CalledProcessError: |
1522 | + except: |
1523 | + return |
1524 | + |
1525 | + def test_can_launch_gtk_app(self): |
1526 | + mahjongg_path = self._get_mahjongg_path() |
1527 | + if not mahjongg_path: |
1528 | self.skip("gnome-mahjongg not found.") |
1529 | - |
1530 | - def test_can_launch_gtk_app(self): |
1531 | - app_proxy = self.launch_test_application(self.app_path) |
1532 | + app_proxy = self.launch_test_application(mahjongg_path) |
1533 | self.assertTrue(app_proxy is not None) |
1534 | |
1535 | def test_can_launch_gtk_script(self): |
1536 | |
1537 | === modified file 'autopilot/tests/functional/test_autopilot_functional.py' |
1538 | --- autopilot/tests/functional/test_autopilot_functional.py 2014-03-10 04:06:08 +0000 |
1539 | +++ autopilot/tests/functional/test_autopilot_functional.py 2014-03-20 12:37:25 +0000 |
1540 | @@ -55,6 +55,9 @@ |
1541 | This assertion is intelligent enough to know that tests are not always |
1542 | printed in alphabetical order. |
1543 | |
1544 | + 'tests' can either be a list of test ids, or a list of tuples |
1545 | + containing (scenario_count, test_id), in the case of scenarios. |
1546 | + |
1547 | """ |
1548 | |
1549 | if type(tests) is not list: |
1550 | @@ -63,8 +66,16 @@ |
1551 | raise TypeError("output must be a string, not %r" % type(output)) |
1552 | |
1553 | expected_heading = 'Loading tests from: %s\n\n' % self.base_path |
1554 | - expected_tests = [' %s' % t for t in tests] |
1555 | - expected_footer = ' %d total %s.' % (len(tests), total_title) |
1556 | + expected_tests = [] |
1557 | + expected_total = 0 |
1558 | + for test in tests: |
1559 | + if type(test) == tuple: |
1560 | + expected_tests.append(' *%d %s' % test) |
1561 | + expected_total += test[0] |
1562 | + else: |
1563 | + expected_tests.append(' %s' % test) |
1564 | + expected_total += 1 |
1565 | + expected_footer = ' %d total %s.' % (expected_total, total_title) |
1566 | |
1567 | parts = output.split('\n') |
1568 | observed_heading = '\n'.join(parts[:2]) + '\n' |
1569 | @@ -232,20 +243,16 @@ |
1570 | """) |
1571 | ) |
1572 | |
1573 | - expected_output = '''\ |
1574 | -Loading tests from: %s |
1575 | - |
1576 | - *2 tests.test_simple.SimpleTest.test_simple |
1577 | - *2 tests.test_simple.SimpleTest.test_simple_two |
1578 | - |
1579 | - |
1580 | - 4 total tests. |
1581 | -''' % self.base_path |
1582 | - |
1583 | code, output, error = self.run_autopilot_list() |
1584 | self.assertThat(code, Equals(0)) |
1585 | self.assertThat(error, Equals('')) |
1586 | - self.assertThat(output, Equals(expected_output)) |
1587 | + self.assertTestsInOutput( |
1588 | + [ |
1589 | + (2, 'tests.test_simple.SimpleTest.test_simple'), |
1590 | + (2, 'tests.test_simple.SimpleTest.test_simple_two'), |
1591 | + ], |
1592 | + output |
1593 | + ) |
1594 | |
1595 | def test_can_list_invalid_scenarios(self): |
1596 | """Autopilot must ignore scenarios that are not lists.""" |
1597 | |
1598 | === modified file 'autopilot/tests/functional/test_input_stack.py' |
1599 | --- autopilot/tests/functional/test_input_stack.py 2014-02-26 01:08:27 +0000 |
1600 | +++ autopilot/tests/functional/test_input_stack.py 2014-03-20 12:37:25 +0000 |
1601 | @@ -34,7 +34,7 @@ |
1602 | from autopilot.input._common import get_center_point |
1603 | from autopilot.matchers import Eventually |
1604 | from autopilot.testcase import AutopilotTestCase, multiply_scenarios |
1605 | -from autopilot.tests.functional import TempDesktopFile |
1606 | +from autopilot.tests.functional.fixtures import TempDesktopFile |
1607 | from autopilot.utilities import on_test_started |
1608 | |
1609 | |
1610 | |
1611 | === modified file 'autopilot/tests/functional/test_introspection_features.py' |
1612 | --- autopilot/tests/functional/test_introspection_features.py 2014-02-26 01:08:27 +0000 |
1613 | +++ autopilot/tests/functional/test_introspection_features.py 2014-03-20 12:37:25 +0000 |
1614 | @@ -43,7 +43,7 @@ |
1615 | from autopilot import platform |
1616 | from autopilot.matchers import Eventually |
1617 | from autopilot.testcase import AutopilotTestCase |
1618 | -from autopilot.tests.functional import TempDesktopFile |
1619 | +from autopilot.tests.functional.fixtures import TempDesktopFile |
1620 | from autopilot.introspection.dbus import CustomEmulatorBase |
1621 | from autopilot.introspection import _connection_matches_pid |
1622 | from autopilot.display import Display |
1623 | |
1624 | === modified file 'autopilot/tests/functional/test_process_emulator.py' |
1625 | --- autopilot/tests/functional/test_process_emulator.py 2014-02-03 04:32:54 +0000 |
1626 | +++ autopilot/tests/functional/test_process_emulator.py 2014-03-20 12:37:25 +0000 |
1627 | @@ -16,20 +16,23 @@ |
1628 | # You should have received a copy of the GNU General Public License |
1629 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
1630 | # |
1631 | - |
1632 | +import os |
1633 | from subprocess import Popen, call |
1634 | +import sys |
1635 | from testtools import skipIf |
1636 | from testtools.matchers import Equals, NotEquals, LessThan |
1637 | +from textwrap import dedent |
1638 | from threading import Thread |
1639 | from time import sleep, time |
1640 | |
1641 | -from autopilot import platform |
1642 | +from autopilot.platform import model |
1643 | from autopilot.exceptions import BackendException |
1644 | from autopilot.testcase import AutopilotTestCase |
1645 | from autopilot.process import ProcessManager |
1646 | - |
1647 | - |
1648 | -@skipIf(platform.model() != "Desktop", "Not suitable for device (ProcManager)") |
1649 | +from autopilot.tests.functional.fixtures import ExecutableScript |
1650 | + |
1651 | + |
1652 | +@skipIf(model() != "Desktop", "Not suitable for device (ProcManager)") |
1653 | class ProcessEmulatorTests(AutopilotTestCase): |
1654 | |
1655 | def ensure_gedit_not_running(self): |
1656 | @@ -89,6 +92,28 @@ |
1657 | self.assertThat(app, NotEquals(None)) |
1658 | self.assertThat(app.name, Equals('Calculator')) |
1659 | |
1660 | + @skipIf(model() != 'Desktop', "Bamf only available on desktop (Qt4)") |
1661 | + def test_bamf_geometry_gives_reliable_results(self): |
1662 | + script = dedent("""\ |
1663 | + #!%s |
1664 | + from PyQt4.QtGui import QMainWindow, QApplication |
1665 | + from sys import argv |
1666 | + |
1667 | + app = QApplication(argv) |
1668 | + win = QMainWindow() |
1669 | + win.show() |
1670 | + app.exec_() |
1671 | + """ % sys.executable) |
1672 | + path = self.useFixture(ExecutableScript(script)).path |
1673 | + app_proxy = self.launch_test_application(path, app_type='qt') |
1674 | + proxy_window = app_proxy.select_single('QMainWindow') |
1675 | + pm = ProcessManager.create() |
1676 | + window = [ |
1677 | + w for w in pm.get_open_windows() |
1678 | + if w.name == os.path.basename(path) |
1679 | + ][0] |
1680 | + self.assertThat(list(window.geometry), Equals(proxy_window.geometry)) |
1681 | + |
1682 | |
1683 | class ProcessManagerApplicationNoCleanupTests(AutopilotTestCase): |
1684 | """Testing the process manager without the automated cleanup that running |
1685 | |
1686 | === modified file 'autopilot/tests/unit/test_application_environment.py' |
1687 | --- autopilot/tests/unit/test_application_environment.py 2014-01-09 03:12:32 +0000 |
1688 | +++ autopilot/tests/unit/test_application_environment.py 2014-03-20 12:37:25 +0000 |
1689 | @@ -19,14 +19,12 @@ |
1690 | |
1691 | from mock import patch |
1692 | from testtools import TestCase |
1693 | -from testtools.matchers import raises, Equals |
1694 | +from testtools.matchers import raises |
1695 | |
1696 | from autopilot.application._environment import ( |
1697 | - _call_upstart_with_args, |
1698 | ApplicationEnvironment, |
1699 | GtkApplicationEnvironment, |
1700 | QtApplicationEnvironment, |
1701 | - UpstartApplicationEnvironment, |
1702 | ) |
1703 | |
1704 | |
1705 | @@ -40,11 +38,6 @@ |
1706 | ) |
1707 | ) |
1708 | |
1709 | - @patch('autopilot.application._environment.subprocess') |
1710 | - def test_call_upstart_with_args_returns_output(self, patched_subprocess): |
1711 | - patched_subprocess.check_output.return_value = "Returned Value" |
1712 | - self.assertThat(_call_upstart_with_args(), Equals("Returned Value")) |
1713 | - |
1714 | |
1715 | class GtkApplicationEnvironmentTests(TestCase): |
1716 | |
1717 | @@ -105,52 +98,3 @@ |
1718 | 'app', ['-testability'] |
1719 | ) |
1720 | self.assertEqual(['-testability'], args) |
1721 | - |
1722 | - |
1723 | -class UpstartApplicationEnvironmentTests(TestCase): |
1724 | - |
1725 | - # These tests patch _unset_upstart_env as cleanUps() calls the unpatched |
1726 | - # _unset_upstart_env which in turn calls the real _call_upstart_with_args |
1727 | - # (making a system call) |
1728 | - @patch('autopilot.application._environment._call_upstart_with_args') |
1729 | - @patch('autopilot.application._environment._unset_upstart_env') |
1730 | - def test_does_not_alter_app(self, patched_unset, patched_call_upstart): |
1731 | - app_environment = self.useFixture(UpstartApplicationEnvironment()) |
1732 | - fake_app = self.getUniqueString() |
1733 | - app, args = app_environment.prepare_environment(fake_app, []) |
1734 | - |
1735 | - self.assertEqual(fake_app, app) |
1736 | - |
1737 | - @patch('autopilot.application._environment._call_upstart_with_args') |
1738 | - @patch('autopilot.application._environment._unset_upstart_env') |
1739 | - def test_does_not_alter_args(self, patched_unset, patched_call_upstart): |
1740 | - app_environment = self.useFixture(UpstartApplicationEnvironment()) |
1741 | - fake_app = self.getUniqueString() |
1742 | - app, args = app_environment.prepare_environment(fake_app, []) |
1743 | - |
1744 | - self.assertEqual([], args) |
1745 | - |
1746 | - @patch('autopilot.application._environment._call_upstart_with_args') |
1747 | - @patch('autopilot.application._environment._unset_upstart_env') |
1748 | - def test_patches_env(self, patched_unset, patched_call_upstart): |
1749 | - app_environment = self.useFixture(UpstartApplicationEnvironment()) |
1750 | - fake_app = self.getUniqueString() |
1751 | - app, args = app_environment.prepare_environment(fake_app, []) |
1752 | - |
1753 | - patched_call_upstart.called_with_args( |
1754 | - 'set-env', |
1755 | - 'QT_LOAD_TESTABILITY=1' |
1756 | - ) |
1757 | - |
1758 | - @patch('autopilot.application._environment._call_upstart_with_args') |
1759 | - def test_unpatches_env(self, patched_call_upstart): |
1760 | - app_environment = self.useFixture(UpstartApplicationEnvironment()) |
1761 | - fake_app = self.getUniqueString() |
1762 | - app, args = app_environment.prepare_environment(fake_app, []) |
1763 | - |
1764 | - app_environment.cleanUp() |
1765 | - |
1766 | - patched_call_upstart.called_with_args( |
1767 | - 'unset-env', |
1768 | - 'QT_LOAD_TESTABILITY' |
1769 | - ) |
1770 | |
1771 | === modified file 'autopilot/tests/unit/test_application_launcher.py' |
1772 | --- autopilot/tests/unit/test_application_launcher.py 2014-03-09 20:10:51 +0000 |
1773 | +++ autopilot/tests/unit/test_application_launcher.py 2014-03-20 12:37:25 +0000 |
1774 | @@ -17,7 +17,7 @@ |
1775 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
1776 | # |
1777 | |
1778 | -import os |
1779 | +from gi.repository import GLib |
1780 | import signal |
1781 | import subprocess |
1782 | from testtools import TestCase |
1783 | @@ -25,6 +25,7 @@ |
1784 | Contains, |
1785 | Equals, |
1786 | GreaterThan, |
1787 | + HasLength, |
1788 | IsInstance, |
1789 | MatchesListwise, |
1790 | Not, |
1791 | @@ -32,16 +33,17 @@ |
1792 | raises, |
1793 | ) |
1794 | from testtools.content import text_content |
1795 | +import tempfile |
1796 | from mock import Mock, patch |
1797 | |
1798 | from autopilot.application import ( |
1799 | ClickApplicationLauncher, |
1800 | NormalApplicationLauncher, |
1801 | + UpstartApplicationLauncher, |
1802 | ) |
1803 | from autopilot.application._environment import ( |
1804 | GtkApplicationEnvironment, |
1805 | QtApplicationEnvironment, |
1806 | - UpstartApplicationEnvironment, |
1807 | ) |
1808 | import autopilot.application._launcher as _l |
1809 | from autopilot.application._launcher import ( |
1810 | @@ -53,15 +55,9 @@ |
1811 | _get_application_environment, |
1812 | _get_application_path, |
1813 | _get_click_app_id, |
1814 | - _get_click_app_pid, |
1815 | - _get_click_app_status, |
1816 | - _get_click_application_log_content_object, |
1817 | - _get_click_application_log_path, |
1818 | _get_click_manifest, |
1819 | _is_process_running, |
1820 | - _kill_pid, |
1821 | _kill_process, |
1822 | - _launch_click_app, |
1823 | ) |
1824 | from autopilot.utilities import sleep |
1825 | |
1826 | @@ -189,15 +185,38 @@ |
1827 | launcher.dbus_application_name, Equals(app_name) |
1828 | ) |
1829 | |
1830 | - @patch.object(UpstartApplicationEnvironment, 'prepare_environment') |
1831 | - def test_prepare_environment_called(self, prep_env): |
1832 | - with patch.object(_l, '_get_click_app_id', return_value="app_id"): |
1833 | + def test_click_launch_calls_upstart_launch(self): |
1834 | + launcher = ClickApplicationLauncher(self.addDetail) |
1835 | + token = self.getUniqueString() |
1836 | + with patch.object(launcher, '_do_upstart_launch') as p_dul: |
1837 | + with patch.object(_l, '_get_click_app_id') as p_gcai: |
1838 | + p_gcai.return_value = token |
1839 | + launcher.launch('some_app_id', 'some_app_name', []) |
1840 | + p_dul.assert_called_once_with(token, []) |
1841 | + |
1842 | + def test_upcalls_to_upstart(self): |
1843 | + class FakeUpstartBase(_l.ApplicationLauncher): |
1844 | + launch_call_args = [] |
1845 | + |
1846 | + def launch(self, *args): |
1847 | + FakeUpstartBase.launch_call_args = list(args) |
1848 | + |
1849 | + patcher = patch.object( |
1850 | + _l.ClickApplicationLauncher, |
1851 | + '__bases__', |
1852 | + (FakeUpstartBase,) |
1853 | + ) |
1854 | + with patcher: |
1855 | + # Prevent mock from trying to delete __bases__ |
1856 | + patcher.is_local = True |
1857 | launcher = ClickApplicationLauncher(self.addDetail) |
1858 | - launcher.setUp() |
1859 | - launcher._launch_click_app = Mock() |
1860 | - |
1861 | - launcher.launch("package_id", "app_name", "") |
1862 | - prep_env.assert_called_with("app_id", "app_name") |
1863 | + launcher._do_upstart_launch('app_id', []) |
1864 | + self.assertEqual( |
1865 | + FakeUpstartBase.launch_call_args, |
1866 | + ['app_id', []]) |
1867 | + |
1868 | + |
1869 | +class ClickFunctionTests(TestCase): |
1870 | |
1871 | def test_get_click_app_id_raises_runtimeerror_on_empty_manifest(self): |
1872 | """_get_click_app_id must raise a RuntimeError if the requested |
1873 | @@ -280,172 +299,285 @@ |
1874 | Equals("com.autopilot.testing_bar_1.0") |
1875 | ) |
1876 | |
1877 | - def test_get_click_application_log_path_formats_correct_path(self): |
1878 | - path_token = self.getUniqueString() |
1879 | - expected = os.path.join(path_token, "application-click-foo.log") |
1880 | - |
1881 | - with patch.object(_l.os.path, 'expanduser', return_value=path_token): |
1882 | - self.assertThat( |
1883 | - _get_click_application_log_path("foo"), |
1884 | - Equals(expected) |
1885 | - ) |
1886 | - |
1887 | - def test_get_click_application_log_content_object_returns_content_object(self): # NOQA |
1888 | - with patch.object(_l, 'content_from_file') as from_file: |
1889 | - self.assertThat( |
1890 | - _get_click_application_log_content_object("foo"), |
1891 | - Equals(from_file()) |
1892 | - ) |
1893 | - |
1894 | - def test_get_click_app_log_works_when_no_log_file_exists(self): |
1895 | - token = self.getUniqueString() |
1896 | - with patch.object( |
1897 | - _l, |
1898 | - '_get_click_application_log_path', |
1899 | - return_value=token |
1900 | - ): |
1901 | - |
1902 | - self.assertThat( |
1903 | - lambda: _l._get_click_application_log_content_object("foo"), |
1904 | - Not(raises(IOError)) |
1905 | - ) |
1906 | - |
1907 | - @patch.object(_l, '_launch_click_app', return_value=123) |
1908 | - def test_launch_click_app_returns_pid(self, patched_launch_click_app): |
1909 | - launcher = ClickApplicationLauncher(self.addDetail) |
1910 | - launcher._add_click_launch_cleanup = Mock() |
1911 | - |
1912 | - with patch.object(_l, 'logger'): |
1913 | - self.assertThat( |
1914 | - launcher._launch_click_app("appid", ""), |
1915 | - Equals(123) |
1916 | - ) |
1917 | - |
1918 | - def test_add_click_launch_cleanup_queues_correct_cleanup_steps(self): |
1919 | - test_app_name = self.getUniqueString() |
1920 | - test_app_pid = self.getUniqueInteger() |
1921 | - launcher = ClickApplicationLauncher(self.addDetail) |
1922 | - launcher.setUp() |
1923 | - launcher._add_click_launch_cleanup(test_app_name, test_app_pid) |
1924 | - |
1925 | - self.assertThat( |
1926 | - launcher._cleanups._cleanups, |
1927 | - MatchesListwise([ |
1928 | - Equals((_kill_pid, (test_app_pid,), {})), |
1929 | - Equals((launcher._add_log_cleanup, (test_app_name,), {})), |
1930 | - ]) |
1931 | - ) |
1932 | - |
1933 | - def test_add_click_launch_cleanup_provides_correct_details(self): |
1934 | - launcher = ClickApplicationLauncher(self.addDetail) |
1935 | - launcher.addCleanup = Mock() |
1936 | - test_app_id = self.getUniqueString() |
1937 | - test_app_pid = self.getUniqueInteger() |
1938 | - |
1939 | - launcher._add_click_launch_cleanup(test_app_id, test_app_pid) |
1940 | - launcher.addCleanup.assert_any_call(_kill_pid, test_app_pid) |
1941 | - launcher.addCleanup.assert_any_call( |
1942 | - launcher._add_log_cleanup, |
1943 | - test_app_id |
1944 | - ) |
1945 | - |
1946 | - def test_add_log_cleanup_adds_details(self): |
1947 | - mock_addDetail = Mock() |
1948 | - launcher = ClickApplicationLauncher(mock_addDetail) |
1949 | - with patch.object( |
1950 | - _l, '_get_click_application_log_content_object' |
1951 | - ) as log_content: |
1952 | - launcher._add_log_cleanup("appid") |
1953 | - |
1954 | - mock_addDetail.assert_called_with("Application Log", log_content()) |
1955 | + |
1956 | +class UpstartApplicationLauncherTests(TestCase): |
1957 | + |
1958 | + def test_can_construct_UpstartApplicationLauncher(self): |
1959 | + UpstartApplicationLauncher(self.addDetail) |
1960 | + |
1961 | + def test_default_values_are_set(self): |
1962 | + launcher = UpstartApplicationLauncher(self.addDetail) |
1963 | + self.assertThat(launcher.emulator_base, Equals(None)) |
1964 | + self.assertThat(launcher.dbus_bus, Equals('session')) |
1965 | + |
1966 | + def test_can_set_emulator_base(self): |
1967 | + mock_emulator_base = Mock() |
1968 | + launcher = UpstartApplicationLauncher( |
1969 | + self.addDetail, |
1970 | + emulator_base=mock_emulator_base |
1971 | + ) |
1972 | + |
1973 | + self.assertThat(launcher.emulator_base, Equals(mock_emulator_base)) |
1974 | + |
1975 | + def test_can_set_dbus_bus(self): |
1976 | + launcher = UpstartApplicationLauncher( |
1977 | + self.addDetail, |
1978 | + dbus_bus='system' |
1979 | + ) |
1980 | + |
1981 | + self.assertThat(launcher.dbus_bus, Equals('system')) |
1982 | + |
1983 | + def test_raises_exception_on_unknown_kwargs(self): |
1984 | + self.assertThat( |
1985 | + lambda: UpstartApplicationLauncher(self.addDetail, unknown=True), |
1986 | + raises(ValueError("Unknown keyword arguments: 'unknown'.")) |
1987 | + ) |
1988 | + |
1989 | + def test_on_failed_only_sets_status_on_correct_app_id(self): |
1990 | + state = { |
1991 | + 'expected_app_id': 'gedit', |
1992 | + } |
1993 | + |
1994 | + UpstartApplicationLauncher._on_failed('some_game', None, state) |
1995 | + |
1996 | + self.assertThat(state, Not(Contains('status'))) |
1997 | + |
1998 | + def assertFunctionSetsCorrectStateAndQuits(self, observer, expected_state): |
1999 | + """Assert that the observer observer sets the correct state id. |
2000 | + |
2001 | + :param observer: The observer callable you want to test. |
2002 | + :param expected_state: The state id the observer callable must set. |
2003 | + |
2004 | + """ |
2005 | + expected_app_id = self.getUniqueString() |
2006 | + state = { |
2007 | + 'expected_app_id': expected_app_id, |
2008 | + 'loop': Mock() |
2009 | + } |
2010 | + |
2011 | + if observer == UpstartApplicationLauncher._on_failed: |
2012 | + observer(expected_app_id, None, state) |
2013 | + elif observer == UpstartApplicationLauncher._on_started: |
2014 | + observer(expected_app_id, state) |
2015 | + else: |
2016 | + observer(state) |
2017 | + |
2018 | + self.assertThat( |
2019 | + state['status'], |
2020 | + Equals(expected_state) |
2021 | + ) |
2022 | + state['loop'].quit.assert_called_once_with() |
2023 | + |
2024 | + def test_on_failed_sets_status_with_correct_app_id(self): |
2025 | + self.assertFunctionSetsCorrectStateAndQuits( |
2026 | + UpstartApplicationLauncher._on_failed, |
2027 | + UpstartApplicationLauncher.Failed |
2028 | + ) |
2029 | + |
2030 | + def test_on_started_sets_status_with_correct_app_id(self): |
2031 | + self.assertFunctionSetsCorrectStateAndQuits( |
2032 | + UpstartApplicationLauncher._on_started, |
2033 | + UpstartApplicationLauncher.Started |
2034 | + ) |
2035 | + |
2036 | + def test_on_timeout_sets_status_and_exits_loop(self): |
2037 | + self.assertFunctionSetsCorrectStateAndQuits( |
2038 | + UpstartApplicationLauncher._on_timeout, |
2039 | + UpstartApplicationLauncher.Timeout |
2040 | + ) |
2041 | + |
2042 | + def test_on_started_only_sets_status_on_correct_app_id(self): |
2043 | + state = { |
2044 | + 'expected_app_id': 'gedit', |
2045 | + } |
2046 | + |
2047 | + UpstartApplicationLauncher._on_started('some_game', state) |
2048 | + |
2049 | + self.assertThat(state, Not(Contains('status'))) |
2050 | + |
2051 | + def test_get_pid_calls_upstart_module(self): |
2052 | + expected_return = self.getUniqueInteger() |
2053 | + with patch.object(_l, 'UpstartAppLaunch') as mock_ual: |
2054 | + mock_ual.get_primary_pid.return_value = expected_return |
2055 | + observed = UpstartApplicationLauncher._get_pid_for_launched_app( |
2056 | + 'gedit' |
2057 | + ) |
2058 | + |
2059 | + mock_ual.get_primary_pid.assert_called_once_with('gedit') |
2060 | + self.assertThat(expected_return, Equals(observed)) |
2061 | + |
2062 | + def test_launch_app_calls_upstart_module(self): |
2063 | + with patch.object(_l, 'UpstartAppLaunch') as mock_ual: |
2064 | + UpstartApplicationLauncher._launch_app( |
2065 | + 'gedit', |
2066 | + ['some', 'uris'] |
2067 | + ) |
2068 | + |
2069 | + mock_ual.start_application_test.assert_called_once_with( |
2070 | + 'gedit', |
2071 | + ['some', 'uris'] |
2072 | + ) |
2073 | + |
2074 | + def test_check_error_raises_RuntimeError_on_timeout(self): |
2075 | + fn = lambda: UpstartApplicationLauncher._check_status_error( |
2076 | + UpstartApplicationLauncher.Timeout |
2077 | + ) |
2078 | + self.assertThat( |
2079 | + fn, |
2080 | + raises( |
2081 | + RuntimeError( |
2082 | + "Timed out while waiting for application to launch" |
2083 | + ) |
2084 | + ) |
2085 | + ) |
2086 | + |
2087 | + def test_check_error_raises_RuntimeError_on_failure(self): |
2088 | + fn = lambda: UpstartApplicationLauncher._check_status_error( |
2089 | + UpstartApplicationLauncher.Failed |
2090 | + ) |
2091 | + self.assertThat( |
2092 | + fn, |
2093 | + raises( |
2094 | + RuntimeError( |
2095 | + "Application Launch Failed" |
2096 | + ) |
2097 | + ) |
2098 | + ) |
2099 | + |
2100 | + def test_check_error_raises_RuntimeError_with_extra_message(self): |
2101 | + fn = lambda: UpstartApplicationLauncher._check_status_error( |
2102 | + UpstartApplicationLauncher.Failed, |
2103 | + "extra message" |
2104 | + ) |
2105 | + self.assertThat( |
2106 | + fn, |
2107 | + raises( |
2108 | + RuntimeError( |
2109 | + "Application Launch Failed: extra message" |
2110 | + ) |
2111 | + ) |
2112 | + ) |
2113 | + |
2114 | + def test_check_error_does_nothing_on_None(self): |
2115 | + UpstartApplicationLauncher._check_status_error(None) |
2116 | + |
2117 | + def test_get_loop_returns_glib_mainloop_instance(self): |
2118 | + loop = UpstartApplicationLauncher._get_glib_loop() |
2119 | + self.assertThat(loop, IsInstance(GLib.MainLoop)) |
2120 | + |
2121 | + def test_launch(self): |
2122 | + launcher = UpstartApplicationLauncher(self.addDetail) |
2123 | + with patch.object(launcher, '_launch_app') as patched_launch: |
2124 | + with patch.object(launcher, '_get_glib_loop'): |
2125 | + launcher.launch('gedit') |
2126 | + |
2127 | + patched_launch.assert_called_once_with('gedit', []) |
2128 | + |
2129 | + def assertFailedObserverSetsExtraMessage(self, fail_type, expected_msg): |
2130 | + """Assert that the on_failed observer must set the expected message |
2131 | + for a particular failure_type.""" |
2132 | + expected_app_id = self.getUniqueString() |
2133 | + state = { |
2134 | + 'expected_app_id': expected_app_id, |
2135 | + 'loop': Mock() |
2136 | + } |
2137 | + UpstartApplicationLauncher._on_failed( |
2138 | + expected_app_id, |
2139 | + fail_type, |
2140 | + state |
2141 | + ) |
2142 | + self.assertEqual(expected_msg, state['message']) |
2143 | + |
2144 | + def test_on_failed_sets_message_for_app_crash(self): |
2145 | + self.assertFailedObserverSetsExtraMessage( |
2146 | + _l.UpstartAppLaunch.AppFailed.CRASH, |
2147 | + 'Application crashed.' |
2148 | + ) |
2149 | + |
2150 | + def test_on_failed_sets_message_for_app_start_failure(self): |
2151 | + self.assertFailedObserverSetsExtraMessage( |
2152 | + _l.UpstartAppLaunch.AppFailed.START_FAILURE, |
2153 | + 'Application failed to start.' |
2154 | + ) |
2155 | + |
2156 | + def test_add_application_cleanups_adds_both_cleanup_actions(self): |
2157 | + token = self.getUniqueString() |
2158 | + state = { |
2159 | + 'status': UpstartApplicationLauncher.Started, |
2160 | + 'expected_app_id': token, |
2161 | + } |
2162 | + launcher = UpstartApplicationLauncher(Mock()) |
2163 | + launcher.setUp() |
2164 | + launcher._maybe_add_application_cleanups(state) |
2165 | + self.assertThat( |
2166 | + launcher._cleanups._cleanups, |
2167 | + Contains( |
2168 | + (launcher._attach_application_log, (token,), {}) |
2169 | + ) |
2170 | + ) |
2171 | + self.assertThat( |
2172 | + launcher._cleanups._cleanups, |
2173 | + Contains( |
2174 | + (launcher._stop_application, (token,), {}) |
2175 | + ) |
2176 | + ) |
2177 | + |
2178 | + def test_add_application_cleanups_does_nothing_when_app_timedout(self): |
2179 | + state = { |
2180 | + 'status': UpstartApplicationLauncher.Timeout, |
2181 | + } |
2182 | + launcher = UpstartApplicationLauncher(Mock()) |
2183 | + launcher.setUp() |
2184 | + launcher._maybe_add_application_cleanups(state) |
2185 | + self.assertThat(launcher._cleanups._cleanups, HasLength(0)) |
2186 | + |
2187 | + def test_add_application_cleanups_does_nothing_when_app_failed(self): |
2188 | + state = { |
2189 | + 'status': UpstartApplicationLauncher.Failed, |
2190 | + } |
2191 | + launcher = UpstartApplicationLauncher(Mock()) |
2192 | + launcher.setUp() |
2193 | + launcher._maybe_add_application_cleanups(state) |
2194 | + self.assertThat(launcher._cleanups._cleanups, HasLength(0)) |
2195 | + |
2196 | + def test_attach_application_log_does_nothing_wth_no_log_specified(self): |
2197 | + app_id = self.getUniqueString() |
2198 | + case_addDetail = Mock() |
2199 | + launcher = UpstartApplicationLauncher(case_addDetail) |
2200 | + with patch.object(_l.UpstartAppLaunch, 'application_log_path') as p: |
2201 | + p.return_value = None |
2202 | + launcher._attach_application_log(app_id) |
2203 | + |
2204 | + p.assert_called_once_with(app_id) |
2205 | + self.assertEqual(0, case_addDetail.call_count) |
2206 | + |
2207 | + def test_attach_application_log_attaches_log_file(self): |
2208 | + token = self.getUniqueString() |
2209 | + case_addDetail = Mock() |
2210 | + launcher = UpstartApplicationLauncher(case_addDetail) |
2211 | + app_id = self.getUniqueString() |
2212 | + with tempfile.NamedTemporaryFile(mode='w') as f: |
2213 | + f.write(token) |
2214 | + f.flush() |
2215 | + with patch.object(_l.UpstartAppLaunch, 'application_log_path', |
2216 | + return_value=f.name): |
2217 | + launcher._attach_application_log(app_id) |
2218 | + |
2219 | + self.assertEqual(1, case_addDetail.call_count) |
2220 | + content_name, content_obj = case_addDetail.call_args[0] |
2221 | + self.assertEqual("Application Log", content_name) |
2222 | + self.assertThat(content_obj.as_text(), Contains(token)) |
2223 | + |
2224 | + def test_stop_applications_calls_UAL_stop_application(self): |
2225 | + app_id = self.getUniqueString() |
2226 | + launcher = UpstartApplicationLauncher(Mock()) |
2227 | + with patch.object(_l.UpstartAppLaunch, 'stop_application') as p_stop: |
2228 | + launcher._stop_application(app_id) |
2229 | + p_stop.assert_called_once_with(app_id) |
2230 | |
2231 | |
2232 | class ApplicationLauncherInternalTests(TestCase): |
2233 | |
2234 | - def test_get_click_app_status(self): |
2235 | - with patch.object(_l, '_call_upstart_with_args') as call_upstart: |
2236 | - _get_click_app_status("app_id") |
2237 | - call_upstart.assert_called_with( |
2238 | - "status", |
2239 | - "application-click", |
2240 | - "APP_ID=app_id" |
2241 | - ) |
2242 | - |
2243 | - def test_get_click_app_pid(self): |
2244 | - with patch.object(_l.subprocess, 'check_output') as check_output: |
2245 | - check_output.return_value = """ |
2246 | - application-click (com.autopilot.testing.test_app_id) |
2247 | - dummy/data\napplication-click (blah) dummy/data\napplication-click |
2248 | - (com.autopilot.testing.test_app_id) start/running, process 1234 |
2249 | - """ |
2250 | - self.assertThat( |
2251 | - _get_click_app_pid("test_app_id"), |
2252 | - Equals(1234) |
2253 | - ) |
2254 | - |
2255 | - def test_get_click_app_pid_raises_runtimeerror_with_no_status(self): |
2256 | - test_app_id = self.getUniqueString() |
2257 | - expected_error = "Could not find autopilot interface for click "\ |
2258 | - "package '%s' after 10 seconds." % test_app_id |
2259 | - with sleep.mocked(): |
2260 | - with patch.object(_l, '_get_click_app_status', return_value=""): |
2261 | - self.assertThat( |
2262 | - lambda: _get_click_app_pid(test_app_id), |
2263 | - raises(RuntimeError(expected_error)) |
2264 | - ) |
2265 | - |
2266 | - def test_get_click_app_pid_tries_10_times_and_raises(self): |
2267 | - test_app_name = self.getUniqueString() |
2268 | - expected_error = "Could not find autopilot interface for click "\ |
2269 | - "package '%s' after 10 seconds." % test_app_name |
2270 | - with sleep.mocked(): |
2271 | - with patch.object( |
2272 | - _l, '_get_click_app_status', |
2273 | - side_effect=subprocess.CalledProcessError(1, "") |
2274 | - ): |
2275 | - self.assertThat( |
2276 | - lambda: _get_click_app_pid(test_app_name), |
2277 | - raises(RuntimeError(expected_error)) |
2278 | - ) |
2279 | - self.assertThat( |
2280 | - sleep.total_time_slept(), |
2281 | - Equals(10) |
2282 | - ) |
2283 | - |
2284 | - @patch('autopilot.application._launcher.subprocess.check_output') |
2285 | - def test_launch_click_app_starts_application(self, check_output): |
2286 | - test_app_name = self.getUniqueString() |
2287 | - with patch.object( |
2288 | - _l, '_get_click_app_pid' |
2289 | - ) as patched_get_click_app_pid: |
2290 | - _launch_click_app(test_app_name, "") |
2291 | - |
2292 | - check_output.assert_called_with([ |
2293 | - "/sbin/start", |
2294 | - "application", |
2295 | - "APP_ID=%s" % test_app_name, |
2296 | - "APP_URIS=''", |
2297 | - ]) |
2298 | - patched_get_click_app_pid.assert_called_with(test_app_name) |
2299 | - |
2300 | - @patch('autopilot.application._launcher.subprocess.check_output') |
2301 | - def test_launch_click_app_starts_application_with_uri(self, check_output): |
2302 | - test_app_name = self.getUniqueString() |
2303 | - test_app_uris = "%s %s" % (self.getUniqueString(), |
2304 | - self.getUniqueString()) |
2305 | - with patch.object( |
2306 | - _l, '_get_click_app_pid' |
2307 | - ) as patched_get_click_app_pid: |
2308 | - _launch_click_app(test_app_name, test_app_uris) |
2309 | - |
2310 | - check_output.assert_called_with([ |
2311 | - "/sbin/start", |
2312 | - "application", |
2313 | - "APP_ID=%s" % test_app_name, |
2314 | - "APP_URIS='%s'" % test_app_uris, |
2315 | - ]) |
2316 | - patched_get_click_app_pid.assert_called_with(test_app_name) |
2317 | - |
2318 | def test_get_app_env_from_string_hint_returns_qt_env(self): |
2319 | self.assertThat( |
2320 | _get_app_env_from_string_hint('QT'), |
2321 | @@ -517,27 +649,6 @@ |
2322 | )) |
2323 | ) |
2324 | |
2325 | - @patch.object(_l, '_attempt_kill_pid') |
2326 | - def test_kill_pid_succeeds(self, patched_killpg): |
2327 | - with patch.object( |
2328 | - _l, '_is_process_running', return_value=False |
2329 | - ) as proc_running: |
2330 | - _kill_pid(0) |
2331 | - proc_running.assert_called_once_with(0) |
2332 | - patched_killpg.assert_called_once_with(0) |
2333 | - |
2334 | - @patch.object(_l, '_attempt_kill_pid') |
2335 | - def test_kill_pid_retried(self, patched_killpid): |
2336 | - with sleep.mocked(): |
2337 | - with patch.object( |
2338 | - _l, '_is_process_running', return_value=True |
2339 | - ) as proc_running: |
2340 | - _kill_pid(0) |
2341 | - proc_running.assert_called_with(0) |
2342 | - self.assertThat(proc_running.call_count, GreaterThan(1)) |
2343 | - self.assertThat(patched_killpid.call_count, Equals(2)) |
2344 | - patched_killpid.assert_called_with(0, signal.SIGKILL) |
2345 | - |
2346 | @patch.object(_l.os, 'killpg') |
2347 | def test_attempt_kill_pid_logs_if_process_already_exited(self, killpg): |
2348 | killpg.side_effect = OSError() |
2349 | |
2350 | === modified file 'autopilot/tests/unit/test_introspection_features.py' |
2351 | --- autopilot/tests/unit/test_introspection_features.py 2014-03-11 17:36:53 +0000 |
2352 | +++ autopilot/tests/unit/test_introspection_features.py 2014-03-20 12:37:25 +0000 |
2353 | @@ -27,6 +27,7 @@ |
2354 | from testtools import TestCase |
2355 | from testtools.matchers import ( |
2356 | Equals, |
2357 | + IsInstance, |
2358 | Not, |
2359 | NotEquals, |
2360 | Raises, |
2361 | @@ -46,13 +47,18 @@ |
2362 | _get_application_name_from_dbus_address, |
2363 | _get_search_criteria_string_representation, |
2364 | _maybe_filter_connections_by_app_name, |
2365 | + get_classname_from_path, |
2366 | get_proxy_object_for_existing_process, |
2367 | ProcessSearchError, |
2368 | ) |
2369 | from autopilot.introspection.dbus import ( |
2370 | _get_filter_string_for_key_value_pair, |
2371 | + _get_default_proxy_class, |
2372 | _is_valid_server_side_filter_param, |
2373 | + _get_proxy_object_class, |
2374 | _object_passes_filters, |
2375 | + _object_registry, |
2376 | + _try_custom_proxy_classes, |
2377 | CustomEmulatorBase, |
2378 | DBusIntrospectionObject, |
2379 | ) |
2380 | @@ -639,3 +645,194 @@ |
2381 | ) |
2382 | ) |
2383 | ) |
2384 | + |
2385 | + |
2386 | +class MakeIntrospectionObjectTests(TestCase): |
2387 | + |
2388 | + """Test selection of custom proxy object class.""" |
2389 | + |
2390 | + class DefaultSelector(CustomEmulatorBase): |
2391 | + pass |
2392 | + |
2393 | + class AlwaysSelected(CustomEmulatorBase): |
2394 | + @classmethod |
2395 | + def validate_dbus_object(cls, path, state): |
2396 | + """Validate always. |
2397 | + |
2398 | + :returns: True |
2399 | + |
2400 | + """ |
2401 | + return True |
2402 | + |
2403 | + class NeverSelected(CustomEmulatorBase): |
2404 | + @classmethod |
2405 | + def validate_dbus_object(cls, path, state): |
2406 | + """Validate never. |
2407 | + |
2408 | + :returns: False |
2409 | + |
2410 | + """ |
2411 | + return False |
2412 | + |
2413 | + def test_class_has_validation_method(self): |
2414 | + """Verify that a class has a validation method by default.""" |
2415 | + self.assertTrue(callable(self.DefaultSelector.validate_dbus_object)) |
2416 | + |
2417 | + @patch('autopilot.introspection.dbus._get_proxy_object_class') |
2418 | + def test_make_introspection_object(self, gpoc): |
2419 | + """Verify that make_introspection_object makes the right call.""" |
2420 | + gpoc.return_value = self.DefaultSelector |
2421 | + fake_object = self.DefaultSelector( |
2422 | + dict(id=[0, 123], path=[0, '/some/path']), |
2423 | + '/', |
2424 | + Mock() |
2425 | + ) |
2426 | + new_fake = fake_object.make_introspection_object(('/Object', {})) |
2427 | + self.assertThat(new_fake, IsInstance(self.DefaultSelector)) |
2428 | + gpoc.assert_called_once_with( |
2429 | + _object_registry[fake_object._id], |
2430 | + self.DefaultSelector, |
2431 | + '/Object', |
2432 | + {} |
2433 | + ) |
2434 | + |
2435 | + @patch('autopilot.introspection.dbus._try_custom_proxy_classes') |
2436 | + @patch('autopilot.introspection.dbus._get_default_proxy_class') |
2437 | + def test_get_proxy_object_class_return_from_list(self, gdpc, tcpc): |
2438 | + """_get_proxy_object_class should return the value of |
2439 | + _try_custom_proxy_classes if there is one.""" |
2440 | + token = self.getUniqueString() |
2441 | + tcpc.return_value = token |
2442 | + gpoc_return = _get_proxy_object_class(None, None, None, None) |
2443 | + |
2444 | + self.assertThat(gpoc_return, Equals(token)) |
2445 | + self.assertFalse(gdpc.called) |
2446 | + |
2447 | + @patch('autopilot.introspection.dbus._try_custom_proxy_classes') |
2448 | + def test_get_proxy_object_class_send_right_args(self, tcpc): |
2449 | + """_get_proxy_object_class should send the right arguments to |
2450 | + _try_custom_proxy_classes.""" |
2451 | + class_dict = {'DefaultSelector': self.DefaultSelector} |
2452 | + path = '/path/to/DefaultSelector' |
2453 | + state = {} |
2454 | + _get_proxy_object_class(class_dict, None, path, state) |
2455 | + tcpc.assert_called_once_with(class_dict, path, state) |
2456 | + |
2457 | + @patch('autopilot.introspection.dbus._try_custom_proxy_classes') |
2458 | + def test_get_proxy_object_class_not_handle_error(self, tcpc): |
2459 | + """_get_proxy_object_class should not handle an exception raised by |
2460 | + _try_custom_proxy_classes.""" |
2461 | + tcpc.side_effect = ValueError |
2462 | + self.assertThat( |
2463 | + lambda: _get_proxy_object_class( |
2464 | + None, |
2465 | + None, |
2466 | + None, |
2467 | + None |
2468 | + ), |
2469 | + raises(ValueError)) |
2470 | + |
2471 | + @patch('autopilot.introspection.dbus._try_custom_proxy_classes') |
2472 | + @patch('autopilot.introspection.dbus._get_default_proxy_class') |
2473 | + @patch('autopilot.introspection.dbus.get_classname_from_path') |
2474 | + def test_get_proxy_object_class_call_default_call(self, gcfp, gdpc, tcpc): |
2475 | + """_get_proxy_object_class should call _get_default_proxy_class if |
2476 | + _try_custom_proxy_classes returns None.""" |
2477 | + tcpc.return_value = None |
2478 | + _get_proxy_object_class(None, None, None, None) |
2479 | + self.assertTrue(gdpc.called) |
2480 | + |
2481 | + @patch('autopilot.introspection.dbus._try_custom_proxy_classes') |
2482 | + @patch('autopilot.introspection.dbus._get_default_proxy_class') |
2483 | + def test_get_proxy_object_class_default_args(self, gdpc, tcpc): |
2484 | + """_get_proxy_object_class should pass the correct arguments to |
2485 | + _get_default_proxy_class""" |
2486 | + tcpc.return_value = None |
2487 | + default = self.DefaultSelector |
2488 | + path = '/path/to/DefaultSelector' |
2489 | + _get_proxy_object_class(None, default, path, None) |
2490 | + gdpc.assert_called_once_with(default, get_classname_from_path(path)) |
2491 | + |
2492 | + @patch('autopilot.introspection.dbus._try_custom_proxy_classes') |
2493 | + @patch('autopilot.introspection.dbus._get_default_proxy_class') |
2494 | + @patch('autopilot.introspection.dbus.get_classname_from_path') |
2495 | + def test_get_proxy_object_class_default(self, gcfp, gdpc, tcpc): |
2496 | + """_get_proxy_object_class should return the value of |
2497 | + _get_default_proxy_class if _try_custom_proxy_classes returns None.""" |
2498 | + token = self.getUniqueString() |
2499 | + gdpc.return_value = token |
2500 | + tcpc.return_value = None |
2501 | + gpoc_return = _get_proxy_object_class(None, None, None, None) |
2502 | + self.assertThat(gpoc_return, Equals(token)) |
2503 | + |
2504 | + def test_try_custom_proxy_classes_zero_results(self): |
2505 | + """_try_custom_proxy_classes must return None if no classes match.""" |
2506 | + proxy_class_dict = {'NeverSelected': self.NeverSelected} |
2507 | + path = '/path/to/NeverSelected' |
2508 | + state = {} |
2509 | + class_type = _try_custom_proxy_classes(proxy_class_dict, path, state) |
2510 | + self.assertThat(class_type, Equals(None)) |
2511 | + |
2512 | + def test_try_custom_proxy_classes_one_result(self): |
2513 | + """_try_custom_proxy_classes must return the matching class if there is |
2514 | + exacly 1.""" |
2515 | + proxy_class_dict = {'DefaultSelector': self.DefaultSelector} |
2516 | + path = '/path/to/DefaultSelector' |
2517 | + state = {} |
2518 | + class_type = _try_custom_proxy_classes(proxy_class_dict, path, state) |
2519 | + self.assertThat(class_type, Equals(self.DefaultSelector)) |
2520 | + |
2521 | + def test_try_custom_proxy_classes_two_results(self): |
2522 | + """_try_custom_proxy_classes must raise ValueError if multiple classes |
2523 | + match.""" |
2524 | + proxy_class_dict = {'DefaultSelector': self.DefaultSelector, |
2525 | + 'AlwaysSelected': self.AlwaysSelected} |
2526 | + path = '/path/to/DefaultSelector' |
2527 | + state = {} |
2528 | + self.assertThat( |
2529 | + lambda: _try_custom_proxy_classes( |
2530 | + proxy_class_dict, |
2531 | + path, |
2532 | + state |
2533 | + ), |
2534 | + raises(ValueError) |
2535 | + ) |
2536 | + |
2537 | + @patch('autopilot.introspection.dbus.get_debug_logger') |
2538 | + def test_get_default_proxy_class_logging(self, gdl): |
2539 | + """_get_default_proxy_class should log a message.""" |
2540 | + _get_default_proxy_class(self.DefaultSelector, None) |
2541 | + gdl.assert_called_once_with() |
2542 | + |
2543 | + def test_get_default_proxy_class_base(self): |
2544 | + """Subclass must return an emulator of base class.""" |
2545 | + class SubclassedProxy(self.DefaultSelector): |
2546 | + pass |
2547 | + |
2548 | + result = _get_default_proxy_class(SubclassedProxy, 'Object') |
2549 | + self.assertTrue(result, Equals(self.DefaultSelector)) |
2550 | + |
2551 | + def test_get_default_proxy_class_base_instead_of_self(self): |
2552 | + """Subclass must not use self if base class works.""" |
2553 | + class SubclassedProxy(self.DefaultSelector): |
2554 | + pass |
2555 | + |
2556 | + result = _get_default_proxy_class(SubclassedProxy, 'Object') |
2557 | + self.assertFalse(issubclass(result, SubclassedProxy)) |
2558 | + |
2559 | + def test_get_default_proxy_class(self): |
2560 | + """Must default to own class if no usable bases present.""" |
2561 | + result = _get_default_proxy_class(self.DefaultSelector, 'Object') |
2562 | + self.assertTrue(result, Equals(self.DefaultSelector)) |
2563 | + |
2564 | + def test_get_default_proxy_name(self): |
2565 | + """Must default to own class if no usable bases present.""" |
2566 | + token = self.getUniqueString() |
2567 | + result = _get_default_proxy_class(self.DefaultSelector, token) |
2568 | + self.assertThat(result.__name__, Equals(token)) |
2569 | + |
2570 | + def test_validate_dbus_object_matches_on_class_name(self): |
2571 | + """Validate_dbus_object must match class name.""" |
2572 | + selected = self.DefaultSelector.validate_dbus_object( |
2573 | + '/DefaultSelector', {}) |
2574 | + self.assertTrue(selected) |
2575 | |
2576 | === modified file 'autopilot/tests/unit/test_test_fixtures.py' |
2577 | --- autopilot/tests/unit/test_test_fixtures.py 2014-02-27 00:20:32 +0000 |
2578 | +++ autopilot/tests/unit/test_test_fixtures.py 2014-03-20 12:37:25 +0000 |
2579 | @@ -17,15 +17,19 @@ |
2580 | # along with this program. If not, see <http://www.gnu.org/licenses/>. |
2581 | # |
2582 | |
2583 | -from autopilot.tests.functional import TempDesktopFile |
2584 | +from autopilot.tests.functional.fixtures import ( |
2585 | + ExecutableScript, |
2586 | + TempDesktopFile, |
2587 | +) |
2588 | |
2589 | +import os |
2590 | import os.path |
2591 | +import stat |
2592 | from mock import patch |
2593 | from shutil import rmtree |
2594 | import tempfile |
2595 | from testtools import TestCase |
2596 | -from testtools.matchers import Equals, FileContains |
2597 | -from textwrap import dedent |
2598 | +from testtools.matchers import Contains, EndsWith, Equals, FileContains |
2599 | |
2600 | |
2601 | class TempDesktopFileTests(TestCase): |
2602 | @@ -121,7 +125,7 @@ |
2603 | |
2604 | def test_remove_desktop_file_removes_created_file_when_path_exists(self): |
2605 | test_created_path = self.getUniqueString() |
2606 | - with patch('autopilot.tests.functional.rmtree') as p_rmtree: |
2607 | + with patch('autopilot.tests.functional.fixtures.rmtree') as p_rmtree: |
2608 | TempDesktopFile._remove_desktop_file_components( |
2609 | test_created_path, "" |
2610 | ) |
2611 | @@ -142,28 +146,70 @@ |
2612 | with patch.object( |
2613 | TempDesktopFile, '_desktop_file_dir', return_value=desktop_file_dir |
2614 | ): |
2615 | - desktop_file = TempDesktopFile._create_desktop_file() |
2616 | + desktop_file = TempDesktopFile._create_desktop_file("") |
2617 | path, head = os.path.split(desktop_file) |
2618 | self.assertThat(path, Equals(desktop_file_dir)) |
2619 | |
2620 | def test_create_desktop_file_writes_correct_data(self): |
2621 | desktop_file_dir = tempfile.mkdtemp(dir="/tmp") |
2622 | self.addCleanup(rmtree, desktop_file_dir) |
2623 | + token = self.getUniqueString() |
2624 | |
2625 | with patch.object( |
2626 | TempDesktopFile, '_desktop_file_dir', return_value=desktop_file_dir |
2627 | ): |
2628 | - desktop_file = TempDesktopFile._create_desktop_file() |
2629 | + desktop_file = TempDesktopFile._create_desktop_file(token) |
2630 | self.assertTrue(desktop_file.endswith('.desktop')) |
2631 | - self.assertThat( |
2632 | - desktop_file, |
2633 | - FileContains( |
2634 | - dedent("""\ |
2635 | - [Desktop Entry] |
2636 | - Type=Application |
2637 | - Exec=Not important |
2638 | - Path=Not important |
2639 | - Name=Test app |
2640 | - Icon=Not important""") |
2641 | - ) |
2642 | - ) |
2643 | + self.assertThat(desktop_file, FileContains(token)) |
2644 | + |
2645 | + def do_parameter_contents_test(self, matcher, **kwargs): |
2646 | + fixture = self.useFixture(TempDesktopFile(**kwargs)) |
2647 | + self.assertThat( |
2648 | + fixture.get_desktop_file_path(), |
2649 | + FileContains(matcher=matcher), |
2650 | + ) |
2651 | + |
2652 | + def test_can_specify_exec_path(self): |
2653 | + token = self.getUniqueString() |
2654 | + self.do_parameter_contents_test( |
2655 | + Contains("Exec="+token), |
2656 | + exec_=token |
2657 | + ) |
2658 | + |
2659 | + def test_can_specify_type(self): |
2660 | + token = self.getUniqueString() |
2661 | + self.do_parameter_contents_test( |
2662 | + Contains("Type="+token), |
2663 | + type=token |
2664 | + ) |
2665 | + |
2666 | + def test_can_specify_name(self): |
2667 | + token = self.getUniqueString() |
2668 | + self.do_parameter_contents_test( |
2669 | + Contains("Name="+token), |
2670 | + name=token |
2671 | + ) |
2672 | + |
2673 | + def test_can_specify_icon(self): |
2674 | + token = self.getUniqueString() |
2675 | + self.do_parameter_contents_test( |
2676 | + Contains("Icon="+token), |
2677 | + icon=token |
2678 | + ) |
2679 | + |
2680 | + |
2681 | +class ExecutableScriptTests(TestCase): |
2682 | + |
2683 | + def test_creates_file_with_content(self): |
2684 | + token = self.getUniqueString() |
2685 | + fixture = self.useFixture(ExecutableScript(script=token)) |
2686 | + self.assertThat(fixture.path, FileContains(token)) |
2687 | + |
2688 | + def test_creates_file_with_correct_extension(self): |
2689 | + token = self.getUniqueString() |
2690 | + fixture = self.useFixture(ExecutableScript(script="", extension=token)) |
2691 | + self.assertThat(fixture.path, EndsWith(token)) |
2692 | + |
2693 | + def test_creates_file_with_execute_bit_set(self): |
2694 | + fixture = self.useFixture(ExecutableScript(script="")) |
2695 | + self.assertTrue(os.stat(fixture.path).st_mode & stat.S_IXUSR) |
2696 | |
2697 | === modified file 'autopilot/utilities.py' |
2698 | --- autopilot/utilities.py 2014-02-25 22:25:32 +0000 |
2699 | +++ autopilot/utilities.py 2014-03-20 12:37:25 +0000 |
2700 | @@ -63,6 +63,7 @@ |
2701 | # Taken from http://ur1.ca/eqapv |
2702 | # licensed under the MIT license. |
2703 | class Silence(object): |
2704 | + |
2705 | """Context manager which uses low-level file descriptors to suppress |
2706 | output to stdout/stderr, optionally redirecting to the named file(s). |
2707 | |
2708 | @@ -72,6 +73,7 @@ |
2709 | # do something that prints to stdout or stderr |
2710 | |
2711 | """ |
2712 | + |
2713 | def __init__(self, stdout=os.devnull, stderr=os.devnull, mode='wb'): |
2714 | self.outfiles = stdout, stderr |
2715 | self.combine = (stdout == stderr) |
2716 | @@ -138,6 +140,7 @@ |
2717 | |
2718 | |
2719 | class Timer(object): |
2720 | + |
2721 | """A context-manager that times a block of code, writing the results to |
2722 | the log.""" |
2723 | |
2724 | @@ -158,6 +161,7 @@ |
2725 | |
2726 | |
2727 | class StagnantStateDetector(object): |
2728 | + |
2729 | """Detect when the state of something doesn't change over many iterations. |
2730 | |
2731 | |
2732 | @@ -185,7 +189,7 @@ |
2733 | :param threshold: Amount of times the updated state can fail to |
2734 | differ consecutively before raising an exception. |
2735 | |
2736 | - :raises: **ValueError** if *threshold* isn't a positive integer. |
2737 | + :raises ValueError: if *threshold* isn't a positive integer. |
2738 | |
2739 | """ |
2740 | if type(threshold) is not int or threshold <= 0: |
2741 | @@ -195,13 +199,13 @@ |
2742 | self._previous_state_hash = -1 |
2743 | |
2744 | def check_state(self, *state): |
2745 | - """Checks if there is a difference between the previous state and |
2746 | + """Check if there is a difference between the previous state and |
2747 | state. |
2748 | |
2749 | :param state: Hashable state argument to compare against the previous |
2750 | iteration |
2751 | |
2752 | - :raises: **TypeError** when state is unhashable |
2753 | + :raises TypeError: when state is unhashable |
2754 | |
2755 | """ |
2756 | state_hash = hash(state) |
2757 | @@ -218,7 +222,11 @@ |
2758 | |
2759 | |
2760 | def get_debug_logger(): |
2761 | - """Get a logging object to be used as a debug logger only.""" |
2762 | + """Get a logging object to be used as a debug logger only. |
2763 | + |
2764 | + :returns: logger object from logging module |
2765 | + |
2766 | + """ |
2767 | logger = logging.getLogger("autopilot.debug") |
2768 | logger.addFilter(DebugLogFilter()) |
2769 | return logger |
2770 | @@ -258,6 +266,7 @@ |
2771 | |
2772 | |
2773 | class _CleanupWrapper(object): |
2774 | + |
2775 | """Support for calling 'addCleanup' outside the test case.""" |
2776 | |
2777 | def __init__(self): |
2778 | @@ -285,7 +294,9 @@ |
2779 | |
2780 | |
2781 | class _TestCleanupMeta(type): |
2782 | - """Metaclass to inject the object into on test start/end functionality""" |
2783 | + |
2784 | + """Metaclass to inject the object into on test start/end functionality.""" |
2785 | + |
2786 | def __new__(cls, classname, bases, classdict): |
2787 | class EmptyStaticMethod(object): |
2788 | """Class used to give us 'default classmethods' for those that |
2789 | @@ -346,6 +357,7 @@ |
2790 | |
2791 | |
2792 | class MockableSleep(object): |
2793 | + |
2794 | """Delay execution for a certain number of seconds. |
2795 | |
2796 | Functionally identical to `time.sleep`, except we can replace it during |
2797 | @@ -365,6 +377,7 @@ |
2798 | self.assertEqual(mock_sleep.total_time_slept(), 10.0) |
2799 | |
2800 | """ |
2801 | + |
2802 | def __init__(self): |
2803 | self._mock_count = 0.0 |
2804 | self._mocked = False |
2805 | |
2806 | === modified file 'debian/control' |
2807 | --- debian/control 2014-02-19 21:41:26 +0000 |
2808 | +++ debian/control 2014-03-20 12:37:25 +0000 |
2809 | @@ -9,6 +9,7 @@ |
2810 | gir1.2-gconf-2.0, |
2811 | gir1.2-gtk-3.0, |
2812 | gir1.2-ibus-1.0, |
2813 | + gir1.2-upstart-app-launch-2, |
2814 | graphviz, |
2815 | liblttng-ust-dev, |
2816 | python-all-dev (>= 2.6), |
2817 | @@ -56,6 +57,7 @@ |
2818 | Architecture: all |
2819 | Depends: ${misc:Depends}, |
2820 | ${python:Depends}, |
2821 | + gir1.2-upstart-app-launch-2, |
2822 | python-contextlib2, |
2823 | python-dbus, |
2824 | python-decorator, |
2825 | @@ -84,6 +86,7 @@ |
2826 | Architecture: all |
2827 | Depends: ${misc:Depends}, |
2828 | ${python3:Depends}, |
2829 | + gir1.2-upstart-app-launch-2, |
2830 | python3-dbus, |
2831 | python3-decorator, |
2832 | python3-fixtures, |
This is smaller, but it should be ready to go.