Unity 8
 All Classes Functions Properties
__init__.py
1 # -*- Mode: Python; coding: utf-8; indent-tabs-mode: nil; tab-width: 4 -*-
2 #
3 # Unity Autopilot Test Suite
4 # Copyright (C) 2012, 2013, 2014 Canonical
5 #
6 # This program is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation, either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program. If not, see <http://www.gnu.org/licenses/>.
18 #
19 
20 """unity autopilot tests."""
21 
22 try:
23  from gi.repository import Gio
24 except ImportError:
25  Gio = None
26 
27 from autopilot.platform import model
28 from autopilot.testcase import AutopilotTestCase
29 from autopilot.matchers import Eventually
30 from autopilot.input import Touch
31 from autopilot.display import Display
32 import logging
33 import os.path
34 import subprocess
35 import sys
36 from testtools.matchers import Equals
37 
38 from unity8 import (
39  get_lib_path,
40  get_binary_path,
41  get_mocks_library_path,
42  get_default_extra_mock_libraries,
43  get_data_dirs
44 )
45 from unity8.process_helpers import restart_unity_with_testability
46 from unity8.shell.emulators import main_window as main_window_emulator
47 from unity8.shell.emulators.dash import Dash
48 
49 
50 logger = logging.getLogger(__name__)
51 
52 UNITYSHELL_GSETTINGS_SCHEMA = "org.compiz.unityshell"
53 UNITYSHELL_GSETTINGS_PATH = "/org/compiz/profiles/unity/plugins/unityshell/"
54 UNITYSHELL_LAUNCHER_KEY = "launcher-hide-mode"
55 UNITYSHELL_LAUNCHER_MODE = 1 # launcher hidden
56 
57 
58 def _get_device_emulation_scenarios(devices='All'):
59  nexus4 = ('Desktop Nexus 4',
60  dict(app_width=768, app_height=1280, grid_unit_px=18))
61  nexus10 = ('Desktop Nexus 10',
62  dict(app_width=2560, app_height=1600, grid_unit_px=20))
63  native = ('Native Device',
64  dict(app_width=0, app_height=0, grid_unit_px=0))
65 
66  if model() == 'Desktop':
67  if devices == 'All':
68  return [nexus4, nexus10]
69  elif devices == 'Nexus4':
70  return [nexus4]
71  elif devices == 'Nexus10':
72  return [nexus10]
73  else:
74  raise RuntimeError(
75  'Unrecognized device-option "%s" passed.' % devices
76  )
77  else:
78  return [native]
79 
80 
81 class UnityTestCase(AutopilotTestCase):
82 
83  """A test case base class for the Unity shell tests."""
84 
85  @classmethod
86  def setUpClass(cls):
87  try:
88  output = subprocess.check_output(
89  ["/sbin/initctl", "status", "unity8"],
90  stderr=subprocess.STDOUT,
91  universal_newlines=True,
92  )
93  except subprocess.CalledProcessError as e:
94  sys.stderr.write(
95  "Error: `initctl status unity8` failed, most probably the "
96  "unity8 session could not be found:\n\n"
97  "{0}\n"
98  "Please install unity8 or copy data/unity8.conf to "
99  "{1}\n".format(
100  e.output,
101  os.path.join(os.getenv("XDG_CONFIG_HOME",
102  os.path.join(os.getenv("HOME"),
103  ".config")
104  ),
105  "upstart")
106  )
107  )
108  sys.exit(1)
109 
110  if "start/" in output:
111  sys.stderr.write(
112  "Error: Unity is currently running, these tests require it to "
113  "be 'stopped'.\n"
114  "Please run this command before running these tests: \n"
115  "initctl stop unity8\n"
116  )
117  sys.exit(2)
118 
119  def setUp(self):
120  super(UnityTestCase, self).setUp()
121  if (Gio is not None and
122  UNITYSHELL_GSETTINGS_SCHEMA in
123  Gio.Settings.list_relocatable_schemas()):
124 
125  # Hide Unity launcher
126  self._unityshell_schema = Gio.Settings.new_with_path(
127  UNITYSHELL_GSETTINGS_SCHEMA,
128  UNITYSHELL_GSETTINGS_PATH,
129  )
130  self._launcher_hide_mode = self._unityshell_schema.get_int(
131  UNITYSHELL_LAUNCHER_KEY,
132  )
133  self._unityshell_schema.set_int(
134  UNITYSHELL_LAUNCHER_KEY,
135  UNITYSHELL_LAUNCHER_MODE,
136  )
137  self.addCleanup(self._reset_launcher)
138 
139  self._proxy = None
140  self._lightdm_mock_type = None
141  self._qml_mock_enabled = True
142  self._data_dirs_mock_enabled = True
143  self._environment = {}
144 
145  #### FIXME: This is a work around re: lp:1238417 ####
146  if model() != "Desktop":
147  from autopilot.input import _uinput
148  _uinput._touch_device = _uinput.create_touch_device()
149  self.addCleanup(_uinput._touch_device.close)
150  ####
151 
152  self.touch = Touch.create()
154 
155  def _reset_launcher(self):
156  """Reset Unity launcher hide mode"""
157  self._unityshell_schema.set_int(
158  UNITYSHELL_LAUNCHER_KEY,
159  self._launcher_hide_mode,
160  )
161 
162  def _setup_display_details(self):
163  scale_divisor = self._determine_geometry()
164  self._setup_grid_size(scale_divisor)
165 
166  def _determine_geometry(self):
167  """Use the geometry that may be supplied or use the default."""
168  width = getattr(self, 'app_width', 0)
169  height = getattr(self, 'app_height', 0)
170  scale_divisor = 1
171  self.unity_geometry_args = []
172  if width > 0 and height > 0:
173  if self._geo_larger_than_display(width, height):
174  scale_divisor = self._get_scaled_down_geo(width, height)
175  width = width / scale_divisor
176  height = height / scale_divisor
177  logger.info(
178  "Geometry larger than display, scaled down to: %dx%d",
179  width,
180  height
181  )
182  geo_string = "%dx%d" % (width, height)
183  self.unity_geometry_args = [
184  '-windowgeometry',
185  geo_string,
186  '-frameless',
187  '-mousetouch'
188  ]
189  return scale_divisor
190 
191  def _setup_grid_size(self, scale_divisor):
192  """Use the grid size that may be supplied or use the default."""
193  if getattr(self, 'grid_unit_px', 0) == 0:
194  self.grid_size = int(os.getenv('GRID_UNIT_PX'))
195  else:
196  self.grid_size = int(self.grid_unit_px / scale_divisor)
197  self._environment["GRID_UNIT_PX"] = str(self.grid_size)
198  # FIXME this is only needed for Hud.get_close_button_coords
199  # we should probably rework it so that it's not required
200  self.patch_environment("GRID_UNIT_PX", str(self.grid_size))
201 
202  def _geo_larger_than_display(self, width, height):
203  should_scale = getattr(self, 'scale_geo', True)
204  if should_scale:
205  screen = Display.create()
206  screen_width = screen.get_screen_width()
207  screen_height = screen.get_screen_height()
208  return (width > screen_width) or (height > screen_height)
209  else:
210  return False
211 
212  def _get_scaled_down_geo(self, width, height):
213  divisor = 1
214  while self._geo_larger_than_display(width / divisor, height / divisor):
215  divisor = divisor * 2
216  return divisor
217 
218  def _patch_environment(self, key, value):
219  """Wrapper for patching env for upstart environment."""
220  try:
221  current_value = subprocess.check_output(
222  ["/sbin/initctl", "get-env", "--global", key],
223  stderr=subprocess.STDOUT,
224  universal_newlines=True,
225  ).rstrip()
226  except subprocess.CalledProcessError:
227  current_value = None
228 
229  subprocess.call([
230  "/sbin/initctl",
231  "set-env",
232  "--global",
233  "%s=%s" % (key, value)
234  ], stderr=subprocess.STDOUT)
235  self.addCleanup(self._upstart_reset_env, key, current_value)
236 
237  def _upstart_reset_env(self, key, value):
238  logger.info("Resetting upstart env %s to %s", key, value)
239  if value is None:
240  subprocess.call(
241  ["/sbin/initctl", "unset-env", key],
242  stderr=subprocess.STDOUT,
243  )
244  else:
245  subprocess.call([
246  "/sbin/initctl",
247  "set-env",
248  "--global",
249  "%s=%s" % (key, value)
250  ], stderr=subprocess.STDOUT)
251 
252  def launch_unity(self, **kwargs):
253  """Launch the unity shell, return a proxy object for it."""
254  binary_path = get_binary_path()
255  lib_path = get_lib_path()
256 
257  logger.info(
258  "Lib path is '%s', binary path is '%s'",
259  lib_path,
260  binary_path
261  )
262 
263  if self._lightdm_mock_type is None:
264  self.patch_lightdm_mock()
265 
266  if self._qml_mock_enabled:
268 
269  if self._data_dirs_mock_enabled:
270  self._patch_data_dirs()
271 
272  # FIXME: we shouldn't be doing this
273  # $MIR_SOCKET, fallback to $XDG_RUNTIME_DIR/mir_socket and
274  # /tmp/mir_socket as last resort
275  try:
276  os.unlink(
277  os.getenv('MIR_SOCKET',
278  os.path.join(os.getenv('XDG_RUNTIME_DIR', "/tmp"),
279  "mir_socket")))
280  except OSError:
281  pass
282  try:
283  os.unlink("/tmp/mir_socket")
284  except OSError:
285  pass
286 
287  app_proxy = self._launch_unity_with_upstart(
288  binary_path,
289  self.unity_geometry_args,
290  )
291 
292  self._set_proxy(app_proxy)
293 
294  # Ensure that the dash is visible before we return:
295  logger.debug("Unity started, waiting for it to be ready.")
296  self.assertUnityReady()
297  logger.debug("Unity loaded and ready.")
298 
299  return app_proxy
300 
301  def _launch_unity_with_upstart(self, binary_path, args):
302  logger.info("Starting unity")
303  self._patch_environment("QT_LOAD_TESTABILITY", 1)
304 
305  binary_arg = "BINARY=%s" % binary_path
306  extra_args = "ARGS=%s" % " ".join(args)
307  env_args = ["%s=%s" % (k, v) for k, v in self._environment.items()]
308  all_args = [binary_arg, extra_args] + env_args
309 
310  self.addCleanup(self._cleanup_launching_upstart_unity)
311 
312  return restart_unity_with_testability(*all_args)
313 
314  def _cleanup_launching_upstart_unity(self):
315  logger.info("Stopping unity")
316  try:
317  subprocess.check_output(
318  ["/sbin/initctl", "stop", "unity8"],
319  stderr=subprocess.STDOUT
320  )
321  except subprocess.CalledProcessError:
322  logger.warning("Appears unity was already stopped!")
323 
324  def _patch_data_dirs(self):
325  data_dirs = get_data_dirs(self._data_dirs_mock_enabled)
326  if data_dirs is not None:
327  self._environment['XDG_DATA_DIRS'] = data_dirs
328 
329  def patch_lightdm_mock(self, mock_type='single'):
330  self._lightdm_mock_type = mock_type
331  logger.info("Setting up LightDM mock type '%s'", mock_type)
332  new_ld_library_path = [
333  get_default_extra_mock_libraries(),
334  self._get_lightdm_mock_path(mock_type)
335  ]
336  if os.getenv('LD_LIBRARY_PATH') is not None:
337  new_ld_library_path.append(os.getenv('LD_LIBRARY_PATH'))
338 
339  new_ld_library_path = ':'.join(new_ld_library_path)
340  logger.info("New library path: %s", new_ld_library_path)
341 
342  self._environment['LD_LIBRARY_PATH'] = new_ld_library_path
343 
344  def _get_lightdm_mock_path(self, mock_type):
345  lib_path = get_mocks_library_path()
346  lightdm_mock_path = os.path.abspath(
347  os.path.join(lib_path, "LightDM", mock_type)
348  )
349 
350  if not os.path.exists(lightdm_mock_path):
351  raise RuntimeError(
352  "LightDM mock '%s' does not exist at path '%s'."
353  % (mock_type, lightdm_mock_path)
354  )
355  return lightdm_mock_path
356 
357  def _setup_extra_mock_environment_patch(self):
358  qml_import_path = [get_mocks_library_path()]
359  if os.getenv('QML2_IMPORT_PATH') is not None:
360  qml_import_path.append(os.getenv('QML2_IMPORT_PATH'))
361 
362  qml_import_path = ':'.join(qml_import_path)
363  logger.info("New QML2 import path: %s", qml_import_path)
364  self._environment['QML2_IMPORT_PATH'] = qml_import_path
365 
366  def _set_proxy(self, proxy):
367  """Keep a copy of the proxy object, so we can use it to get common
368  parts of the shell later on.
369 
370  """
371  self._proxy = proxy
372  self.addCleanup(self._clear_proxy)
373 
374  def _clear_proxy(self):
375  self._proxy = None
376 
377  def assertUnityReady(self):
378  dash = self.get_dash()
379  home_scope = dash.get_scope('clickscope')
380 
381  # FIXME! There is a huge timeout here for when we're doing CI on
382  # VMs. See lp:1203715
383  self.assertThat(
384  home_scope.isLoaded,
385  Eventually(Equals(True), timeout=60)
386  )
387  self.assertThat(home_scope.isCurrent, Eventually(Equals(True)))
388 
389  def get_dash(self):
390  dash = self._proxy.wait_select_single(Dash)
391  return dash
392 
393  @property
394  def main_window(self):
395  return self._proxy.select_single(main_window_emulator.QQuickView)