• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5import json
6import logging
7
8from autotest_lib.client.bin import test, utils
9from autotest_lib.client.common_lib import error
10from autotest_lib.client.common_lib.cros import chrome
11from autotest_lib.client.cros import cros_ui
12
13class 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