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  self.grid_size = int(os.getenv('GRID_UNIT_PX'))
160  else:
161  self.grid_size = int(self.grid_unit_px / scale_divisor)
162  self._environment["GRID_UNIT_PX"] = str(self.grid_size)
163 
164  def _geo_larger_than_display(self, width, height):
165  should_scale = getattr(self, 'scale_geo', True)
166  if should_scale:
167  screen = Display.create()
168  screen_width = screen.get_screen_width()
169  screen_height = screen.get_screen_height()
170  return (width > screen_width) or (height > screen_height)
171  else:
172  return False
173 
174  def _get_scaled_down_geo(self, width, height):
175  divisor = 1
176  while self._geo_larger_than_display(width / divisor, height / divisor):
177  divisor = divisor * 2
178  return divisor
179 
180  def launch_unity(self, mode="full-greeter", *args):
181  """
182  Launch the unity shell, return a proxy object for it.
183 
184  :param str mode: The type of greeter/shell mode to use
185  :param args: A list of aguments to pass to unity8
186 
187  """
188  binary_path = get_binary_path()
189  lib_path = get_lib_path()
190 
191  logger.info(
192  "Lib path is '%s', binary path is '%s'",
193  lib_path,
194  binary_path
195  )
196 
197  self.patch_lightdm_mock()
198 
199  if self._qml_mock_enabled:
200  self._environment['QML2_IMPORT_PATH'] = (
201  get_qml_import_path_with_mock()
202  )
203 
204  if self._data_dirs_mock_enabled:
205  self._patch_data_dirs()
206 
207  unity8_cli_args_list = ["--mode={}".format(mode)]
208  if len(args) != 0:
209  unity8_cli_args_list += args
210 
211  app_proxy = self._launch_unity_with_upstart(
212  binary_path,
213  self.unity_geometry_args + unity8_cli_args_list
214  )
215 
216  self._set_proxy(app_proxy)
217 
218  # Ensure that the dash is visible before we return:
219  logger.debug("Unity started, waiting for it to be ready.")
220  self.wait_for_unity()
221  logger.debug("Unity loaded and ready.")
222 
223  if model() == 'Desktop':
224  # On desktop, close the dash because it's opened in a separate
225  # window and it gets in the way.
226  process_helpers.stop_job('unity8-dash')
227 
228  return app_proxy
229 
230  def _launch_unity_with_upstart(self, binary_path, args):
231  logger.info("Starting unity")
232  self.useFixture(toolkit_fixtures.InitctlEnvironmentVariable(
233  global_=True, QT_LOAD_TESTABILITY=1))
234 
235  variables = self._environment
236  variables['ARGS'] = " ".join(args)
237  launch_unity_fixture = fixture_setup.RestartUnityWithTestability(
238  binary_path, variables)
239  self.useFixture(launch_unity_fixture)
240  return launch_unity_fixture.unity_proxy
241 
242  def _patch_data_dirs(self):
243  data_dirs = get_data_dirs(self._data_dirs_mock_enabled)
244  if data_dirs is not None:
245  self._environment['XDG_DATA_DIRS'] = data_dirs
246 
247  def patch_lightdm_mock(self):
248  logger.info("Setting up LightDM mock lib")
249  new_ld_library_path = [
250  get_default_extra_mock_libraries(),
252  ]
253  if os.getenv('LD_LIBRARY_PATH') is not None:
254  new_ld_library_path.append(os.getenv('LD_LIBRARY_PATH'))
255 
256  new_ld_library_path = ':'.join(new_ld_library_path)
257  logger.info("New library path: %s", new_ld_library_path)
258 
259  self._environment['LD_LIBRARY_PATH'] = new_ld_library_path
260 
261  def _get_lightdm_mock_path(self):
262  lib_path = get_mocks_library_path()
263  lightdm_mock_path = os.path.abspath(
264  os.path.join(lib_path, "IntegratedLightDM", "liblightdm")
265  )
266 
267  if not os.path.exists(lightdm_mock_path):
268  raise RuntimeError(
269  "LightDM mock does not exist at path '%s'."
270  % (lightdm_mock_path)
271  )
272  return lightdm_mock_path
273 
274  def _set_proxy(self, proxy):
275  """Keep a copy of the proxy object, so we can use it to get common
276  parts of the shell later on.
277 
278  """
279  self._proxy = proxy
280  self.addCleanup(self._clear_proxy)
281 
282  def _clear_proxy(self):
283  self._proxy = None
284 
285  def wait_for_unity(self):
286  greeter = self.main_window.wait_select_single(objectName='greeter')
287  greeter.waiting.wait_for(False)
288 
289  def get_dash(self):
290  pid = process_helpers.get_job_pid('unity8-dash')
291  dash_proxy = introspection.get_proxy_object_for_existing_process(
292  pid=pid,
293  emulator_base=ubuntuuitoolkit.UbuntuUIToolkitCustomProxyObjectBase
294  )
295  dash_app = dash_helpers.DashApp(dash_proxy)
296  return dash_app.dash
297 
298  @property
299  def main_window(self):
300  return self._proxy.select_single(shell.ShellView)
301 
302 
303 class DashBaseTestCase(AutopilotTestCase):
304 
305  scenarios = ubuntu_scenarios.get_device_simulation_scenarios()
306  qml_mock_enabled = True
307  environment = {}
308 
309  def setUp(self):
310  super().setUp()
311 
312  if is_unity7_running():
313  self.useFixture(toolkit_fixtures.HideUnity7Launcher())
314 
315  if model() != 'Desktop':
316  # On the phone, we need unity to be running and unlocked.
317  self.addCleanup(process_helpers.stop_job, 'unity8')
318  process_helpers.restart_unity_with_testability()
319  process_helpers.unlock_unity()
320 
321  self.ensure_dash_not_running()
322 
323  if self.qml_mock_enabled:
324  self.environment['QML2_IMPORT_PATH'] = (
325  get_qml_import_path_with_mock()
326  )
327 
328  if self.should_simulate_device():
329  # This sets the grid units, so it should be called before launching
330  # the app.
331  self.simulate_device()
332 
333  binary_path = get_binary_path('unity8-dash')
334  dash_proxy = self.launch_dash(binary_path, self.environment)
335 
336  self.dash_app = dash_helpers.DashApp(dash_proxy)
337  self.dash = self.dash_app.dash
338  self.wait_for_dash()
339 
340  def ensure_dash_not_running(self):
341  if process_helpers.is_job_running('unity8-dash'):
342  process_helpers.stop_job('unity8-dash')
343 
344  def launch_dash(self, binary_path, variables):
345  launch_dash_app_fixture = fixture_setup.LaunchDashApp(
346  binary_path, variables)
347  self.useFixture(launch_dash_app_fixture)
348  return launch_dash_app_fixture.application_proxy
349 
350  def wait_for_dash(self):
351  home_scope = self.dash.get_scope_by_index(0)
352  # FIXME! There is a huge timeout here for when we're doing CI on
353  # VMs. See lp:1203715
354  self.assertThat(
355  home_scope.isLoaded,
356  Eventually(Equals(True), timeout=60)
357  )
358  self.assertThat(home_scope.isCurrent, Eventually(Equals(True)))
359 
360  def should_simulate_device(self):
361  return (hasattr(self, 'app_width') and hasattr(self, 'app_height') and
362  hasattr(self, 'grid_unit_px'))
363 
364  def simulate_device(self):
365  simulate_device_fixture = self.useFixture(
366  toolkit_fixtures.SimulateDevice(
367  self.app_width, self.app_height, self.grid_unit_px))
368  self.environment['GRID_UNIT_PX'] = simulate_device_fixture.grid_unit_px
369  self.environment['ARGS'] = '-windowgeometry {0}x{1}'\
370  .format(simulate_device_fixture.app_width,
371  simulate_device_fixture.app_height)
def launch_unity(self, mode="full-greeter", args)
Definition: __init__.py:180
def _set_proxy(self, proxy)
Definition: __init__.py:274
def _geo_larger_than_display(self, width, height)
Definition: __init__.py:164
def _setup_grid_size(self, scale_divisor)
Definition: __init__.py:156
def _launch_unity_with_upstart(self, binary_path, args)
Definition: __init__.py:230
def _get_scaled_down_geo(self, width, height)
Definition: __init__.py:174