Home | History | Annotate | Download | only in login_OobeLocalization
      1 # Copyright 2014 The Chromium OS 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.
      4 
      5 import json
      6 import logging
      7 
      8 from autotest_lib.client.bin import test, utils
      9 from autotest_lib.client.common_lib import error
     10 from autotest_lib.client.common_lib.cros import chrome
     11 from autotest_lib.client.cros import cros_ui
     12 
     13 class login_OobeLocalization(test.test):
     14     """Tests different region configurations at OOBE."""
     15     version = 1
     16 
     17     _LANGUAGE_SELECT = 'language-select'
     18     _KEYBOARD_SELECT = 'keyboard-select'
     19     _FALLBACK_KEYBOARD = 'xkb:us::eng'
     20 
     21     # dump_vpd_log reads the VPD cache in lieu of running `vpd -l`.
     22     _VPD_FILENAME = '/var/cache/vpd/full-v2.txt'
     23     # The filtered cache is created from the cache by dump_vpd_log. It is read
     24     # at startup if the device is not owned. (Otherwise /tmp/machine-info is
     25     # created by dump_vpd_log and read. See
     26     # /platform/login_manager/init/machine-info.conf.)
     27     _FILTERED_VPD_FILENAME = '/var/log/vpd_2.0.txt'
     28     # cros-regions.json has information for each region (locale, input method,
     29     # etc.) in JSON format.
     30     _REGIONS_FILENAME = '/usr/share/misc/cros-regions.json'
     31     # input_methods.txt lists supported input methods.
     32     _INPUT_METHODS_FILENAME = ('/usr/share/chromeos-assets/input_methods/'
     33                               'input_methods.txt')
     34 
     35 
     36     def initialize(self):
     37         self._login_keyboards = self._get_login_keyboards()
     38         self._comp_ime_prefix = self._run_with_chrome(
     39                 self._get_comp_ime_prefix)
     40 
     41 
     42     def run_once(self):
     43         for region in self._get_regions():
     44             # Unconfirmed regions may have incorrect data. The 'confirm'
     45             # property is optional when all regions in database are confirmed so
     46             # we have to check explicit 'False'.
     47             if region.get('confirmed', True) is False:
     48                 logging.info('Skip unconfirmed region: %s',
     49                              region['region_code'])
     50                 continue
     51 
     52             # TODO(hungte) When OOBE supports cros-regions.json
     53             # (crosbug.com/p/34536) we can remove initial_locale,
     54             # initial_timezone, and keyboard_layout.
     55             self._set_vpd({'region': region['region_code'],
     56                            'initial_locale': ','.join(region['locales']),
     57                            'initial_timezone': ','.join(region['time_zones']),
     58                            'keyboard_layout': ','.join(region['keyboards'])})
     59             self._run_with_chrome(self._run_localization_test, region)
     60 
     61 
     62     def cleanup(self):
     63         """Removes cache files so our changes don't persist."""
     64         cros_ui.stop()
     65         utils.run('rm /home/chronos/Local\ State', ignore_status=True)
     66         utils.run('dump_vpd_log --clean')
     67 
     68 
     69     def _run_with_chrome(self, func, *args):
     70         with chrome.Chrome(auto_login=False) as self._chrome:
     71             utils.poll_for_condition(
     72                     self._is_oobe_ready,
     73                     exception=error.TestFail('OOBE not ready'))
     74             return func(*args)
     75 
     76 
     77     def _run_localization_test(self, region):
     78         """Checks the network screen for the proper dropdown values."""
     79 
     80         # Find the language(s), or acceptable alternate value(s).
     81         initial_locale = ','.join(region['locales'])
     82         if not self._verify_initial_options(
     83                 self._LANGUAGE_SELECT,
     84                 initial_locale,
     85                 alternate_values = self._resolve_language(initial_locale),
     86                 check_separator = True):
     87             raise error.TestFail(
     88                     'Language not found for region "%s".\n'
     89                     'Actual value of %s:\n%s' % (
     90                             region['region_code'],
     91                             self._LANGUAGE_SELECT,
     92                             self._dump_options(self._LANGUAGE_SELECT)))
     93 
     94         # We expect to see only login keyboards at OOBE.
     95         keyboards = region['keyboards']
     96         keyboards = [kbd for kbd in keyboards if kbd in self._login_keyboards]
     97 
     98         # If there are no login keyboards, expect only the fallback keyboard.
     99         keyboards = keyboards or [self._FALLBACK_KEYBOARD]
    100 
    101         # Prepend each xkb value with the component extension id.
    102         keyboard_ids = ','.join(
    103                 [self._comp_ime_prefix + xkb for xkb in keyboards])
    104 
    105         # Find the keyboard layout(s).
    106         if not self._verify_initial_options(
    107                 self._KEYBOARD_SELECT,
    108                 keyboard_ids):
    109             raise error.TestFail(
    110                     'Keyboard not found for region "%s".\n'
    111                     'Actual value of %s:\n%s' % (
    112                             region['region_code'],
    113                             self._KEYBOARD_SELECT,
    114                             self._dump_options(self._KEYBOARD_SELECT)))
    115 
    116         # Check that the fallback keyboard is present.
    117         if self._FALLBACK_KEYBOARD not in keyboards:
    118             if not self._verify_option_exists(
    119                     self._KEYBOARD_SELECT,
    120                     self._comp_ime_prefix + self._FALLBACK_KEYBOARD):
    121                 raise error.TestFail(
    122                         'Fallback keyboard layout not found for region "%s".\n'
    123                         'Actual value of %s:\n%s' % (
    124                                 region['region_code'],
    125                                 self._KEYBOARD_SELECT,
    126                                 self._dump_options(self._KEYBOARD_SELECT)))
    127 
    128 
    129     def _set_vpd(self, vpd_settings):
    130         """Changes VPD cache on disk.
    131         @param vpd_settings: Dictionary of VPD key-value pairs.
    132         """
    133         cros_ui.stop()
    134 
    135         vpd = {}
    136         with open(self._VPD_FILENAME, 'r+') as vpd_log:
    137             # Read the existing VPD info.
    138             for line in vpd_log:
    139                 # Extract "key"="value" pair.
    140                 key, _, value = line.replace('"', '').partition('=')
    141                 vpd[key] = value
    142 
    143             vpd.update(vpd_settings);
    144 
    145             # Write the new set of settings to disk.
    146             vpd_log.seek(0)
    147             for key in vpd:
    148                 vpd_log.write('"%s"="%s"\n' % (key, vpd[key]))
    149             vpd_log.truncate()
    150 
    151         # Remove filtered cache so dump_vpd_log recreates it from the cache we
    152         # just updated.
    153         utils.run('rm ' + self._FILTERED_VPD_FILENAME, ignore_status=True)
    154         utils.run('dump_vpd_log')
    155 
    156         # Remove cached files to clear initial locale info.
    157         utils.run('rm /home/chronos/Local\ State', ignore_status=True)
    158         utils.run('rm /home/chronos/.oobe_completed', ignore_status=True)
    159         cros_ui.start()
    160 
    161 
    162     def _verify_initial_options(self, select_id, values,
    163                                 alternate_values='', check_separator=False):
    164         """Verifies that |values| are the initial elements of |select_id|.
    165 
    166         @param select_id: ID of the select element to check.
    167         @param values: Comma-separated list of values that should appear,
    168                 in order, at the top of the select before any options group.
    169         @param alternate_values: Optional comma-separated list of alternate
    170                 values for the corresponding items in values.
    171         @param check_separator: If True, also verifies that an options group
    172                 label appears after the initial set of values.
    173 
    174         @returns whether the select fits the given constraints.
    175 
    176         @raises EvaluateException if the JS expression fails to evaluate.
    177         """
    178         js_expression = """
    179                 (function () {
    180                   var select = document.querySelector('#%s');
    181                   if (!select || select.selectedIndex)
    182                     return false;
    183                   var values = '%s'.split(',');
    184                   var alternate_values = '%s'.split(',');
    185                   for (var i = 0; i < values.length; i++) {
    186                     if (select.options[i].value != values[i] &&
    187                         (!alternate_values[i] ||
    188                          select.options[i].value != alternate_values[i]))
    189                       return false;
    190                   }
    191                   if (%d) {
    192                     return select.children[values.length].tagName ==
    193                         'OPTGROUP';
    194                   }
    195                   return true;
    196                 })()""" % (select_id,
    197                            values,
    198                            alternate_values,
    199                            check_separator)
    200 
    201         return self._chrome.browser.oobe.EvaluateJavaScript(js_expression)
    202 
    203 
    204     def _verify_option_exists(self, select_id, value):
    205         """Verifies that |value| exists in |select_id|.
    206 
    207         @param select_id: ID of the select element to check.
    208         @param value: A single value to find in the select.
    209 
    210         @returns whether the value is found.
    211 
    212         @raises EvaluateException if the JS expression fails to evaluate.
    213         """
    214         js_expression = """
    215                 (function () {
    216                   return !!document.querySelector(
    217                       '#%s option[value=\\'%s\\']');
    218                 })()""" % (select_id, value)
    219 
    220         return self._chrome.browser.oobe.EvaluateJavaScript(js_expression)
    221 
    222 
    223     def _get_login_keyboards(self):
    224         """Returns the set of login xkbs from the input methods file."""
    225         login_keyboards = set()
    226         with open(self._INPUT_METHODS_FILENAME) as input_methods_file:
    227             for line in input_methods_file:
    228                 columns = line.strip().split()
    229                 # The 5th column will be "login" if this keyboard layout will
    230                 # be used on login.
    231                 if len(columns) == 5 and columns[4] == 'login':
    232                     login_keyboards.add(columns[0])
    233         return login_keyboards
    234 
    235 
    236     def _get_regions(self):
    237         regions = {}
    238         with open(self._REGIONS_FILENAME, 'r') as regions_file:
    239             return json.load(regions_file).values()
    240 
    241 
    242     def _get_comp_ime_prefix(self):
    243         """Finds the xkb values' component extension id prefix, if any.
    244         @returns the prefix if found, or an empty string
    245         """
    246         return self._chrome.browser.oobe.EvaluateJavaScript("""
    247                 var value = document.getElementById('%s').value;
    248                 value.substr(0, value.lastIndexOf('xkb:'))""" %
    249                 self._KEYBOARD_SELECT)
    250 
    251 
    252     def _resolve_language(self, locale):
    253         """Falls back to an existing locale if the given locale matches a
    254         language but not the country. Mirrors
    255         chromium:ui/base/l10n/l10n_util.cc.
    256         """
    257         lang, _, region = map(str.lower, str(locale).partition('-'))
    258         if not region:
    259             return ''
    260 
    261         # Map from other countries to a localized country.
    262         if lang == 'es' and region == 'es':
    263             return 'es-419'
    264         if lang == 'zh':
    265             if region in ('hk', 'mo'):
    266                 return 'zh-TW'
    267             return 'zh-CN'
    268         if lang == 'en':
    269             if region in ('au', 'ca', 'nz', 'za'):
    270                 return 'en-GB'
    271             return 'en-US'
    272 
    273         # No mapping found.
    274         return ''
    275 
    276 
    277     def _is_oobe_ready(self):
    278         return (self._chrome.browser.oobe and
    279                 self._chrome.browser.oobe.EvaluateJavaScript(
    280                         "var select = document.getElementById('%s');"
    281                         "select && select.children.length >= 2" %
    282                                 self._LANGUAGE_SELECT))
    283 
    284 
    285     def _dump_options(self, select_id):
    286         js_expression = """
    287                 (function () {
    288                   var selector = '#%s';
    289                   var divider = ',';
    290                   var select = document.querySelector(selector);
    291                   if (!select)
    292                     return 'document.querySelector(\\'' + selector +
    293                         '\\') failed.';
    294                   var dumpOptgroup = function(group) {
    295                     var result = '';
    296                     for (var i = 0; i < group.children.length; i++) {
    297                       if (i > 0)
    298                         result += divider;
    299                       if (group.children[i].value)
    300                         result += group.children[i].value;
    301                       else
    302                         result += '__NO_VALUE__';
    303                     }
    304                     return result;
    305                   };
    306                   var result = '';
    307                   if (select.selectedIndex != 0) {
    308                     result += '(selectedIndex=' + select.selectedIndex +
    309                         ', selected \' +
    310                         select.options[select.selectedIndex].value +
    311                         '\)';
    312                   }
    313                   var children = select.children;
    314                   for (var i = 0; i < children.length; i++) {
    315                     if (i > 0)
    316                       result += divider;
    317                     if (children[i].value)
    318                       result += children[i].value;
    319                     else if (children[i].tagName === 'OPTGROUP')
    320                       result += '[' + dumpOptgroup(children[i]) + ']';
    321                     else
    322                       result += '__NO_VALUE__';
    323                   }
    324                   return result;
    325                 })()""" % select_id
    326         return self._chrome.browser.oobe.EvaluateJavaScript(js_expression)
    327