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 
207  def _geo_larger_than_display(self, width, height):
208  should_scale = getattr(self, 'scale_geo', True)
209  if should_scale:
210  screen = Display.create()
211  screen_width = screen.get_screen_width()
212  screen_height = screen.get_screen_height()
213  return (width > screen_width) or (height > screen_height)
214  else:
215  return False
216 
217  def _get_scaled_down_geo(self, width, height):
218  divisor = 1
219  while self._geo_larger_than_display(width / divisor, height / divisor):
220  divisor = divisor * 2
221  return divisor
222 
223  def _patch_environment(self, key, value):
224  """Wrapper for patching env for upstart environment."""
225  try:
226  current_value = subprocess.check_output(
227  ["/sbin/initctl", "get-env", "--global", key],
228  stderr=subprocess.STDOUT,
229  universal_newlines=True,
230  ).rstrip()
231  except subprocess.CalledProcessError:
232  current_value = None
233 
234  subprocess.call([
235  "/sbin/initctl",
236  "set-env",
237  "--global",
238  "%s=%s" % (key, value)
239  ], stderr=subprocess.STDOUT)
240  self.addCleanup(self._upstart_reset_env, key, current_value)
241 
242  def _upstart_reset_env(self, key, value):
243  logger.info("Resetting upstart env %s to %s", key, value)
244  if value is None:
245  subprocess.call(
246  ["/sbin/initctl", "unset-env", key],
247  stderr=subprocess.STDOUT,
248  )
249  else:
250  subprocess.call([
251  "/sbin/initctl",
252  "set-env",
253  "--global",
254  "%s=%s" % (key, value)
255  ], stderr=subprocess.STDOUT)
256 
257  def launch_unity(self, **kwargs):
258  """Launch the unity shell, return a proxy object for it."""
259  binary_path = get_binary_path()
260  lib_path = get_lib_path()
261 
262  logger.info(
263  "Lib path is '%s', binary path is '%s'",
264  lib_path,
265  binary_path
266  )
267 
268  if self._lightdm_mock_type is None:
269  self.patch_lightdm_mock()
270 
271  if self._qml_mock_enabled:
272  self._environment['QML2_IMPORT_PATH'] = (
273  get_qml_import_path_with_mock()
274  )
275 
276  if self._data_dirs_mock_enabled:
277  self._patch_data_dirs()
278 
279  # FIXME: we shouldn't be doing this
280  # $MIR_SOCKET, fallback to $XDG_RUNTIME_DIR/mir_socket and
281  # /tmp/mir_socket as last resort
282  try:
283  os.unlink(
284  os.getenv('MIR_SOCKET',
285  os.path.join(os.getenv('XDG_RUNTIME_DIR', "/tmp"),
286  "mir_socket")))
287  except OSError:
288  pass
289  try:
290  os.unlink("/tmp/mir_socket")
291  except OSError:
292  pass
293 
294  app_proxy = self._launch_unity_with_upstart(
295  binary_path,
296  self.unity_geometry_args,
297  )
298 
299  self._set_proxy(app_proxy)
300 
301  # Ensure that the dash is visible before we return:
302  logger.debug("Unity started, waiting for it to be ready.")
303  self.wait_for_unity()
304  logger.debug("Unity loaded and ready.")
305 
306  if model() == 'Desktop':
307  # On desktop, close the dash because it's opened in a separate
308  # window and it gets in the way.
309  process_helpers.stop_job('unity8-dash')
310 
311  return app_proxy
312 
313  def _launch_unity_with_upstart(self, binary_path, args):
314  logger.info("Starting unity")
315  self._patch_environment("QT_LOAD_TESTABILITY", 1)
316 
317  binary_arg = "BINARY=%s" % binary_path
318  extra_args = "ARGS=%s" % " ".join(args)
319  env_args = ["%s=%s" % (k, v) for k, v in self._environment.items()]
320  all_args = [binary_arg, extra_args] + env_args
321 
322  self.addCleanup(self._cleanup_launching_upstart_unity)
323 
324  return process_helpers.restart_unity_with_testability(*all_args)
325 
326  def _cleanup_launching_upstart_unity(self):
327  logger.info("Stopping unity")
328  try:
329  subprocess.check_output(
330  ["/sbin/initctl", "stop", "unity8"],
331  stderr=subprocess.STDOUT
332  )
333  except subprocess.CalledProcessError:
334  logger.warning("Appears unity was already stopped!")
335 
336  def _patch_data_dirs(self):
337  data_dirs = get_data_dirs(self._data_dirs_mock_enabled)
338  if data_dirs is not None:
339  self._environment['XDG_DATA_DIRS'] = data_dirs
340 
341  def patch_lightdm_mock(self, mock_type='single'):
342  self._lightdm_mock_type = mock_type
343  logger.info("Setting up LightDM mock type '%s'", mock_type)
344  new_ld_library_path = [
345  get_default_extra_mock_libraries(),
346  self._get_lightdm_mock_path(mock_type)
347  ]
348  if os.getenv('LD_LIBRARY_PATH') is not None:
349  new_ld_library_path.append(os.getenv('LD_LIBRARY_PATH'))
350 
351  new_ld_library_path = ':'.join(new_ld_library_path)
352  logger.info("New library path: %s", new_ld_library_path)
353 
354  self._environment['LD_LIBRARY_PATH'] = new_ld_library_path
355 
356  def _get_lightdm_mock_path(self, mock_type):
357  lib_path = get_mocks_library_path()
358  lightdm_mock_path = os.path.abspath(
359  os.path.join(lib_path, "LightDM", mock_type)
360  )
361 
362  if not os.path.exists(lightdm_mock_path):
363  raise RuntimeError(
364  "LightDM mock '%s' does not exist at path '%s'."
365  % (mock_type, lightdm_mock_path)
366  )
367  return lightdm_mock_path
368 
369  def _set_proxy(self, proxy):
370  """Keep a copy of the proxy object, so we can use it to get common
371  parts of the shell later on.
372 
373  """
374  self._proxy = proxy
375  self.addCleanup(self._clear_proxy)
376 
377  def _clear_proxy(self):
378  self._proxy = None
379 
380  def wait_for_unity(self):
381  greeter_content_loader = self.main_window.wait_select_single(
382  objectName='greeterContentLoader')
383  greeter_content_loader.progress.wait_for(1)
384 
385  def get_dash(self):
386  pid = process_helpers.get_job_pid('unity8-dash')
387  dash_proxy = introspection.get_proxy_object_for_existing_process(
388  pid=pid,
389  emulator_base=emulators.UnityEmulatorBase,
390  )
391  dash_app = dash_helpers.DashApp(dash_proxy)
392  return dash_app.dash
393 
394  @property
395  def main_window(self):
396  return self._proxy.select_single(main_window_emulator.QQuickView)
397 
398 
399 class DashBaseTestCase(AutopilotTestCase):
400 
401  scenarios = ubuntu_scenarios.get_device_simulation_scenarios()
402  qml_mock_enabled = True
403  environment = {}
404 
405  def setUp(self):
406  super(DashBaseTestCase, self).setUp()
407 
408  if is_unity7_running():
409  self.useFixture(toolkit_fixtures.HideUnity7Launcher())
410 
411  if model() != 'Desktop':
412  # On the phone, we need unity to be running and unlocked.
413  self.addCleanup(process_helpers.stop_job, 'unity8')
414  process_helpers.restart_unity_with_testability()
415  process_helpers.unlock_unity()
416 
417  self.ensure_dash_not_running()
418 
419  if self.qml_mock_enabled:
420  self.environment['QML2_IMPORT_PATH'] = (
421  get_qml_import_path_with_mock()
422  )
423 
424  if self.should_simulate_device():
425  # This sets the grid units, so it should be called before launching
426  # the app.
427  self.simulate_device()
428 
429  binary_path = get_binary_path('unity8-dash')
430  dash_proxy = self.launch_dash(binary_path, self.environment)
431 
432  self.dash_app = dash_helpers.DashApp(dash_proxy)
433  self.dash = self.dash_app.dash
434  self.wait_for_dash()
435 
436  def ensure_dash_not_running(self):
437  if process_helpers.is_job_running('unity8-dash'):
438  process_helpers.stop_job('unity8-dash')
439 
440  def launch_dash(self, binary_path, variables):
441  launch_dash_app_fixture = fixture_setup.LaunchDashApp(
442  binary_path, variables)
443  self.useFixture(launch_dash_app_fixture)
444  return launch_dash_app_fixture.application_proxy
445 
446  def wait_for_dash(self):
447  home_scope = self.dash.get_scope_by_index(0)
448  # FIXME! There is a huge timeout here for when we're doing CI on
449  # VMs. See lp:1203715
450  self.assertThat(
451  home_scope.isLoaded,
452  Eventually(Equals(True), timeout=60)
453  )
454  self.assertThat(home_scope.isCurrent, Eventually(Equals(True)))
455 
456  def should_simulate_device(self):
457  return (hasattr(self, 'app_width') and hasattr(self, 'app_height') and
458  hasattr(self, 'grid_unit_px'))
459 
460  def simulate_device(self):
461  simulate_device_fixture = self.useFixture(
462  toolkit_fixtures.SimulateDevice(
463  self.app_width, self.app_height, self.grid_unit_px))
464  self.environment['GRID_UNIT_PX'] = simulate_device_fixture.grid_unit_px
465  self.environment['ARGS'] = '-windowgeometry {0}x{1}'\
466  .format(simulate_device_fixture.app_width,
467  simulate_device_fixture.app_height)