| # Copyright 2017 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| |
| """Updates Device Data (manually or from predefined values in test list). |
| |
| Description |
| ----------- |
| The Device Data (``cros.factory.test.device_data``) is a special data structure |
| for manipulating DUT information. This test can determine Device Data |
| information (usually for VPD) without using shopfloor backend, usually |
| including: |
| |
| - ``serials.serial_number``: The device serial number. |
| - ``vpd.ro.region``: Region data (RO region). |
| - ``vpd.rw.ubind_attribute`` and ``vpd.rw.gbind_attribute``: User and group |
| registration codes. |
| - Or other values specified in argument ``fields`` or ``config_name``. |
| |
| When argument `manual_input` is True, every values specified in ``fields`` will |
| be displayed on screen with an edit box before written into device data. |
| Note all the values will be written as string in manual mode. |
| |
| The ``fields`` argument is a sequence in format |
| ``(data_key, value, display_name, value_check)``: |
| |
| ================ ============================================================== |
| Name Description |
| ================ ============================================================== |
| ``data_key`` The Device Data key name to write. |
| ``value`` The value to be written, can be modified if ``manual_input`` is |
| True. |
| ``display_name`` The label or name to be displayed on UI. |
| ``value_check`` To validate the input value. Can be a regular expression, |
| sequence of string or boolean values, or None for any string. |
| ================ ============================================================== |
| |
| If you want to manually configure without default values, the sequence can be |
| replaced by a simple string of key name. |
| |
| The ``config_name`` refers to a JSON config file loaded by |
| ``cros.factory.py.utils.config_utils`` with single dictionary that the keys |
| and values will be directly sent to Device Data. This is helpful if you need to |
| define board-specific data. |
| |
| ``config_name`` and ``fields`` are both optional, but you must specify at least |
| one. |
| |
| If you want to set device data (especially VPD values) using shopfloor or |
| pre-defined values: |
| |
| 1. Use ``shopfloor_service`` test with method=GetDeviceInfo to retrieve |
| ``vpd.{ro,rw}.*``. |
| 2. Use ``update_device_data`` test to write pre-defined or update values to |
| ``vpd.{ro,rw}.*``. |
| 3. Use ``write_device_data_to_vpd`` to flush data into firmware VPD sections. |
| |
| Test Procedure |
| -------------- |
| If argument ``manual_input`` is not True, this will be an automated test |
| without user interaction. |
| |
| If argument ``manual_input`` is True, the test will go through all the fields: |
| |
| 1. Display the name and key of the value. |
| 2. Display an input edit box for simple values, or a list of selection |
| if the ``value_check`` is a sequence of strings or boolean values. |
| 3. Wait for operator to select or input right value. |
| 4. If operator presses ESC, abandon changes and keep original value. |
| 5. If operator clicks Enter, validate the input by ``value_check`` argument. |
| If failed, prompt and go back to 3. |
| Otherwise, write into device data and move to next field. |
| 6. Pass when all fields were processed. |
| |
| Dependency |
| ---------- |
| None. This test only deals with the ``device_data`` module inside factory |
| software framework. |
| |
| Examples |
| -------- |
| To silently load device-specific data defined in board overlay |
| ``py/config/default_device_data.json``, add this in test list:: |
| |
| { |
| "pytest_name": "update_device_data", |
| "args": { |
| "config_name": "default", |
| "manual_input": false |
| } |
| } |
| |
| To silently set a device data 'component.has_touchscreen' to True:: |
| |
| { |
| "pytest_name": "update_device_data", |
| "args": { |
| "fields": [ |
| [ |
| "component.has_touchscreen", |
| true, |
| "Device has touch screen", |
| null |
| ] |
| ], |
| "manual_input": false |
| } |
| } |
| |
| For RMA process to set serial number, region, registration codes, and specify |
| if the device has peripherals like touchscreen:: |
| |
| { |
| "pytest_name": "update_device_data", |
| "args": { |
| "fields": [ |
| [ |
| "serials.serial_number", |
| null, |
| "Device Serial Number", |
| "[A-Z0-9]+" |
| ], |
| ["vpd.ro.region", "us", "Region", null], |
| ["vpd.rw.ubind_attribute", null, "User ECHO", null], |
| ["vpd.rw.gbind_attribute", null, "Group ECHO", null], |
| [ |
| "component.has_touchscreen", |
| null, |
| "Has touchscreen", |
| [true, false] |
| ] |
| ] |
| } |
| } |
| |
| If you don't need default values, there's an alternative to list only key |
| names:: |
| |
| { |
| "pytest_name": "update_device_data", |
| "args": { |
| "fields": [ |
| "serials.serial_number", |
| "vpd.ro.region", |
| "vpd.rw.ubind_attribute", |
| "vpd.rw.gbind_attribute" |
| ] |
| } |
| } |
| """ |
| |
| |
| from __future__ import print_function |
| |
| import Queue |
| import logging |
| import re |
| |
| import factory_common # pylint: disable=unused-import |
| from cros.factory.test import device_data |
| from cros.factory.test import i18n |
| from cros.factory.test.i18n import _ |
| from cros.factory.test.l10n import regions |
| from cros.factory.test import test_case |
| from cros.factory.test import test_ui |
| from cros.factory.test import ui_templates |
| from cros.factory.utils.arg_utils import Arg |
| from cros.factory.utils import sync_utils |
| |
| |
| # Known regions to be listed first. |
| _KNOWN_REGIONS = ( |
| 'us', 'gb', 'de', 'fr', 'ch', 'nordic', 'latam-es-419', |
| ) |
| |
| _KNOWN_KEY_LABELS = { |
| device_data.KEY_SERIAL_NUMBER: _('Device Serial Number'), |
| device_data.KEY_MLB_SERIAL_NUMBER: _('Mainboard Serial Number'), |
| device_data.KEY_VPD_REGION: _('VPD Region Code'), |
| device_data.KEY_VPD_USER_REGCODE: _('User Registration Code'), |
| device_data.KEY_VPD_GROUP_REGCODE: _('Group Registration Code'), |
| } |
| |
| _SELECTION_PER_PAGE = 10 |
| |
| |
| class DataEntry(object): |
| """Quick access to an entry in DeviceData. |
| |
| Properties: |
| key: A string as Device Data key. |
| value: Default value to be set. |
| label: A I18N label to display on UI. |
| value_check: A regular expression string or list of values or None for |
| validation of new value. |
| re_checker: The compiled regular expression to validate input value. |
| codes: A list of string for UI to use as reference to selected values. |
| options: A list of strings for UI to display as option to select from. |
| """ |
| |
| def __init__(self, key, value=None, display_name=None, value_check=None): |
| device_data.CheckValidDeviceDataKey(key) |
| self.key = key |
| self.value = device_data.GetDeviceData(key) if value is None else value |
| if display_name is None and key in _KNOWN_KEY_LABELS: |
| display_name = _KNOWN_KEY_LABELS[key] |
| self.label = (i18n.StringFormat('{name} ({key})', name=display_name, |
| key=key) if display_name else key) |
| |
| self.re_checker = None |
| self.value_check = None |
| self.codes = None |
| self.options = None |
| |
| if isinstance(value, bool) and value_check is None: |
| value_check = [True, False] |
| |
| if isinstance(value_check, basestring): |
| self.re_checker = re.compile(value_check) |
| elif isinstance(value_check, list): |
| self.value_check = value_check |
| self.codes = [str(v) for v in value_check] |
| elif value_check is None: |
| self.value_check = value_check |
| else: |
| raise TypeError('value_check (%r) for %s must be either regex, sequence, ' |
| 'or None.' % (value_check, key)) |
| |
| # Region should be processed differently. |
| if key == device_data.KEY_VPD_REGION: |
| all_regions = regions.REGIONS.keys() |
| if not value_check: |
| ordered_values = [v for v in _KNOWN_REGIONS if v in all_regions] |
| other_values = list(set(all_regions) - set(ordered_values)) |
| other_values.sort() |
| value_check = ordered_values + other_values |
| self.value_check = value_check |
| |
| assert set(self.value_check).issubset(set(all_regions)) |
| self.codes = value_check |
| self.options = [ |
| '%d - %s; %s' % (i + 1, v, regions.REGIONS[v].description) |
| for i, v in enumerate(self.codes)] |
| elif isinstance(self.value_check, list): |
| self.codes = [str(v) for v in self.value_check] |
| self.options = [ |
| '%d - %s' % (i + 1, v) if isinstance(v, basestring) else str(v) |
| for i, v in enumerate(self.value_check)] |
| |
| def GetInputList(self): |
| """Returns the list of allowed input, or None for raw input.""" |
| return self.codes |
| |
| def GetOptionList(self): |
| """Returns the options to display if the allowed input is a list.""" |
| return self.options |
| |
| def GetValueIndex(self): |
| """Returns the index of current value in input list. |
| |
| Raises: |
| ValueError if current value is not in known list. |
| """ |
| return self.value_check.index(self.value) |
| |
| def IsValidInput(self, input_data): |
| """Checks if input data is valid. |
| |
| The input data may be a real string or one of the value in self.codes. |
| """ |
| if self.re_checker: |
| logging.info('trying re_checker') |
| return self.re_checker.match(input_data) |
| if self.value_check is None: |
| return True |
| if self.codes: |
| return input_data in self.codes |
| raise ValueError('Unknown value_check: %r' % self.value_check) |
| |
| def GetValue(self, input_data): |
| """Returns the real value from input data.""" |
| if self.codes: |
| return self.value_check[self.codes.index(input_data)] |
| return input_data |
| |
| |
| class UpdateDeviceData(test_case.TestCase): |
| ARGS = [ |
| Arg('manual_input', bool, |
| 'Set to False to silently updating all values. Otherwise each value ' |
| 'will be prompted before set into Device Data.', |
| default=True), |
| Arg('config_name', basestring, |
| 'A JSON config name to load representing the device data to update.', |
| default=None), |
| Arg('fields', list, |
| ('A list of [data_key, value, display_name, value_check] ' |
| 'indicating the Device Data field by data_key must be updated to ' |
| 'specified value.'), |
| default=None), |
| ] |
| |
| def setUp(self): |
| # Either config_name or fields must be specified. |
| if self.args.config_name is None and self.args.fields is None: |
| raise ValueError('Either config_name or fields must be specified.') |
| |
| fields = [] |
| |
| if self.args.config_name: |
| fields += [(k, v, None, None) for k, v in |
| device_data.LoadConfig(self.args.config_name).iteritems()] |
| |
| if self.args.fields: |
| fields += self.args.fields |
| |
| # Syntax sugar: If the sequence was replaced by a simple string, consider |
| # that as data_key only. |
| self.entries = [ |
| DataEntry(args) if isinstance(args, basestring) else DataEntry(*args) |
| for args in fields |
| ] |
| |
| # Setup UI and update accordingly. |
| self.ui.ToggleTemplateClass('font-large', True) |
| |
| def runTest(self): |
| if self.args.manual_input: |
| for entry in self.entries: |
| self.ManualInput(entry) |
| else: |
| results = dict((entry.key, entry.value) for entry in self.entries) |
| device_data.UpdateDeviceData(results) |
| |
| def ManualInput(self, entry): |
| event_subtype = 'devicedata-' + entry.key |
| event_queue = Queue.Queue() |
| |
| if entry.GetInputList(): |
| self._RenderSelectBox(entry) |
| self.ui.BindKeyJS(test_ui.ENTER_KEY, 'window.sendSelectValue(%r, %r)' % |
| (entry.key, event_subtype)) |
| else: |
| self._RenderInputBox(entry) |
| self.ui.BindKey( |
| test_ui.ESCAPE_KEY, lambda unused_event: event_queue.put(None)) |
| self.ui.BindKeyJS(test_ui.ENTER_KEY, 'window.sendInputValue(%r, %r)' % ( |
| entry.key, event_subtype)) |
| |
| self.event_loop.AddEventHandler(event_subtype, event_queue.put) |
| |
| while True: |
| event = sync_utils.QueueGet(event_queue) |
| if event is None: |
| # ESC pressed. |
| if entry.value is not None: |
| break |
| self._SetErrorMsg( |
| _('No valid data on machine for {label}.', label=entry.label)) |
| else: |
| data = event.data |
| if entry.IsValidInput(data): |
| value = entry.GetValue(data) |
| if value is not None: |
| device_data.UpdateDeviceData({entry.key: value}) |
| break |
| self._SetErrorMsg(_('Invalid value for {label}.', label=entry.label)) |
| |
| self.ui.UnbindAllKeys() |
| self.event_loop.ClearHandlers() |
| |
| def _SetErrorMsg(self, msg): |
| self.ui.SetHTML( |
| ['<span class="test-error">', msg, '</span>'], id='errormsg') |
| |
| def _RenderSelectBox(self, entry): |
| # Renders a select box to list all the possible values. |
| select_box = ui_templates.SelectBox(entry.key, _SELECTION_PER_PAGE) |
| for value, option in zip(entry.GetInputList(), |
| entry.GetOptionList()): |
| select_box.AppendOption(value, option) |
| |
| try: |
| select_box.SetSelectedIndex(entry.GetValueIndex()) |
| except ValueError: |
| pass |
| |
| html = [ |
| _('Select {label}:', label=entry.label), |
| select_box.GenerateHTML(), |
| _('Select with ENTER') |
| ] |
| |
| self.ui.SetState(html) |
| self.ui.SetFocus(entry.key) |
| |
| def _RenderInputBox(self, entry): |
| html = [ |
| _('Enter {label}: ', label=entry.label), |
| '<input type="text" id="%s" value="%s" style="width: 20em;">' |
| '<div id="errormsg" class="test-error"></div>' % (entry.key, |
| entry.value or '') |
| ] |
| |
| if entry.value: |
| # The "ESC" is available primarily for RMA and testing process, when |
| # operator does not want to change existing serial number. |
| html.append(_('(ESC to keep current value)')) |
| |
| self.ui.SetState(html) |
| self.ui.SetSelected(entry.key) |
| self.ui.SetFocus(entry.key) |