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 import logging
28 import os
29 
30 from autopilot import introspection
31 from autopilot.platform import model
32 from autopilot.testcase import AutopilotTestCase
33 from autopilot.matchers import Eventually
34 from autopilot.display import Display
35 from testtools.matchers import Equals
36 
37 import ubuntuuitoolkit
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 import (
55  dash as dash_helpers,
56  shell
57 )
58 
59 
60 logger = logging.getLogger(__name__)
61 
62 UNITYSHELL_GSETTINGS_SCHEMA = "org.compiz.unityshell"
63 UNITYSHELL_GSETTINGS_PATH = "/org/compiz/profiles/unity/plugins/unityshell/"
64 UNITYSHELL_LAUNCHER_KEY = "launcher-hide-mode"
65 UNITYSHELL_LAUNCHER_MODE = 1 # launcher hidden
66 
67 
68 def is_unity7_running():
69  """Return True if Unity7 is running. Otherwise, return False."""
70  return (
71  Gio is not None and
72  UNITYSHELL_GSETTINGS_SCHEMA in
73  Gio.Settings.list_relocatable_schemas()
74  )
75 
76 
77 def get_qml_import_path_with_mock():
78  """Return the QML2_IMPORT_PATH value with the mock path prepended."""
79  qml_import_path = [get_mocks_library_path()]
80  if os.getenv('QML2_IMPORT_PATH') is not None:
81  qml_import_path.append(os.getenv('QML2_IMPORT_PATH'))
82 
83  qml_import_path = ':'.join(qml_import_path)
84  logger.info("New QML2 import path: %s", qml_import_path)
85  return qml_import_path
86 
87 
88 class UnityTestCase(AutopilotTestCase):
89 
90  """A test case base class for the Unity shell tests."""
91 
92  @classmethod
93  def setUpClass(cls):
94  try:
95  is_unity_running = process_helpers.is_job_running('unity8')
96  except process_helpers.JobError as e:
97  xdg_config_home = os.getenv(
98  'XDG_CONFIG_HOME', os.path.join(os.getenv('HOME'), '.config'))
99  upstart_config_path = os.path.join(xdg_config_home, 'upstart')
100  logger.error(
101  '`initctl status unity8` failed, most probably the '
102  'unity8 session could not be found:\n\n'
103  '{0}\n'
104  'Please install unity8 or copy data/unity8.conf to '
105  '{1}\n'.format(e.output, upstart_config_path)
106  )
107  raise e
108  else:
109  assert not is_unity_running, (
110  'Unity is currently running, these tests require it to be '
111  'stopped.\n'
112  'Please run this command before running these tests: \n'
113  'initctl stop unity8\n')
114 
115  def setUp(self):
116  super().setUp()
117  if is_unity7_running():
118  self.useFixture(toolkit_fixtures.HideUnity7Launcher())
119 
120  self._proxy = None
121  self._qml_mock_enabled = True
122  self._data_dirs_mock_enabled = True
123  self._environment = {}
124 
126 
127  def _setup_display_details(self):
128  scale_divisor = self._determine_geometry()
129  self._setup_grid_size(scale_divisor)
130 
131  def _determine_geometry(self):
132  """Use the geometry that may be supplied or use the default."""
133  width = getattr(self, 'app_width', 0)
134  height = getattr(self, 'app_height', 0)
135  scale_divisor = 1
136  self.unity_geometry_args = []
137  if width > 0 and height > 0:
138  if self._geo_larger_than_display(width, height):
139  scale_divisor = self._get_scaled_down_geo(width, height)
140  width = width / scale_divisor
141  height = height / scale_divisor
142  logger.info(
143  "Geometry larger than display, scaled down to: %dx%d",
144  width,
145  height
146  )
147  geo_string = "%dx%d" % (width, height)
148  self.unity_geometry_args = [
149  '-windowgeometry',
150  geo_string,
151  '-frameless',
152  '-mousetouch'
153  ]
154  return scale_divisor
155 
156  def _setup_grid_size(self, scale_divisor):
157  """Use the grid size that may be supplied or use the default."""
158  if getattr(self, 'grid_unit_px', 0) == 0:
159  if os.getenv('GRID_UNIT_PX') == None:
160  self.grid_size = 8
161  else:
162  self.grid_size = int(os.getenv('GRID_UNIT_PX'))
163  else:
164  self.grid_size = int(self.grid_unit_px / scale_divisor)
165  self._environment["GRID_UNIT_PX"] = str(self.grid_size)
166 
167  def _geo_larger_than_display(self, width, height):
168  should_scale = getattr(self, 'scale_geo', True)
169  if should_scale:
170  screen = Display.create()
171  screen_width = screen.get_screen_width()
172  screen_height = screen.get_screen_height()
173  return (width > screen_width) or (height > screen_height)
174  else:
175  return False
176 
177  def _get_scaled_down_geo(self, width, height):
178  divisor = 1
179  while self._geo_larger_than_display(width / divisor, height / divisor):
180  divisor = divisor * 2
181  return divisor
182 
183  def launch_unity(self, mode="full-greeter", *args):
184  """
185  Launch the unity shell, return a proxy object for it.
186 
187  :param str mode: The type of greeter/shell mode to use
188  :param args: A list of aguments to pass to unity8
189 
190  """
191  binary_path = get_binary_path()
192  lib_path = get_lib_path()
193 
194  logger.info(
195  "Lib path is '%s', binary path is '%s'",
196  lib_path,
197  binary_path
198  )
199 
200  self.patch_lightdm_mock()
201 
202  if self._qml_mock_enabled:
203  self._environment['QML2_IMPORT_PATH'] = (
204  get_qml_import_path_with_mock()
205  )
206 
207  if self._data_dirs_mock_enabled:
208  self._patch_data_dirs()
209 
210  unity8_cli_args_list = ["--mode={}".format(mode)]
211  if len(args) != 0:
212  unity8_cli_args_list += args
213 
214  app_proxy = self._launch_unity_with_upstart(
215  binary_path,
216  self.unity_geometry_args + unity8_cli_args_list
217  )
218 
219  self._set_proxy(app_proxy)
220 
221  # Ensure that the dash is visible before we return:
222  logger.debug("Unity started, waiting for it to be ready.")
223  self.wait_for_unity()
224  logger.debug("Unity loaded and ready.")
225 
226  if model() == 'Desktop':
227  # On desktop, close the dash because it's opened in a separate
228  # window and it gets in the way.
229  process_helpers.stop_job('unity8-dash')
230 
231  return app_proxy
232 
233  def _launch_unity_with_upstart(self, binary_path, args):
234  logger.info("Starting unity")
235  self.useFixture(toolkit_fixtures.InitctlEnvironmentVariable(
236  global_=True, QT_LOAD_TESTABILITY=1))
237 
238  variables = self._environment
239  variables['ARGS'] = " ".join(args)
240  launch_unity_fixture = fixture_setup.RestartUnityWithTestability(
241  binary_path, variables)
242  self.useFixture(launch_unity_fixture)
243  return launch_unity_fixture.unity_proxy
244 
245  def _patch_data_dirs(self):
246  data_dirs = get_data_dirs(self._data_dirs_mock_enabled)
247  if data_dirs is not None:
248  self._environment['XDG_DATA_DIRS'] = data_dirs
249 
250  def patch_lightdm_mock(self):
251  logger.info("Setting up LightDM mock lib")
252  new_ld_library_path = [
253  get_default_extra_mock_libraries(),
255  ]
256  if os.getenv('LD_LIBRARY_PATH') is not None:
257  new_ld_library_path.append(os.getenv('LD_LIBRARY_PATH'))
258 
259  new_ld_library_path = ':'.join(new_ld_library_path)
260  logger.info("New library path: %s", new_ld_library_path)
261 
262  self._environment['LD_LIBRARY_PATH'] = new_ld_library_path
263 
264  def _get_lightdm_mock_path(self):
265  lib_path = get_mocks_library_path()
266  lightdm_mock_path = os.path.abspath(
267  os.path.join(lib_path, "LightDM" ,"IntegratedLightDM", "liblightdm")
268  )
269 
270  if not os.path.exists(lightdm_mock_path):
271  raise RuntimeError(
272  "LightDM mock does not exist at path '%s'."
273  % (lightdm_mock_path)
274  )
275  return lightdm_mock_path
276 
277  def _set_proxy(self, proxy):
278  """Keep a copy of the proxy object, so we can use it to get common
279  parts of the shell later on.
280 
281  """
282  self._proxy = proxy
283  self.addCleanup(self._clear_proxy)
284 
285  def _clear_proxy(self):
286  self._proxy = None
287 
288  def wait_for_unity(self):
289  greeter = self.main_window.wait_select_single(objectName='greeter')
290  greeter.waiting.wait_for(False)
291 
292  def get_dash(self):
293  pid = process_helpers.get_job_pid('unity8-dash')
294  dash_proxy = introspection.get_proxy_object_for_existing_process(
295  pid=pid,
296  emulator_base=ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase
297  )
298  dash_app = dash_helpers.DashApp(dash_proxy)
299  return dash_app.dash
300 
301  @property
302  def main_window(self):
303  return self._proxy.select_single(shell.ShellView)
304 
305 
306 class DashBaseTestCase(AutopilotTestCase):
307 
308  scenarios = ubuntu_scenarios.get_device_simulation_scenarios()
309  qml_mock_enabled = True
310  environment = {}
311 
312  def setUp(self):
313  super().setUp()
314 
315  if is_unity7_running():
316  self.useFixture(toolkit_fixtures.HideUnity7Launcher())
317 
318  if model() != 'Desktop':
319  # On the phone, we need unity to be running and unlocked.
320  self.addCleanup(process_helpers.stop_job, 'unity8')
321  process_helpers.restart_unity_with_testability()
322  process_helpers.unlock_unity()
323 
324  self.ensure_dash_not_running()
325 
326  if self.qml_mock_enabled:
327  self.environment['QML2_IMPORT_PATH'] = (
328  get_qml_import_path_with_mock()
329  )
330 
331  if self.should_simulate_device():
332  # This sets the grid units, so it should be called before launching
333  # the app.
334  self.simulate_device()
335 
336  binary_path = get_binary_path('unity8-dash')
337  dash_proxy = self.launch_dash(binary_path, self.environment)
338 
339  self.dash_app = dash_helpers.DashApp(dash_proxy)
340  self.dash = self.dash_app.dash
341  self.wait_for_dash()
342 
343  def ensure_dash_not_running(self):
344  if process_helpers.is_job_running('unity8-dash'):
345  process_helpers.stop_job('unity8-dash')
346 
347  def launch_dash(self, binary_path, variables):
348  launch_dash_app_fixture = fixture_setup.LaunchDashApp(
349  binary_path, variables)
350  self.useFixture(launch_dash_app_fixture)
351  return launch_dash_app_fixture.application_proxy
352 
353  def wait_for_dash(self):
354  home_scope = self.dash.get_scope_by_index(0)
355  # FIXME! There is a huge timeout here for when we're doing CI on
356  # VMs. See lp:1203715
357  self.assertThat(
358  home_scope.isLoaded,
359  Eventually(Equals(True), timeout=60)
360  )
361  self.assertThat(home_scope.isCurrent, Eventually(Equals(True)))
362 
363  def should_simulate_device(self):
364  return (hasattr(self, 'app_width') and hasattr(self, 'app_height') and
365  hasattr(self, 'grid_unit_px'))
366 
367  def simulate_device(self):
368  simulate_device_fixture = self.useFixture(
369  toolkit_fixtures.SimulateDevice(
370  self.app_width, self.app_height, self.grid_unit_px))
371  self.environment['GRID_UNIT_PX'] = simulate_device_fixture.grid_unit_px
372  self.environment['ARGS'] = '-windowgeometry {0}x{1}'\
373  .format(simulate_device_fixture.app_width,
374  simulate_device_fixture.app_height)
def launch_unity(self, mode="full-greeter", args)
Definition: __init__.py:183
def _set_proxy(self, proxy)
Definition: __init__.py:277
def _geo_larger_than_display(self, width, height)
Definition: __init__.py:167
def _setup_grid_size(self, scale_divisor)
Definition: __init__.py:156
def _launch_unity_with_upstart(self, binary_path, args)
Definition: __init__.py:233
def _get_scaled_down_geo(self, width, height)
Definition: __init__.py:177