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  self.touch = Touch.create()
153 
154  def _reset_launcher(self):
155  """Reset Unity launcher hide mode"""
156  self._unityshell_schema.set_int(
157  UNITYSHELL_LAUNCHER_KEY,
158  self._launcher_hide_mode,
159  )
160 
161  def _setup_display_details(self):
162  scale_divisor = self._determine_geometry()
163  self._setup_grid_size(scale_divisor)
164 
165  def _determine_geometry(self):
166  """Use the geometry that may be supplied or use the default."""
167  width = getattr(self, 'app_width', 0)
168  height = getattr(self, 'app_height', 0)
169  scale_divisor = 1
170  self.unity_geometry_args = []
171  if width > 0 and height > 0:
172  if self._geo_larger_than_display(width, height):
173  scale_divisor = self._get_scaled_down_geo(width, height)
174  width = width / scale_divisor
175  height = height / scale_divisor
176  logger.info(
177  "Geometry larger than display, scaled down to: %dx%d",
178  width,
179  height
180  )
181  geo_string = "%dx%d" % (width, height)
182  self.unity_geometry_args = [
183  '-windowgeometry',
184  geo_string,
185  '-frameless',
186  '-mousetouch'
187  ]
188  return scale_divisor
189 
190  def _setup_grid_size(self, scale_divisor):
191  """Use the grid size that may be supplied or use the default."""
192  if getattr(self, 'grid_unit_px', 0) == 0:
193  self.grid_size = int(os.getenv('GRID_UNIT_PX'))
194  else:
195  self.grid_size = int(self.grid_unit_px / scale_divisor)
196  self._environment["GRID_UNIT_PX"] = str(self.grid_size)
197  # FIXME this is only needed for Hud.get_close_button_coords
198  # we should probably rework it so that it's not required
199  self.patch_environment("GRID_UNIT_PX", str(self.grid_size))
200 
201  def _geo_larger_than_display(self, width, height):
202  should_scale = getattr(self, 'scale_geo', True)
203  if should_scale:
204  screen = Display.create()
205  screen_width = screen.get_screen_width()
206  screen_height = screen.get_screen_height()
207  return (width > screen_width) or (height > screen_height)
208  else:
209  return False
210 
211  def _get_scaled_down_geo(self, width, height):
212  divisor = 1
213  while self._geo_larger_than_display(width / divisor, height / divisor):
214  divisor = divisor * 2
215  return divisor
216 
217  def _patch_environment(self, key, value):
218  """Wrapper for patching env for upstart environment."""
219  try:
220  current_value = subprocess.check_output(
221  ["/sbin/initctl", "get-env", "--global", key],
222  stderr=subprocess.STDOUT,
223  universal_newlines=True,
224  ).rstrip()
225  except subprocess.CalledProcessError:
226  current_value = None
227 
228  subprocess.call([
229  "/sbin/initctl",
230  "set-env",
231  "--global",
232  "%s=%s" % (key, value)
233  ], stderr=subprocess.STDOUT)
234  self.addCleanup(self._upstart_reset_env, key, current_value)
235 
236  def _upstart_reset_env(self, key, value):
237  logger.info("Resetting upstart env %s to %s", key, value)
238  if value is None:
239  subprocess.call(
240  ["/sbin/initctl", "unset-env", key],
241  stderr=subprocess.STDOUT,
242  )
243  else:
244  subprocess.call([
245  "/sbin/initctl",
246  "set-env",
247  "--global",
248  "%s=%s" % (key, value)
249  ], stderr=subprocess.STDOUT)
250 
251  def launch_unity(self, **kwargs):
252  """Launch the unity shell, return a proxy object for it."""
253  binary_path = get_binary_path()
254  lib_path = get_lib_path()
255 
256  logger.info(
257  "Lib path is '%s', binary path is '%s'",
258  lib_path,
259  binary_path
260  )
261 
262  if self._lightdm_mock_type is None:
263  self.patch_lightdm_mock()
264 
265  if self._qml_mock_enabled:
267 
268  if self._data_dirs_mock_enabled:
269  self._patch_data_dirs()
270 
271  # FIXME: we shouldn't be doing this
272  # $MIR_SOCKET, fallback to $XDG_RUNTIME_DIR/mir_socket and
273  # /tmp/mir_socket as last resort
274  try:
275  os.unlink(
276  os.getenv('MIR_SOCKET',
277  os.path.join(os.getenv('XDG_RUNTIME_DIR', "/tmp"),
278  "mir_socket")))
279  except OSError:
280  pass
281  try:
282  os.unlink("/tmp/mir_socket")
283  except OSError:
284  pass
285 
286  app_proxy = self._launch_unity_with_upstart(
287  binary_path,
288  self.unity_geometry_args,
289  )
290 
291  self._set_proxy(app_proxy)
292 
293  # Ensure that the dash is visible before we return:
294  logger.debug("Unity started, waiting for it to be ready.")
295  self.assertUnityReady()
296  logger.debug("Unity loaded and ready.")
297 
298  return app_proxy
299 
300  def _launch_unity_with_upstart(self, binary_path, args):
301  logger.info("Starting unity")
302  self._patch_environment("QT_LOAD_TESTABILITY", 1)
303 
304  binary_arg = "BINARY=%s" % binary_path
305  extra_args = "ARGS=%s" % " ".join(args)
306  env_args = ["%s=%s" % (k, v) for k, v in self._environment.items()]
307  all_args = [binary_arg, extra_args] + env_args
308 
309  self.addCleanup(self._cleanup_launching_upstart_unity)
310 
311  return restart_unity_with_testability(*all_args)
312 
313  def _cleanup_launching_upstart_unity(self):
314  logger.info("Stopping unity")
315  try:
316  subprocess.check_output(
317  ["/sbin/initctl", "stop", "unity8"],
318  stderr=subprocess.STDOUT
319  )
320  except subprocess.CalledProcessError:
321  logger.warning("Appears unity was already stopped!")
322 
323  def _patch_data_dirs(self):
324  data_dirs = get_data_dirs(self._data_dirs_mock_enabled)
325  if data_dirs is not None:
326  self._environment['XDG_DATA_DIRS'] = data_dirs
327 
328  def patch_lightdm_mock(self, mock_type='single'):
329  self._lightdm_mock_type = mock_type
330  logger.info("Setting up LightDM mock type '%s'", mock_type)
331  new_ld_library_path = [
332  get_default_extra_mock_libraries(),
333  self._get_lightdm_mock_path(mock_type)
334  ]
335  if os.getenv('LD_LIBRARY_PATH') is not None:
336  new_ld_library_path.append(os.getenv('LD_LIBRARY_PATH'))
337 
338  new_ld_library_path = ':'.join(new_ld_library_path)
339  logger.info("New library path: %s", new_ld_library_path)
340 
341  self._environment['LD_LIBRARY_PATH'] = new_ld_library_path
342 
343  def _get_lightdm_mock_path(self, mock_type):
344  lib_path = get_mocks_library_path()
345  lightdm_mock_path = os.path.abspath(
346  os.path.join(lib_path, "LightDM", mock_type)
347  )
348 
349  if not os.path.exists(lightdm_mock_path):
350  raise RuntimeError(
351  "LightDM mock '%s' does not exist at path '%s'."
352  % (mock_type, lightdm_mock_path)
353  )
354  return lightdm_mock_path
355 
356  def _setup_extra_mock_environment_patch(self):
357  qml_import_path = [get_mocks_library_path()]
358  if os.getenv('QML2_IMPORT_PATH') is not None:
359  qml_import_path.append(os.getenv('QML2_IMPORT_PATH'))
360 
361  qml_import_path = ':'.join(qml_import_path)
362  logger.info("New QML2 import path: %s", qml_import_path)
363  self._environment['QML2_IMPORT_PATH'] = qml_import_path
364 
365  def _set_proxy(self, proxy):
366  """Keep a copy of the proxy object, so we can use it to get common
367  parts of the shell later on.
368 
369  """
370  self._proxy = proxy
371  self.addCleanup(self._clear_proxy)
372 
373  def _clear_proxy(self):
374  self._proxy = None
375 
376  def assertUnityReady(self):
377  dash = self.get_dash()
378  home_scope = dash.get_scope('clickscope')
379 
380  # FIXME! There is a huge timeout here for when we're doing CI on
381  # VMs. See lp:1203715
382  self.assertThat(
383  home_scope.isLoaded,
384  Eventually(Equals(True), timeout=60)
385  )
386  self.assertThat(home_scope.isCurrent, Eventually(Equals(True)))
387 
388  def get_dash(self):
389  dash = self._proxy.wait_select_single(Dash)
390  return dash
391 
392  @property
393  def main_window(self):
394  return self._proxy.select_single(main_window_emulator.QQuickView)