Merge lp:~nuclearbob/autopilot/pep257-b into lp:autopilot

Proposed by Max Brustkern
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
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.

To post a comment you must log in.
Revision history for this message
Max Brustkern (nuclearbob) wrote :

This is smaller, but it should be ready to go.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
lp:~nuclearbob/autopilot/pep257-b updated
452. By Max Brustkern

Fixed flake8 issue

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

LGTM

review: Approve
lp:~nuclearbob/autopilot/pep257-b updated
453. By Max Brustkern

Merged trunk

454. By Max Brustkern

Resolved conflict with other branch

455. By Max Brustkern

Merged proposed release

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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,

Subscribers

People subscribed via source and target branches