1# Copyright (c) 2012 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4import json
5import logging
6import os
7import unittest
8
9from telemetry.core import browser_finder
10from telemetry.core import exceptions
11from telemetry.core import extension_to_load
12from telemetry.core import util
13from telemetry.core.backends.chrome import cros_interface
14from telemetry.unittest import options_for_unittests
15
16class CrOSAutoTest(unittest.TestCase):
17  def setUp(self):
18    options = options_for_unittests.GetCopy()
19    self._cri = cros_interface.CrOSInterface(options.cros_remote,
20                                             options.cros_ssh_identity)
21    self._is_guest = options.browser_type == 'cros-chrome-guest'
22    self._username = '' if self._is_guest else options.browser_options.username
23    self._password = options.browser_options.password
24
25  def _IsCryptohomeMounted(self):
26    """Returns True if cryptohome is mounted"""
27    cryptohomeJSON, _ = self._cri.RunCmdOnDevice(['/usr/sbin/cryptohome',
28                                                 '--action=status'])
29    cryptohomeStatus = json.loads(cryptohomeJSON)
30    return (cryptohomeStatus['mounts'] and
31            cryptohomeStatus['mounts'][0]['mounted'])
32
33  def _CreateBrowser(self, autotest_ext=False, auto_login=True):
34    """Finds and creates a browser for tests. if autotest_ext is True,
35    also loads the autotest extension"""
36    options = options_for_unittests.GetCopy()
37
38    if autotest_ext:
39      extension_path = os.path.join(os.path.dirname(__file__), 'autotest_ext')
40      self._load_extension = extension_to_load.ExtensionToLoad(
41          path=extension_path,
42          browser_type=options.browser_type,
43          is_component=True)
44      options.extensions_to_load = [self._load_extension]
45
46    browser_to_create = browser_finder.FindBrowser(options)
47    self.assertTrue(browser_to_create)
48    options.browser_options.create_browser_with_oobe = True
49    options.browser_options.auto_login = auto_login
50    b = browser_to_create.Create()
51    b.Start()
52    return b
53
54  def _GetAutotestExtension(self, browser):
55    """Returns the autotest extension instance"""
56    extension = browser.extensions[self._load_extension]
57    self.assertTrue(extension)
58    return extension
59
60  def _GetLoginStatus(self, browser):
61      extension = self._GetAutotestExtension(browser)
62      self.assertTrue(extension.EvaluateJavaScript(
63          "typeof('chrome.autotestPrivate') != 'undefined'"))
64      extension.ExecuteJavaScript('''
65        window.__login_status = null;
66        chrome.autotestPrivate.loginStatus(function(s) {
67          window.__login_status = s;
68        });
69      ''')
70      return util.WaitFor(
71          lambda: extension.EvaluateJavaScript('window.__login_status'), 10)
72
73  def testCryptohomeMounted(self):
74    """Verifies cryptohome mount status for regular and guest user and when
75    logged out"""
76    with self._CreateBrowser() as b:
77      self.assertEquals(1, len(b.tabs))
78      self.assertTrue(b.tabs[0].url)
79      self.assertTrue(self._IsCryptohomeMounted())
80
81      chronos_fs = self._cri.FilesystemMountedAt('/home/chronos/user')
82      self.assertTrue(chronos_fs)
83      if self._is_guest:
84        self.assertEquals(chronos_fs, 'guestfs')
85      else:
86        home, _ = self._cri.RunCmdOnDevice(['/usr/sbin/cryptohome-path',
87                                            'user', self._username])
88        self.assertEquals(self._cri.FilesystemMountedAt(home.rstrip()),
89                          chronos_fs)
90
91    self.assertFalse(self._IsCryptohomeMounted())
92    self.assertEquals(self._cri.FilesystemMountedAt('/home/chronos/user'),
93                      '/dev/mapper/encstateful')
94
95  def testLoginStatus(self):
96    """Tests autotestPrivate.loginStatus"""
97    with self._CreateBrowser(autotest_ext=True) as b:
98      login_status = self._GetLoginStatus(b)
99      self.assertEquals(type(login_status), dict)
100
101      self.assertEquals(not self._is_guest, login_status['isRegularUser'])
102      self.assertEquals(self._is_guest, login_status['isGuest'])
103      self.assertEquals(login_status['email'], self._username)
104      self.assertFalse(login_status['isScreenLocked'])
105
106  def _IsScreenLocked(self, browser):
107    return self._GetLoginStatus(browser)['isScreenLocked']
108
109  def _LockScreen(self, browser):
110      self.assertFalse(self._IsScreenLocked(browser))
111
112      extension = self._GetAutotestExtension(browser)
113      self.assertTrue(extension.EvaluateJavaScript(
114          "typeof chrome.autotestPrivate.lockScreen == 'function'"))
115      logging.info('Locking screen')
116      extension.ExecuteJavaScript('chrome.autotestPrivate.lockScreen();')
117
118      logging.info('Waiting for the lock screen')
119      def ScreenLocked():
120        return (browser.oobe and
121            browser.oobe.EvaluateJavaScript("typeof Oobe == 'function'") and
122            browser.oobe.EvaluateJavaScript(
123            "typeof Oobe.authenticateForTesting == 'function'"))
124      util.WaitFor(ScreenLocked, 10)
125      self.assertTrue(self._IsScreenLocked(browser))
126
127  def _AttemptUnlockBadPassword(self, browser):
128      logging.info('Trying a bad password')
129      def ErrorBubbleVisible():
130        return not browser.oobe.EvaluateJavaScript('''
131            document.getElementById('bubble').hidden
132        ''')
133      self.assertFalse(ErrorBubbleVisible())
134      browser.oobe.ExecuteJavaScript('''
135          Oobe.authenticateForTesting('%s', 'bad');
136      ''' % self._username)
137      util.WaitFor(ErrorBubbleVisible, 10)
138      self.assertTrue(self._IsScreenLocked(browser))
139
140  def _UnlockScreen(self, browser):
141      logging.info('Unlocking')
142      browser.oobe.ExecuteJavaScript('''
143          Oobe.authenticateForTesting('%s', '%s');
144      ''' % (self._username, self._password))
145      util.WaitFor(lambda: not browser.oobe, 10)
146      self.assertFalse(self._IsScreenLocked(browser))
147
148  def testScreenLock(self):
149    """Tests autotestPrivate.screenLock"""
150    with self._CreateBrowser(autotest_ext=True) as browser:
151      self._LockScreen(browser)
152      self._AttemptUnlockBadPassword(browser)
153      self._UnlockScreen(browser)
154
155  def testLogout(self):
156    """Tests autotestPrivate.logout"""
157    with self._CreateBrowser(autotest_ext=True) as b:
158      extension = self._GetAutotestExtension(b)
159      try:
160        extension.ExecuteJavaScript('chrome.autotestPrivate.logout();')
161      except (exceptions.BrowserConnectionGoneException,
162              exceptions.BrowserGoneException):
163        pass
164      util.WaitFor(lambda: not self._IsCryptohomeMounted(), 20)
165
166  def _SwitchRegion(self, region):
167    self._cri.RunCmdOnDevice(['stop', 'ui'])
168
169    # Change VPD (requires RW-enabled firmware).
170    # To save time, region and initial_timezone are not set.
171    vpd = {'initial_locale': region.language_code,
172           'keyboard_layout': region.keyboard}
173
174    for (key, value) in vpd.items():
175      self._cri.RunCmdOnDevice(['vpd', '-s', '"%s"="%s"' % (key, value)])
176
177    # Remove cached files to clear initial locale info and force regeneration.
178    self._cri.RunCmdOnDevice(['rm', '/home/chronos/Local\ State'])
179    self._cri.RunCmdOnDevice(['rm', '/home/chronos/.oobe_completed'])
180    self._cri.RunCmdOnDevice(['dump_vpd_log', '--force'])
181
182    self._cri.RunCmdOnDevice(['start', 'ui'])
183
184  def _OobeHasOption(self, browser, selectId, value):
185    hasOptionJs = '''
186      // Check that the option is present, and selected if it is the default.
187      (function hasOption(selectId, value, isDefault) {
188        var options = document.getElementById(selectId).options;
189        for (var i = 0; i < options.length; i++) {
190          if (options[i].value == value) {
191            // The option is present. Make sure it's selected if necessary.
192            return !isDefault || options.selectedIndex == i;
193          }
194        }
195        return false;
196      })("%s", "%s", %s);
197    '''
198    return browser.oobe.EvaluateJavaScript(
199        hasOptionJs % (selectId, value, 'true'))
200
201  def _ResolveLanguage(self, locale):
202    # If the locale matches a language but not the country, fall back to
203    # an existing locale. See ui/base/l10n/l10n_util.cc.
204    lang, _, region = map(str.lower, locale.partition('-'))
205    if not region:
206      return ""
207
208    # Map from other countries to a localized country
209    if lang == 'es' and region == 'es':
210      return 'es-419'
211    if lang == 'zh':
212      if region in ('hk', 'mo'):
213        return 'zh-TW'
214      return 'zh-CN'
215    if lang == 'en':
216      if region in ('au', 'ca', 'nz', 'za'):
217        return 'en-GB'
218      return 'en-US'
219
220    # No mapping found
221    return ""
222
223  def testOobeLocalization(self):
224    """Tests different region configurations at OOBE"""
225    # Save the original device localization settings.
226    # To save time, only read initial_locale and keyboard_layout.
227    initial_region = self.Region('', '', '', '', '')
228    initial_region.language_code, _ = self._cri.RunCmdOnDevice(
229        ['vpd', '-g', 'initial_locale'])
230    initial_region.keyboard, _ = self._cri.RunCmdOnDevice(
231        ['vpd', '-g', 'keyboard_layout'])
232
233    for region in self.REGIONS_LIST:
234      self._SwitchRegion(region)
235      with self._CreateBrowser(auto_login=False) as browser:
236        # Ensure the dropdown lists have been created.
237        util.WaitFor(lambda: browser.oobe.EvaluateJavaScript(
238                     'document.getElementById("language-select") != null'),
239                     10)
240
241        # Find the language, or an acceptable fallback value.
242        languageFound = self._OobeHasOption(browser,
243                                            'language-select',
244                                            region.language_code)
245        if not languageFound:
246          fallback = self._ResolveLanguage(region.language_code)
247          self.assertTrue(fallback and
248                          self._OobeHasOption(browser,
249                                              'language-select',
250                                              fallback))
251
252        # Find the keyboard layout.
253        self.assertTrue(self._OobeHasOption(
254            browser, 'keyboard-select', region.keyboard))
255
256    # Test is finished. Restore original region settings.
257    self._SwitchRegion(initial_region)
258
259  # The Region class and region list will be available in regions.py.
260  class Region(object):
261    def __init__(self, region_code, keyboard, time_zone, language_code,
262                 keyboard_mechanical_layout, description=None, notes=None):
263      self.region_code = region_code
264      self.keyboard = keyboard
265      self.time_zone = time_zone
266      self.language_code = language_code
267      self.keyboard_mechanical_layout = keyboard_mechanical_layout
268      self.description = description or region_code
269      self.notes = notes
270
271  class Enum(frozenset):
272    def __getattr__(self, name):
273      if name in self:
274        return name
275      raise AttributeError
276
277  KeyboardMechanicalLayout = Enum(['ANSI', 'ISO', 'JIS', 'ABNT2'])
278  _KML = KeyboardMechanicalLayout
279  REGIONS_LIST = [
280    Region('au', 'xkb:us::eng', 'Australia/Sydney', 'en-AU', _KML.ANSI,
281           'Australia'),
282    Region('ca.ansi', 'xkb:us::eng', 'America/Toronto', 'en-CA', _KML.ANSI,
283           'Canada (US keyboard)',
284           'Canada with US (ANSI) keyboard; see http://goto/cros-canada'),
285    Region('ca.fr', 'xkb:ca::fra', 'America/Toronto', 'fr-CA', _KML.ISO,
286           'Canada (French keyboard)',
287           ('Canadian French (ISO) keyboard. The most common configuration for '
288            'Canadian French SKUs.  See http://goto/cros-canada')),
289    Region('ca.hybrid', 'xkb:ca:eng:eng', 'America/Toronto', 'en-CA', _KML.ISO,
290           'Canada (hybrid)',
291           ('Canada with hybrid xkb:ca:eng:eng + xkb:ca::fra keyboard (ISO), '
292            'defaulting to English language and keyboard.  Used only if there '
293            'needs to be a single SKU for all of Canada.  See '
294            'http://goto/cros-canada')),
295    Region('ca.multix', 'xkb:ca:multix:fra', 'America/Toronto', 'fr-CA',
296           _KML.ISO, 'Canada (multilingual)',
297           ("Canadian Multilingual keyboard; you probably don't want this. See "
298            "http://goto/cros-canada")),
299    Region('de', 'xkb:de::ger', 'Europe/Berlin', 'de', _KML.ISO, 'Germany'),
300    Region('fi', 'xkb:fi::fin', 'Europe/Helsinki', 'fi', _KML.ISO, 'Finland'),
301    Region('fr', 'xkb:fr::fra', 'Europe/Paris', 'fr', _KML.ISO, 'France'),
302    Region('gb', 'xkb:gb:extd:eng', 'Europe/London', 'en-GB', _KML.ISO, 'UK'),
303    Region('ie', 'xkb:gb:extd:eng', 'Europe/Dublin', 'en-GB', _KML.ISO,
304           'Ireland'),
305    Region('in', 'xkb:us::eng', 'Asia/Calcutta', 'en-US', _KML.ANSI, 'India'),
306    Region('my', 'xkb:us::eng', 'Asia/Kuala_Lumpur', 'ms', _KML.ANSI,
307           'Malaysia'),
308    Region('nl', 'xkb:us:intl:eng', 'Europe/Amsterdam', 'nl', _KML.ANSI,
309           'Netherlands'),
310    Region('nordic', 'xkb:se::swe', 'Europe/Stockholm', 'en-US', _KML.ISO,
311           'Nordics',
312           ('Unified SKU for Sweden, Norway, and Denmark.  This defaults '
313            'to Swedish keyboard layout, but starts with US English language '
314            'for neutrality.  Use if there is a single combined SKU for Nordic '
315            'countries.')),
316    Region('se', 'xkb:se::swe', 'Europe/Stockholm', 'sv', _KML.ISO, 'Sweden',
317           ("Use this if there separate SKUs for Nordic countries (Sweden, "
318            "Norway, and Denmark), or the device is only shipping to Sweden. "
319            "If there is a single unified SKU, use 'nordic' instead.")),
320    Region('sg', 'xkb:us::eng', 'Asia/Singapore', 'en-GB', _KML.ANSI,
321           'Singapore'),
322    Region('us', 'xkb:us::eng', 'America/Los_Angeles', 'en-US', _KML.ANSI,
323           'United States'),
324  ]
325