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