| # 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. |
| |
| import difflib |
| import logging |
| import os |
| import pprint |
| import re |
| import time |
| |
| from autotest_lib.client.common_lib import error |
| from autotest_lib.server.cros.faft.cr50_test import Cr50Test |
| |
| |
| class firmware_Cr50ConsoleCommands(Cr50Test): |
| """ |
| Verify the cr50 console output for important commands. |
| |
| This test verifies the output of pinmux, help, gpiocfg. These are the main |
| console commands we can use to check cr50 configuration. |
| """ |
| version = 1 |
| |
| # The board properties that are actively being used. This also excludes all |
| # ccd board properties, because they might change based on whether ccd is |
| # enabled. |
| # |
| # This information is in ec/board/cr50/scratch_reg1.h |
| RELEVANT_PROPERTIES = 0x63 |
| COMPARE_LINES = '\n' |
| COMPARE_WORDS = None |
| SORTED = True |
| CMD_RETRY_COUNT = 5 |
| TESTS = [ |
| ['pinmux', 'pinmux(.*)>', COMPARE_LINES, not SORTED], |
| ['help', 'Known commands:(.*)HELP LIST.*>', COMPARE_WORDS, SORTED], |
| ['gpiocfg', 'gpiocfg(.*)>', COMPARE_LINES, not SORTED], |
| ] |
| CCD_HOOK_WAIT = 2 |
| # Lists connecting the board property values to the labels. |
| # [ board property, match label, exclude label ] |
| # exclude can be none if there is no label that shoud be excluded based on |
| # the property. |
| BOARD_PROPERTIES = [ |
| ['BOARD_PERIPH_CONFIG_SPI', 'sps', 'i2cs'], |
| ['BOARD_PERIPH_CONFIG_I2C', 'i2cs', 'sps,sps_ds_resume'], |
| ['BOARD_USE_PLT_RESET', 'plt_rst', 'sys_rst'], |
| [ |
| 'BOARD_CLOSED_SOURCE_SET1', 'closed_source_set1', |
| 'open_source_set' |
| ], |
| ['BOARD_EC_CR50_COMM_SUPPORT', 'ec_comm', 'no_ec_comm'], |
| [ |
| 'BOARD_CCD_REC_LID_PIN_DIOA1', 'rec_lid_a1', |
| 'rec_lid_a9,rec_lid_a12, i2cs,sps_ds_resume' |
| ], |
| [ |
| 'BOARD_CCD_REC_LID_PIN_DIOA9', 'rec_lid_a9', |
| 'rec_lid_a1,rec_lid_a12,i2cs' |
| ], |
| [ |
| 'BOARD_CCD_REC_LID_PIN_DIOA12', 'rec_lid_a12', |
| 'rec_lid_a1,rec_lid_a9,sps' |
| ], |
| ] |
| GUC_BRANCH_STR = 'cr50_v1.9308_26_0.' |
| MP_BRANCH_STR = 'cr50_v1.9308_87_mp.' |
| MP_BRANCH_STR_NEW = 'cr50_v1.9311' |
| PREPVT_BRANCH_STR = 'cr50_v1.9308_B.' |
| TOT_STR = 'cr50_v2.0.' |
| OPTIONAL_EXT = '_optional' |
| |
| def initialize(self, host, cmdline_args, full_args): |
| super(firmware_Cr50ConsoleCommands, self).initialize(host, cmdline_args, |
| full_args) |
| self.host = host |
| self.missing = [] |
| self.extra = [] |
| self.past_matches = {} |
| self._ext = '' |
| |
| # Make sure the console is restricted |
| if self.cr50.get_cap('GscFullConsole')[self.cr50.CAP_REQ] == 'Always': |
| logging.info('Restricting console') |
| self.fast_ccd_open(enable_testlab=True) |
| self.cr50.set_cap('GscFullConsole', 'IfOpened') |
| time.sleep(self.CCD_HOOK_WAIT) |
| self.cr50.set_ccd_level('lock') |
| self.is_tot_run = (full_args.get('tot_test_run', '').lower() == 'true') |
| |
| |
| def parse_output(self, output, split_str): |
| """Split the output with the given delimeter and remove empty strings""" |
| output = output.split(split_str) if split_str else output.split() |
| cleaned_output = [] |
| for line in output: |
| # Replace whitespace characters with one space. |
| line = ' '.join(line.strip().split()) |
| if line: |
| cleaned_output.append(line) |
| return cleaned_output |
| |
| |
| def get_output(self, cmd, regexp, split_str, sort): |
| """Return the cr50 console output""" |
| old_output = [] |
| output = self.cr50.send_command_retry_get_output( |
| cmd, [regexp], safe=True, compare_output=True, |
| retries=10)[0][1].strip() |
| |
| # Record the original command output |
| results_path = os.path.join(self.resultsdir, cmd) |
| with open(results_path, 'w') as f: |
| f.write(output) |
| |
| output = self.parse_output(output, split_str) |
| if sort: |
| # Sort the output ignoring any '-'s at the start of the command. |
| output.sort(key=lambda cmd: cmd.lstrip('-')) |
| if not len(output): |
| raise error.TestFail('Could not get %s output' % cmd) |
| return '\n'.join(output) + '\n' |
| |
| |
| def get_expected_output(self, cmd, split_str): |
| """Return the expected cr50 console output""" |
| file_dir = os.path.dirname(os.path.realpath(__file__)) |
| path = os.path.join(file_dir, cmd + self._ext) |
| if not os.path.isfile(path): |
| path = os.path.join(file_dir, cmd) |
| if not os.path.isfile(path): |
| raise error.TestFail('Could not find %s file %s' % (cmd, path)) |
| logging.info('reading %s', path) |
| |
| with open(path, 'r') as f: |
| contents = f.read() |
| |
| return self.parse_output(contents, split_str) |
| |
| |
| def check_command(self, cmd, regexp, split_str, sort): |
| """Compare the actual console command output to the expected output""" |
| expected_output = self.get_expected_output(cmd, split_str) |
| output = self.get_output(cmd, regexp, split_str, sort) |
| diff_info = difflib.unified_diff(expected_output, output.splitlines()) |
| logging.debug('%s DIFF:\n%s', cmd, '\n'.join(diff_info)) |
| missing = [] |
| extra = [] |
| for regexp in expected_output: |
| match = re.search(regexp, output) |
| if match: |
| # Update the past_matches dict with the matches from this line. |
| # |
| # Make sure if matches for any keys existed before, they exist |
| # now and if they didn't exist, they don't exist now. |
| for k, v in match.groupdict().items(): |
| old_val = self.past_matches.get(k, [v, v])[0] |
| |
| # If there's an optional key, then the value may or may not |
| # match. Save any value that's not none, so the test can |
| # verify boards correctly exclude optional lines. |
| if self.OPTIONAL_EXT in k: |
| self.past_matches[k] = [old_val or v, regexp] |
| elif old_val and not v: |
| missing.append('%s:%s' % (k, regexp)) |
| elif not old_val and v: |
| extra.append('%s:%s' % (k, v)) |
| else: |
| self.past_matches[k] = [v, regexp] |
| |
| # Remove the matching string from the output. |
| output, n = re.subn('%s\s*' % regexp, '', output, 1) |
| if not n: |
| missing.append(regexp) |
| |
| |
| if missing: |
| self.missing.append('%s-(%s)' % (cmd, ', '.join(missing))) |
| output = output.strip() |
| if output: |
| extra.extend(output.split('\n')) |
| if extra: |
| self.extra.append('%s-(%s)' % (cmd, ', '.join(extra))) |
| |
| |
| def get_image_properties(self): |
| """Save the board properties |
| |
| The saved board property flags will not include oboslete flags or the wp |
| setting. These won't change the gpio or pinmux settings. |
| """ |
| self.include = [] |
| self.exclude = [] |
| for prop, include, exclude in self.BOARD_PROPERTIES: |
| if self.cr50.uses_board_property(prop): |
| self.include.extend(include.split(',')) |
| if exclude: |
| self.exclude.extend(exclude.split(',')) |
| else: |
| self.exclude.append(include) |
| version = self.cr50.get_full_version() |
| # Factory images end with 22. Expect guc attributes if the version |
| # ends in 22. |
| if self.GUC_BRANCH_STR in version: |
| self._ext = '.guc' |
| self.include.append('guc') |
| self.exclude.append('tot') |
| self.exclude.append('prepvt') |
| elif self.is_tot_run or self.TOT_STR in version: |
| # TOT isn't that controlled. It may include prepvt, mp, or guc |
| # changes. Don't exclude any branches. |
| self.include.append('tot') |
| elif self.MP_BRANCH_STR in version or self.MP_BRANCH_STR_NEW in version: |
| self.include.append('mp') |
| |
| self.exclude.append('prepvt') |
| self.exclude.append('guc') |
| self.exclude.append('tot') |
| elif self.PREPVT_BRANCH_STR in version: |
| self.include.append('prepvt') |
| |
| self.exclude.append('mp') |
| self.exclude.append('guc') |
| self.exclude.append('tot') |
| else: |
| raise error.TestNAError('Unsupported branch %s', version) |
| brdprop = self.cr50.get_board_properties() |
| logging.info('brdprop: 0x%x', brdprop) |
| logging.info('include: %s', ', '.join(self.include)) |
| logging.info('exclude: %s', ', '.join(self.exclude)) |
| |
| |
| def run_once(self, host): |
| """Verify the Cr50 gpiocfg, pinmux, and help output.""" |
| err = [] |
| test_err = [] |
| self.get_image_properties() |
| for command, regexp, split_str, sort in self.TESTS: |
| self.check_command(command, regexp, split_str, sort) |
| |
| if len(self.missing): |
| err.append('MISSING OUTPUT: ' + ', '.join(self.missing)) |
| if len(self.extra): |
| err.append('EXTRA OUTPUT: ' + ', '.join(self.extra)) |
| logging.debug("Past matches:\n%s", pprint.pformat(self.past_matches)) |
| |
| if len(err): |
| raise error.TestFail('\t'.join(err)) |
| |
| # Check all of the labels we did/didn't match. Make sure they match the |
| # expected cr50 settings. Raise a test error if there are any mismatches |
| missing_labels = [] |
| for label in self.include: |
| if label in self.past_matches and not self.past_matches[label][0]: |
| missing_labels.append(label) |
| extra_labels = [] |
| for label in self.exclude: |
| if label in self.past_matches and self.past_matches[label][0]: |
| extra_labels.append(label) |
| label = label + self.OPTIONAL_EXT |
| if label in self.past_matches and self.past_matches[label][0]: |
| extra_labels.append(label) |
| if missing_labels: |
| test_err.append('missing: %s' % ', '.join(missing_labels)) |
| if extra_labels: |
| test_err.append('matched: %s' % ', '.join(extra_labels)) |
| if test_err: |
| raise error.TestError('\t'.join(test_err)) |