Unity 8
__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, 2015 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 import introspection
28 from autopilot.platform import model
29 from autopilot.testcase import AutopilotTestCase
30 from autopilot.matchers import Eventually
31 from autopilot.input import Touch
32 from autopilot.display import Display
33 import logging
34 import os.path
35 import subprocess
36 import sys
37 from testtools.matchers import Equals
38 from ubuntuuitoolkit import (
39  fixture_setup as toolkit_fixtures,
40  ubuntu_scenarios
41 )
42 
43 from unity8 import (
44  get_lib_path,
45  get_binary_path,
46  get_mocks_library_path,
47  get_default_extra_mock_libraries,
48  get_data_dirs
49 )
50 from unity8 import (
51  fixture_setup,
52  process_helpers
53 )
54 from unity8.shell import emulators
55 from unity8.shell.emulators import (
56  dash as dash_helpers,
57  main_window as main_window_emulator,
58 )
59 
60 
61 logger = logging.getLogger(__name__)
62 
63 UNITYSHELL_GSETTINGS_SCHEMA = "org.compiz.unityshell"
64 UNITYSHELL_GSETTINGS_PATH = "/org/compiz/profiles/unity/plugins/unityshell/"
65 UNITYSHELL_LAUNCHER_KEY = "launcher-hide-mode"
66 UNITYSHELL_LAUNCHER_MODE = 1 # launcher hidden
67 
68 def _get_device_emulation_scenarios(devices='All'):
69  nexus4 = ('Desktop Nexus 4',
70  dict(app_width=768, app_height=1280, grid_unit_px=18))
71  nexus10 = ('Desktop Nexus 10',
72  dict(app_width=2560, app_height=1600, grid_unit_px=20))
73  native = ('Native Device',
74  dict(app_width=0, app_height=0, grid_unit_px=0))
75 
76  if model() == 'Desktop':
77  if devices == 'All':
78  return [nexus4, nexus10]
79  elif devices == 'Nexus4':
80  return [nexus4]
81  elif devices == 'Nexus10':
82  return [nexus10]
83  else:
84  raise RuntimeError(
85  'Unrecognized device-option "%s" passed.' % devices
86  )
87  else:
88  return [native]
89 
90 
91 def is_unity7_running():
92  """Return True if Unity7 is running. Otherwise, return False."""
93  return (
94  Gio is not None and
95  UNITYSHELL_GSETTINGS_SCHEMA in
96  Gio.Settings.list_relocatable_schemas()
97  )
98 
99 
100 def get_qml_import_path_with_mock():
101  """Return the QML2_IMPORT_PATH value with the mock path prepended."""
102  qml_import_path = [get_mocks_library_path()]
103  if os.getenv('QML2_IMPORT_PATH') is not None:
104  qml_import_path.append(os.getenv('QML2_IMPORT_PATH'))
105 
106  qml_import_path = ':'.join(qml_import_path)
107  logger.info("New QML2 import path: %s", qml_import_path)
108  return qml_import_path
109 
110 
111 class UnityTestCase(AutopilotTestCase):
112 
113  """A test case base class for the Unity shell tests."""
114 
115  @classmethod
116  def setUpClass(cls):
117  try:
118  output = subprocess.check_output(
119  ["/sbin/initctl", "status", "unity8"],
120  stderr=subprocess.STDOUT,
121  universal_newlines=True,
122  )
123  except subprocess.CalledProcessError as e:
124  sys.stderr.write(
125  "Error: `initctl status unity8` failed, most probably the "
126  "unity8 session could not be found:\n\n"
127  "{0}\n"
128  "Please install unity8 or copy data/unity8.conf to "
129  "{1}\n".format(
130  e.output,
131  os.path.join(os.getenv("XDG_CONFIG_HOME",
132  os.path.join(os.getenv("HOME"),
133  ".config")
134  ),
135  "upstart")
136  )
137  )
138  sys.exit(1)
139 
140  if "start/" in output:
141  sys.stderr.write(
142  "Error: Unity is currently running, these tests require it to "
143  "be 'stopped'.\n"
144  "Please run this command before running these tests: \n"
145  "initctl stop unity8\n"
146  )
147  sys.exit(2)
148 
149  def setUp(self):
150  super().setUp()
151  if is_unity7_running():
152  self.useFixture(toolkit_fixtures.HideUnity7Launcher())
153 
154  self._proxy = None
155  self._qml_mock_enabled = True
156  self._data_dirs_mock_enabled = True
157  self._environment = {}
158 
159  # FIXME: This is a work around re: lp:1238417
160  if model() != "Desktop":
161  from autopilot.input import _uinput
162  _uinput._touch_device = _uinput.create_touch_device()
163  self.addCleanup(_uinput._touch_device.close)
164 
165  self.touch = Touch.create()
167 
168  def _setup_display_details(self):
169  scale_divisor = self._determine_geometry()
170  self._setup_grid_size(scale_divisor)
171 
172  def _determine_geometry(self):
173  """Use the geometry that may be supplied or use the default."""
174  width = getattr(self, 'app_width', 0)
175  height = getattr(self, 'app_height', 0)
176  scale_divisor = 1
177  self.unity_geometry_args = []
178  if width > 0 and height > 0:
179  if self._geo_larger_than_display(width, height):
180  scale_divisor = self._get_scaled_down_geo(width, height)
181  width = width / scale_divisor
182  height = height / scale_divisor
183  logger.info(
184  "Geometry larger than display, scaled down to: %dx%d",
185  width,
186  height
187  )
188  geo_string = "%dx%d" % (width, height)
189  self.unity_geometry_args = [
190  '-windowgeometry',
191  geo_string,
192  '-frameless',
193  '-mousetouch'
194  ]
195  return scale_divisor
196 
197  def _setup_grid_size(self, scale_divisor):
198  """Use the grid size that may be supplied or use the default."""
199  if getattr(self, 'grid_unit_px', 0) == 0:
200  self.grid_size = int(os.getenv('GRID_UNIT_PX'))
201  else:
202  self.grid_size = int(self.grid_unit_px / scale_divisor)
203  self._environment["GRID_UNIT_PX"] = str(self.grid_size)
204 
205  def _geo_larger_than_display(self, width, height):
206  should_scale = getattr(self, 'scale_geo', True)
207  if should_scale:
208  screen = Display.create()
209  screen_width = screen.get_screen_width()
210  screen_height = screen.get_screen_height()
211  return (width > screen_width) or (height > screen_height)
212  else:
213  return False
214 
215  def _get_scaled_down_geo(self, width, height):
216  divisor = 1
217  while self._geo_larger_than_display(width / divisor, height / divisor):
218  divisor = divisor * 2
219  return divisor
220 
221  def _patch_environment(self, key, value):
222  """Wrapper for patching env for upstart environment."""
223  try:
224  current_value = subprocess.check_output(
225  ["/sbin/initctl", "get-env", "--global", key],
226  stderr=subprocess.STDOUT,
227  universal_newlines=True,
228  ).rstrip()
229  except subprocess.CalledProcessError:
230  current_value = None
231 
232  subprocess.call([
233  "/sbin/initctl",
234  "set-env",
235  "--global",
236  "%s=%s" % (key, value)
237  ], stderr=subprocess.STDOUT)
238  self.addCleanup(self._upstart_reset_env, key, current_value)
239 
240  def _upstart_reset_env(self, key, value):
241  logger.info("Resetting upstart env %s to %s", key, value)
242  if value is None:
243  subprocess.call(
244  ["/sbin/initctl", "unset-env", key],
245  stderr=subprocess.STDOUT,
246  )
247  else:
248  subprocess.call([
249  "/sbin/initctl",
250  "set-env",
251  "--global",
252  "%s=%s" % (key, value)
253  ], stderr=subprocess.STDOUT)
254 
255  def launch_unity(self, mode="full-greeter", *args):
256  """
257  Launch the unity shell, return a proxy object for it.
258 
259  :param str mode: The type of greeter/shell mode to use
260  :param args: A list of aguments to pass to unity8
261 
262  """
263  binary_path = get_binary_path()
264  lib_path = get_lib_path()
265 
266  logger.info(
267  "Lib path is '%s', binary path is '%s'",
268  lib_path,
269  binary_path
270  )
271 
272  self.patch_lightdm_mock()
273 
274  if self._qml_mock_enabled:
275  self._environment['QML2_IMPORT_PATH'] = (
276  get_qml_import_path_with_mock()
277  )
278 
279  if self._data_dirs_mock_enabled:
280  self._patch_data_dirs()
281 
282  # FIXME: we shouldn't be doing this
283  # $MIR_SOCKET, fallback to $XDG_RUNTIME_DIR/mir_socket and
284  # /tmp/mir_socket as last resort
285  try:
286  os.unlink(
287  os.getenv('MIR_SOCKET',
288  os.path.join(os.getenv('XDG_RUNTIME_DIR', "/tmp"),
289  "mir_socket")))
290  except OSError:
291  pass
292  try:
293  os.unlink("/tmp/mir_socket")
294  except OSError:
295  pass
296 
297  unity8_cli_args_list = ["--mode={}".format(mode)]
298  if len(args) != 0:
299  unity8_cli_args_list += args
300 
301  app_proxy = self._launch_unity_with_upstart(
302  binary_path,
303  self.unity_geometry_args + unity8_cli_args_list
304  )
305 
306  self._set_proxy(app_proxy)
307 
308  # Ensure that the dash is visible before we return:
309  logger.debug("Unity started, waiting for it to be ready.")
310  self.wait_for_unity()
311  logger.debug("Unity loaded and ready.")
312 
313  if model() == 'Desktop':
314  # On desktop, close the dash because it's opened in a separate
315  # window and it gets in the way.
316  process_helpers.stop_job('unity8-dash')
317 
318  return app_proxy
319 
320  def _launch_unity_with_upstart(self, binary_path, args):
321  logger.info("Starting unity")
322  self._patch_environment("QT_LOAD_TESTABILITY", 1)
323 
324  binary_arg = "BINARY=%s" % binary_path
325  extra_args = "ARGS=%s" % " ".join(args)
326  env_args = ["%s=%s" % (k, v) for k, v in self._environment.items()]
327  all_args = [binary_arg, extra_args] + env_args
328 
329  self.addCleanup(self._cleanup_launching_upstart_unity)
330 
331  return process_helpers.restart_unity_with_testability(*all_args)
332 
333  def _cleanup_launching_upstart_unity(self):
334  logger.info("Stopping unity")
335  try:
336  subprocess.check_output(
337  ["/sbin/initctl", "stop", "unity8"],
338  stderr=subprocess.STDOUT
339  )
340  except subprocess.CalledProcessError:
341  logger.warning("Appears unity was already stopped!")
342 
343  def _patch_data_dirs(self):
344  data_dirs = get_data_dirs(self._data_dirs_mock_enabled)
345  if data_dirs is not None:
346  self._environment['XDG_DATA_DIRS'] = data_dirs
347 
348  def patch_lightdm_mock(self):
349  logger.info("Setting up LightDM mock lib")
350  new_ld_library_path = [
351  get_default_extra_mock_libraries(),
353  ]
354  if os.getenv('LD_LIBRARY_PATH') is not None:
355  new_ld_library_path.append(os.getenv('LD_LIBRARY_PATH'))
356 
357  new_ld_library_path = ':'.join(new_ld_library_path)
358  logger.info("New library path: %s", new_ld_library_path)
359 
360  self._environment['LD_LIBRARY_PATH'] = new_ld_library_path
361 
362  def _get_lightdm_mock_path(self):
363  lib_path = get_mocks_library_path()
364  lightdm_mock_path = os.path.abspath(
365  os.path.join(lib_path, "LightDM", "liblightdm")
366  )
367 
368  if not os.path.exists(lightdm_mock_path):
369  raise RuntimeError(
370  "LightDM mock does not exist at path '%s'."
371  % (lightdm_mock_path)
372  )
373  return lightdm_mock_path
374 
375  def _set_proxy(self, proxy):
376  """Keep a copy of the proxy object, so we can use it to get common
377  parts of the shell later on.
378 
379  """
380  self._proxy = proxy
381  self.addCleanup(self._clear_proxy)
382 
383  def _clear_proxy(self):
384  self._proxy = None
385 
386  def wait_for_unity(self):
387  greeter = self.main_window.wait_select_single(objectName='greeter')
388  greeter.waiting.wait_for(False)
389 
390  def get_dash(self):
391  pid = process_helpers.get_job_pid('unity8-dash')
392  dash_proxy = introspection.get_proxy_object_for_existing_process(
393  pid=pid,
394  emulator_base=emulators.UnityEmulatorBase,
395  )
396  dash_app = dash_helpers.DashApp(dash_proxy)
397  return dash_app.dash
398 
399  @property
400  def main_window(self):
401  return self._proxy.select_single(main_window_emulator.QQuickView)
402 
403 
404 class DashBaseTestCase(AutopilotTestCase):
405 
406  scenarios = ubuntu_scenarios.get_device_simulation_scenarios()
407  qml_mock_enabled = True
408  environment = {}
409 
410  def setUp(self):
411  super().setUp()
412 
413  if is_unity7_running():
414  self.useFixture(toolkit_fixtures.HideUnity7Launcher())
415 
416  if model() != 'Desktop':
417  # On the phone, we need unity to be running and unlocked.
418  self.addCleanup(process_helpers.stop_job, 'unity8')
419  process_helpers.restart_unity_with_testability()
420  process_helpers.unlock_unity()
421 
422  self.ensure_dash_not_running()
423 
424  if self.qml_mock_enabled:
425  self.environment['QML2_IMPORT_PATH'] = (
426  get_qml_import_path_with_mock()
427  )
428 
429  if self.should_simulate_device():
430  # This sets the grid units, so it should be called before launching
431  # the app.
432  self.simulate_device()
433 
434  binary_path = get_binary_path('unity8-dash')
435  dash_proxy = self.launch_dash(binary_path, self.environment)
436 
437  self.dash_app = dash_helpers.DashApp(dash_proxy)
438  self.dash = self.dash_app.dash
439  self.wait_for_dash()
440 
441  def ensure_dash_not_running(self):
442  if process_helpers.is_job_running('unity8-dash'):
443  process_helpers.stop_job('unity8-dash')
444 
445  def launch_dash(self, binary_path, variables):
446  launch_dash_app_fixture = fixture_setup.LaunchDashApp(
447  binary_path, variables)
448  self.useFixture(launch_dash_app_fixture)
449  return launch_dash_app_fixture.application_proxy
450 
451  def wait_for_dash(self):
452  home_scope = self.dash.get_scope_by_index(0)
453  # FIXME! There is a huge timeout here for when we're doing CI on
454  # VMs. See lp:1203715
455  self.assertThat(
456  home_scope.isLoaded,
457  Eventually(Equals(True), timeout=60)
458  )
459  self.assertThat(home_scope.isCurrent, Eventually(Equals(True)))
460 
461  def should_simulate_device(self):
462  return (hasattr(self, 'app_width') and hasattr(self, 'app_height') and
463  hasattr(self, 'grid_unit_px'))
464 
465  def simulate_device(self):
466  simulate_device_fixture = self.useFixture(
467  toolkit_fixtures.SimulateDevice(
468  self.app_width, self.app_height, self.grid_unit_px))
469  self.environment['GRID_UNIT_PX'] = simulate_device_fixture.grid_unit_px
470  self.environment['ARGS'] = '-windowgeometry {0}x{1}'\
471  .format(simulate_device_fixture.app_width,
472  simulate_device_fixture.app_height)
def launch_unity(self, mode="full-greeter", args)
Definition: __init__.py:255
def _set_proxy(self, proxy)
Definition: __init__.py:375
def _geo_larger_than_display(self, width, height)
Definition: __init__.py:205
def _cleanup_launching_upstart_unity(self)
Definition: __init__.py:333
def _setup_grid_size(self, scale_divisor)
Definition: __init__.py:197
def _patch_environment(self, key, value)
Definition: __init__.py:221
def _upstart_reset_env(self, key, value)
Definition: __init__.py:240
def _launch_unity_with_upstart(self, binary_path, args)
Definition: __init__.py:320
def _get_scaled_down_geo(self, width, height)
Definition: __init__.py:215