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