Unity 8
 All Classes Functions
__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 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(UnityTestCase, self).setUp()
152  if is_unity7_running():
153  self.useFixture(toolkit_fixtures.HideUnity7Launcher())
154 
155  self._proxy = None
156  self._lightdm_mock_type = None
157  self._qml_mock_enabled = True
158  self._data_dirs_mock_enabled = True
159  self._environment = {}
160 
161  # FIXME: This is a work around re: lp:1238417
162  if model() != "Desktop":
163  from autopilot.input import _uinput
164  _uinput._touch_device = _uinput.create_touch_device()
165  self.addCleanup(_uinput._touch_device.close)
166 
167  self.touch = Touch.create()
169 
170  def _setup_display_details(self):
171  scale_divisor = self._determine_geometry()
172  self._setup_grid_size(scale_divisor)
173 
174  def _determine_geometry(self):
175  """Use the geometry that may be supplied or use the default."""
176  width = getattr(self, 'app_width', 0)
177  height = getattr(self, 'app_height', 0)
178  scale_divisor = 1
179  self.unity_geometry_args = []
180  if width > 0 and height > 0:
181  if self._geo_larger_than_display(width, height):
182  scale_divisor = self._get_scaled_down_geo(width, height)
183  width = width / scale_divisor
184  height = height / scale_divisor
185  logger.info(
186  "Geometry larger than display, scaled down to: %dx%d",
187  width,
188  height
189  )
190  geo_string = "%dx%d" % (width, height)
191  self.unity_geometry_args = [
192  '-windowgeometry',
193  geo_string,
194  '-frameless',
195  '-mousetouch'
196  ]
197  return scale_divisor
198 
199  def _setup_grid_size(self, scale_divisor):
200  """Use the grid size that may be supplied or use the default."""
201  if getattr(self, 'grid_unit_px', 0) == 0:
202  self.grid_size = int(os.getenv('GRID_UNIT_PX'))
203  else:
204  self.grid_size = int(self.grid_unit_px / scale_divisor)
205  self._environment["GRID_UNIT_PX"] = str(self.grid_size)
206  # FIXME this is only needed for Hud.get_close_button_coords
207  # we should probably rework it so that it's not required
208  self.patch_environment("GRID_UNIT_PX", str(self.grid_size))
209 
210  def _geo_larger_than_display(self, width, height):
211  should_scale = getattr(self, 'scale_geo', True)
212  if should_scale:
213  screen = Display.create()
214  screen_width = screen.get_screen_width()
215  screen_height = screen.get_screen_height()
216  return (width > screen_width) or (height > screen_height)
217  else:
218  return False
219 
220  def _get_scaled_down_geo(self, width, height):
221  divisor = 1
222  while self._geo_larger_than_display(width / divisor, height / divisor):
223  divisor = divisor * 2
224  return divisor
225 
226  def _patch_environment(self, key, value):
227  """Wrapper for patching env for upstart environment."""
228  try:
229  current_value = subprocess.check_output(
230  ["/sbin/initctl", "get-env", "--global", key],
231  stderr=subprocess.STDOUT,
232  universal_newlines=True,
233  ).rstrip()
234  except subprocess.CalledProcessError:
235  current_value = None
236 
237  subprocess.call([
238  "/sbin/initctl",
239  "set-env",
240  "--global",
241  "%s=%s" % (key, value)
242  ], stderr=subprocess.STDOUT)
243  self.addCleanup(self._upstart_reset_env, key, current_value)
244 
245  def _upstart_reset_env(self, key, value):
246  logger.info("Resetting upstart env %s to %s", key, value)
247  if value is None:
248  subprocess.call(
249  ["/sbin/initctl", "unset-env", key],
250  stderr=subprocess.STDOUT,
251  )
252  else:
253  subprocess.call([
254  "/sbin/initctl",
255  "set-env",
256  "--global",
257  "%s=%s" % (key, value)
258  ], stderr=subprocess.STDOUT)
259 
260  def launch_unity(self, **kwargs):
261  """Launch the unity shell, return a proxy object for it."""
262  binary_path = get_binary_path()
263  lib_path = get_lib_path()
264 
265  logger.info(
266  "Lib path is '%s', binary path is '%s'",
267  lib_path,
268  binary_path
269  )
270 
271  if self._lightdm_mock_type is None:
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  app_proxy = self._launch_unity_with_upstart(
298  binary_path,
299  self.unity_geometry_args,
300  )
301 
302  self._set_proxy(app_proxy)
303 
304  # Ensure that the dash is visible before we return:
305  logger.debug("Unity started, waiting for it to be ready.")
306  self.wait_for_unity()
307  logger.debug("Unity loaded and ready.")
308 
309  if model() == 'Desktop':
310  # On desktop, close the dash because it's opened in a separate
311  # window and it gets in the way.
312  process_helpers.stop_job('unity8-dash')
313 
314  return app_proxy
315 
316  def _launch_unity_with_upstart(self, binary_path, args):
317  logger.info("Starting unity")
318  self._patch_environment("QT_LOAD_TESTABILITY", 1)
319 
320  binary_arg = "BINARY=%s" % binary_path
321  extra_args = "ARGS=%s" % " ".join(args)
322  env_args = ["%s=%s" % (k, v) for k, v in self._environment.items()]
323  all_args = [binary_arg, extra_args] + env_args
324 
325  self.addCleanup(self._cleanup_launching_upstart_unity)
326 
327  return process_helpers.restart_unity_with_testability(*all_args)
328 
329  def _cleanup_launching_upstart_unity(self):
330  logger.info("Stopping unity")
331  try:
332  subprocess.check_output(
333  ["/sbin/initctl", "stop", "unity8"],
334  stderr=subprocess.STDOUT
335  )
336  except subprocess.CalledProcessError:
337  logger.warning("Appears unity was already stopped!")
338 
339  def _patch_data_dirs(self):
340  data_dirs = get_data_dirs(self._data_dirs_mock_enabled)
341  if data_dirs is not None:
342  self._environment['XDG_DATA_DIRS'] = data_dirs
343 
344  def patch_lightdm_mock(self, mock_type='single'):
345  self._lightdm_mock_type = mock_type
346  logger.info("Setting up LightDM mock type '%s'", mock_type)
347  new_ld_library_path = [
348  get_default_extra_mock_libraries(),
349  self._get_lightdm_mock_path(mock_type)
350  ]
351  if os.getenv('LD_LIBRARY_PATH') is not None:
352  new_ld_library_path.append(os.getenv('LD_LIBRARY_PATH'))
353 
354  new_ld_library_path = ':'.join(new_ld_library_path)
355  logger.info("New library path: %s", new_ld_library_path)
356 
357  self._environment['LD_LIBRARY_PATH'] = new_ld_library_path
358 
359  def _get_lightdm_mock_path(self, mock_type):
360  lib_path = get_mocks_library_path()
361  lightdm_mock_path = os.path.abspath(
362  os.path.join(lib_path, "LightDM", mock_type)
363  )
364 
365  if not os.path.exists(lightdm_mock_path):
366  raise RuntimeError(
367  "LightDM mock '%s' does not exist at path '%s'."
368  % (mock_type, lightdm_mock_path)
369  )
370  return lightdm_mock_path
371 
372  def _set_proxy(self, proxy):
373  """Keep a copy of the proxy object, so we can use it to get common
374  parts of the shell later on.
375 
376  """
377  self._proxy = proxy
378  self.addCleanup(self._clear_proxy)
379 
380  def _clear_proxy(self):
381  self._proxy = None
382 
383  def wait_for_unity(self):
384  greeter_content_loader = self.main_window.wait_select_single(
385  objectName='greeterContentLoader')
386  greeter_content_loader.progress.wait_for(1)
387 
388  def get_dash(self):
389  pid = process_helpers.get_job_pid('unity8-dash')
390  dash_proxy = introspection.get_proxy_object_for_existing_process(
391  pid=pid,
392  emulator_base=emulators.UnityEmulatorBase,
393  )
394  dash_app = dash_helpers.DashApp(dash_proxy)
395  return dash_app.dash
396 
397  @property
398  def main_window(self):
399  return self._proxy.select_single(main_window_emulator.QQuickView)
400 
401 
402 class DashBaseTestCase(AutopilotTestCase):
403 
404  scenarios = ubuntu_scenarios.get_device_simulation_scenarios()
405  qml_mock_enabled = True
406  environment = {}
407 
408  def setUp(self):
409  super(DashBaseTestCase, self).setUp()
410 
411  if is_unity7_running():
412  self.useFixture(toolkit_fixtures.HideUnity7Launcher())
413 
414  if model() != 'Desktop':
415  # On the phone, we need unity to be running and unlocked.
416  self.addCleanup(process_helpers.stop_job, 'unity8')
417  process_helpers.restart_unity_with_testability()
418  process_helpers.unlock_unity()
419 
420  self.ensure_dash_not_running()
421 
422  if self.qml_mock_enabled:
423  self.environment['QML2_IMPORT_PATH'] = (
424  get_qml_import_path_with_mock()
425  )
426 
427  if self.should_simulate_device():
428  # This sets the grid units, so it should be called before launching
429  # the app.
430  self.simulate_device()
431 
432  binary_path = get_binary_path('unity8-dash')
433  dash_proxy = self.launch_dash(binary_path, self.environment)
434 
435  self.dash_app = dash_helpers.DashApp(dash_proxy)
436  self.dash = self.dash_app.dash
437  self.wait_for_dash()
438 
439  def ensure_dash_not_running(self):
440  if process_helpers.is_job_running('unity8-dash'):
441  process_helpers.stop_job('unity8-dash')
442 
443  def launch_dash(self, binary_path, variables):
444  launch_dash_app_fixture = fixture_setup.LaunchDashApp(
445  binary_path, variables)
446  self.useFixture(launch_dash_app_fixture)
447  return launch_dash_app_fixture.application_proxy
448 
449  def wait_for_dash(self):
450  home_scope = self.dash.get_scope('clickscope')
451  # FIXME! There is a huge timeout here for when we're doing CI on
452  # VMs. See lp:1203715
453  self.assertThat(
454  home_scope.isLoaded,
455  Eventually(Equals(True), timeout=60)
456  )
457  self.assertThat(home_scope.isCurrent, Eventually(Equals(True)))
458 
459  def should_simulate_device(self):
460  return (hasattr(self, 'app_width') and hasattr(self, 'app_height') and
461  hasattr(self, 'grid_unit_px'))
462 
463  def simulate_device(self):
464  simulate_device_fixture = self.useFixture(
465  toolkit_fixtures.SimulateDevice(
466  self.app_width, self.app_height, self.grid_unit_px))
467  self.environment['GRID_UNIT_PX'] = simulate_device_fixture.grid_unit_px
468  self.environment['ARGS'] = '-windowgeometry {0}x{1}'\
469  .format(simulate_device_fixture.app_width,
470  simulate_device_fixture.app_height)