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