2
Autopilot tests for Unity.
6
from compizconfig import Setting, Plugin
7
from dbus import DBusException
10
from StringIO import StringIO
11
from subprocess import call, check_output, Popen, PIPE, STDOUT
12
from tempfile import mktemp
13
from testscenarios import TestWithScenarios
14
from testtools import TestCase
15
from testtools.content import text_content
16
from testtools.matchers import Equals
19
from autopilot.emulators.bamf import Bamf
20
from autopilot.emulators.unity import (
25
from autopilot.emulators.unity.dash import Dash
26
from autopilot.emulators.unity.hud import Hud
27
from autopilot.emulators.unity.launcher import LauncherController
28
from autopilot.emulators.unity.panel import PanelController
29
from autopilot.emulators.unity.switcher import Switcher
30
from autopilot.emulators.unity.window_manager import WindowManager
31
from autopilot.emulators.unity.workspace import WorkspaceManager
32
from autopilot.emulators.X11 import ScreenGeometry, Keyboard, Mouse, reset_display
33
from autopilot.glibrunner import GlibRunner
34
from autopilot.globals import (global_context,
35
video_recording_enabled,
36
video_record_directory,
38
from autopilot.keybindings import KeybindingsHelper
41
logger = logging.getLogger(__name__)
45
from testscenarios.scenarios import multiply_scenarios
47
from itertools import product
48
def multiply_scenarios(*scenarios):
49
"""Multiply two or more iterables of scenarios.
51
It is safe to pass scenario generators or iterators.
53
:returns: A list of compound scenarios: the cross-product of all
54
scenarios, with the names concatenated and the parameters
58
scenario_lists = map(list, scenarios)
59
for combination in product(*scenario_lists):
60
names, parameters = zip(*combination)
61
scenario_name = ','.join(names)
62
scenario_parameters = {}
63
for parameter in parameters:
64
scenario_parameters.update(parameter)
65
result.append((scenario_name, scenario_parameters))
69
class LoggedTestCase(TestWithScenarios, TestCase):
70
"""Initialize the logging for the test case."""
73
self._setUpTestLogging()
74
self._setUpUnityLogging()
75
# The reason that the super setup is done here is due to making sure
76
# that the logging is properly set up prior to calling it.
77
super(LoggedTestCase, self).setUp()
79
def _setUpTestLogging(self):
80
class MyFormatter(logging.Formatter):
82
def formatTime(self, record, datefmt=None):
83
ct = self.converter(record.created)
85
s = time.strftime(datefmt, ct)
87
t = time.strftime("%H:%M:%S", ct)
88
s = "%s.%03d" % (t, record.msecs)
91
self._log_buffer = StringIO()
92
root_logger = logging.getLogger()
93
root_logger.setLevel(logging.DEBUG)
94
handler = logging.StreamHandler(stream=self._log_buffer)
95
log_format = "%(asctime)s %(levelname)s %(module)s:%(lineno)d - %(message)s"
96
handler.setFormatter(MyFormatter(log_format))
97
root_logger.addHandler(handler)
98
#Tear down logging in a cleanUp handler, so it's done after all other
99
# tearDown() calls and cleanup handlers.
100
self.addCleanup(self._tearDownLogging)
102
def _tearDownLogging(self):
103
logger = logging.getLogger()
104
for handler in logger.handlers:
106
self._log_buffer.seek(0)
107
self.addDetail('test-log', text_content(self._log_buffer.getvalue()))
108
logger.removeHandler(handler)
109
# Calling del to remove the handler and flush the buffer. We are
110
# abusing the log handlers here a little.
113
def _setUpUnityLogging(self):
114
self._unity_log_file_name = mktemp(prefix=self.shortDescription())
115
start_log_to_file(self._unity_log_file_name)
116
self.addCleanup(self._tearDownUnityLogging)
118
def _tearDownUnityLogging(self):
119
# If unity dies, our dbus interface has gone, and reset_logging will fail
120
# but we still want our log, so we ignore any errors.
123
except DBusException:
125
with open(self._unity_log_file_name) as unity_log:
126
self.addDetail('unity-log', text_content(unity_log.read()))
127
os.remove(self._unity_log_file_name)
128
self._unity_log_file_name = ""
130
def set_unity_log_level(self, component, level):
131
"""Set the unity log level for 'component' to 'level'.
133
Valid levels are: TRACE, DEBUG, INFO, WARNING and ERROR.
135
Components are dotted unity component names. The empty string specifies
136
the root logging component.
138
set_log_severity(component, level)
141
class VideoCapturedTestCase(LoggedTestCase):
142
"""Video capture autopilot tests, saving the results if the test failed."""
144
_recording_app = '/usr/bin/recordmydesktop'
145
_recording_opts = ['--no-sound', '--no-frame', '-o',]
148
super(VideoCapturedTestCase, self).setUp()
149
global video_recording_enabled
150
if video_recording_enabled and not self._have_recording_app():
151
video_recording_enabled = False
152
logger.warning("Disabling video capture since '%s' is not present", self._recording_app)
154
if video_recording_enabled:
155
self._test_passed = True
156
self.addOnException(self._on_test_failed)
157
self.addCleanup(self._stop_video_capture)
158
self._start_video_capture()
160
def _have_recording_app(self):
161
return os.path.exists(self._recording_app)
163
def _start_video_capture(self):
164
args = self._get_capture_command_line()
165
self._capture_file = self._get_capture_output_file()
166
self._ensure_directory_exists_but_not_file(self._capture_file)
167
args.append(self._capture_file)
168
logger.debug("Starting: %r", args)
169
self._capture_process = Popen(args, stdout=PIPE, stderr=STDOUT)
171
def _stop_video_capture(self):
172
"""Stop the video capture. If the test failed, save the resulting file."""
174
if self._test_passed:
175
# We use kill here because we don't want the recording app to start
176
# encoding the video file (since we're removing it anyway.)
177
self._capture_process.kill()
178
self._capture_process.wait()
180
self._capture_process.terminate()
181
self._capture_process.wait()
182
if self._capture_process.returncode != 0:
183
self.addDetail('video capture log', text_content(self._capture_process.stdout.read()))
184
self._capture_process = None
186
def _get_capture_command_line(self):
187
return [self._recording_app] + self._recording_opts
189
def _get_capture_output_file(self):
190
return os.path.join(video_record_directory, '%s.ogv' % (self.shortDescription()))
192
def _ensure_directory_exists_but_not_file(self, file_path):
193
dirpath = os.path.dirname(file_path)
194
if not os.path.exists(dirpath):
196
elif os.path.exists(file_path):
197
logger.warning("Video capture file '%s' already exists, deleting.", file_path)
200
def _on_test_failed(self, ex_info):
201
"""Called when a test fails."""
202
self._test_passed = False
205
class AutopilotTestCase(VideoCapturedTestCase, KeybindingsHelper):
206
"""Wrapper around testtools.TestCase that takes care of some cleaning."""
208
run_test_with = GlibRunner
212
'desktop-file': 'gucharmap.desktop',
213
'process-name': 'gucharmap',
216
'desktop-file': 'gcalctool.desktop',
217
'process-name': 'gcalctool',
220
'desktop-file': 'mahjongg.desktop',
221
'process-name': 'mahjongg',
224
'desktop-file': 'remmina.desktop',
225
'process-name': 'remmina',
227
'System Settings' : {
228
'desktop-file': 'gnome-control-center.desktop',
229
'process-name': 'gnome-control-center',
232
'desktop-file': 'gedit.desktop',
233
'process-name': 'gedit',
238
super(AutopilotTestCase, self).setUp()
240
self.keyboard = Keyboard()
244
self.launcher = self._get_launcher_controller()
245
self.panels = self._get_panel_controller()
246
self.switcher = Switcher()
247
self.window_manager = self._get_window_manager()
248
self.workspace = WorkspaceManager()
249
self.screen_geo = ScreenGeometry()
250
self.addCleanup(self.workspace.switch_to, self.workspace.current_workspace)
251
self.addCleanup(Keyboard.cleanup)
252
self.addCleanup(Mouse.cleanup)
254
def start_app(self, app_name, files=[], locale=None):
255
"""Start one of the known apps, and kill it on tear down.
257
If files is specified, start the application with the specified files.
258
If locale is specified, the locale will be set when the application is launched.
260
The method returns the BamfApplication instance.
264
os.putenv("LC_ALL", locale)
265
self.addCleanup(os.unsetenv, "LC_ALL")
266
logger.info("Starting application '%s' with files %r in locale %s", app_name, files, locale)
268
logger.info("Starting application '%s' with files %r", app_name, files)
270
app = self.KNOWN_APPS[app_name]
271
self.bamf.launch_application(app['desktop-file'], files)
272
apps = self.bamf.get_running_applications_by_desktop_file(app['desktop-file'])
273
self.addCleanup(call, "kill `pidof %s`" % (app['process-name']), shell=True)
274
self.assertThat(len(apps), Equals(1))
277
def close_all_app(self, app_name):
278
"""Close all instances of the app_name."""
279
app = self.KNOWN_APPS[app_name]
280
self.addCleanup(call, "kill `pidof %s`" % (app['process-name']), shell=True)
281
super(LoggedTestCase, self).tearDown()
283
def get_app_instances(self, app_name):
284
"""Get BamfApplication instances for app_name."""
285
desktop_file = self.KNOWN_APPS[app_name]['desktop-file']
286
return self.bamf.get_running_applications_by_desktop_file(desktop_file)
288
def app_is_running(self, app_name):
289
"""Returns true if an instance of the application is running."""
290
apps = self.get_app_instances(app_name)
293
def call_gsettings_cmd(self, command, schema, *args):
294
"""Set a desktop wide gsettings option
296
Using the gsettings command because there's a bug with importing
297
from gobject introspection and pygtk2 simultaneously, and the Xlib
298
keyboard layout bits are very unweildy. This seems like the best
299
solution, even a little bit brutish.
301
cmd = ['gsettings', command, schema] + list(args)
302
# strip to remove the trailing \n.
303
ret = check_output(cmd).strip()
308
def set_unity_option(self, option_name, option_value):
309
"""Set an option in the unity compiz plugin options.
311
The value will be set for the current test only.
314
self.set_compiz_option("unityshell", option_name, option_value)
316
def set_compiz_option(self, plugin_name, setting_name, setting_value):
317
"""Set setting `setting_name` in compiz plugin `plugin_name` to value `setting_value`
320
old_value = self._set_compiz_option(plugin_name, setting_name, setting_value)
321
self.addCleanup(self._set_compiz_option, plugin_name, setting_name, old_value)
322
# Allow unity time to respond to the new setting.
325
def _set_compiz_option(self, plugin_name, option_name, option_value):
326
logger.info("Setting compiz option '%s' in plugin '%s' to %r",
327
option_name, plugin_name, option_value)
328
plugin = Plugin(global_context, plugin_name)
329
setting = Setting(plugin, option_name)
330
old_value = setting.Value
331
setting.Value = option_value
332
global_context.Write()
335
def _get_launcher_controller(self):
336
controllers = LauncherController.get_all_instances()
337
self.assertThat(len(controllers), Equals(1))
338
return controllers[0]
340
def _get_panel_controller(self):
341
controllers = PanelController.get_all_instances()
342
self.assertThat(len(controllers), Equals(1))
343
return controllers[0]
345
def _get_window_manager(self):
346
managers = WindowManager.get_all_instances()
347
self.assertThat(len(managers), Equals(1))
350
def assertVisibleWindowStack(self, stack_start):
351
"""Check that the visible window stack starts with the windows passed in.
353
The start_stack is an iterable of BamfWindow objects.
354
Minimised windows are skipped.
357
stack = [win for win in self.bamf.get_open_windows() if not win.is_hidden]
358
for pos, win in enumerate(stack_start):
359
self.assertThat(stack[pos].x_id, Equals(win.x_id),
360
"%r at %d does not equal %r" % (stack[pos], pos, win))