1# Copyright 2017 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 difflib 6import logging 7import os 8import pprint 9import re 10import time 11 12from autotest_lib.client.common_lib import error 13from autotest_lib.server.cros.faft.cr50_test import Cr50Test 14 15 16class firmware_Cr50ConsoleCommands(Cr50Test): 17 """ 18 Verify the cr50 console output for important commands. 19 20 This test verifies the output of pinmux, help, gpiocfg. These are the main 21 console commands we can use to check cr50 configuration. 22 """ 23 version = 1 24 25 # The board properties that are actively being used. This also excludes all 26 # ccd board properties, because they might change based on whether ccd is 27 # enabled. 28 # 29 # This information is in ec/board/cr50/scratch_reg1.h 30 RELEVANT_PROPERTIES = 0x63 31 COMPARE_LINES = '\n' 32 COMPARE_WORDS = None 33 SORTED = True 34 CMD_RETRY_COUNT = 5 35 TESTS = [ 36 ['pinmux', 'pinmux(.*)>', COMPARE_LINES, not SORTED], 37 ['help', 'Known commands:(.*)HELP LIST.*>', COMPARE_WORDS, SORTED], 38 ['gpiocfg', 'gpiocfg(.*)>', COMPARE_LINES, not SORTED], 39 ] 40 CCD_HOOK_WAIT = 2 41 # Lists connecting the board property values to the labels. 42 # [ board property, match label, exclude label ] 43 # exclude can be none if there is no label that shoud be excluded based on 44 # the property. 45 BOARD_PROPERTIES = [ 46 ['BOARD_PERIPH_CONFIG_SPI', 'sps', 'i2cs'], 47 ['BOARD_PERIPH_CONFIG_I2C', 'i2cs', 'sps,sps_ds_resume'], 48 ['BOARD_USE_PLT_RESET', 'plt_rst', 'sys_rst'], 49 [ 50 'BOARD_CLOSED_SOURCE_SET1', 'closed_source_set1', 51 'open_source_set' 52 ], 53 ['BOARD_EC_CR50_COMM_SUPPORT', 'ec_comm', 'no_ec_comm'], 54 [ 55 'BOARD_CCD_REC_LID_PIN_DIOA1', 'rec_lid_a1', 56 'rec_lid_a9,rec_lid_a12, i2cs,sps_ds_resume' 57 ], 58 [ 59 'BOARD_CCD_REC_LID_PIN_DIOA9', 'rec_lid_a9', 60 'rec_lid_a1,rec_lid_a12,i2cs' 61 ], 62 [ 63 'BOARD_CCD_REC_LID_PIN_DIOA12', 'rec_lid_a12', 64 'rec_lid_a1,rec_lid_a9,sps' 65 ], 66 ] 67 GUC_BRANCH_STR = 'cr50_v1.9308_26_0.' 68 MP_BRANCH_STR = 'cr50_v2.94_mp' 69 PREPVT_BRANCH_STR = 'cr50_v3.94_pp' 70 TOT_STR = 'cr50_v2.0.' 71 OPTIONAL_EXT = '_optional' 72 73 def initialize(self, host, cmdline_args, full_args): 74 super(firmware_Cr50ConsoleCommands, self).initialize(host, cmdline_args, 75 full_args) 76 self.host = host 77 self.missing = [] 78 self.extra = [] 79 self.past_matches = {} 80 self._ext = '' 81 82 # Make sure the console is restricted 83 if self.cr50.get_cap('GscFullConsole')[self.cr50.CAP_REQ] == 'Always': 84 logging.info('Restricting console') 85 self.fast_ccd_open(enable_testlab=True) 86 self.cr50.set_cap('GscFullConsole', 'IfOpened') 87 time.sleep(self.CCD_HOOK_WAIT) 88 self.cr50.set_ccd_level('lock') 89 self.is_tot_run = (full_args.get('tot_test_run', '').lower() == 'true') 90 91 92 def parse_output(self, output, split_str): 93 """Split the output with the given delimeter and remove empty strings""" 94 output = output.split(split_str) if split_str else output.split() 95 cleaned_output = [] 96 for line in output: 97 # Replace whitespace characters with one space. 98 line = ' '.join(line.strip().split()) 99 if line: 100 cleaned_output.append(line) 101 return cleaned_output 102 103 104 def get_output(self, cmd, regexp, split_str, sort): 105 """Return the cr50 console output""" 106 old_output = [] 107 output = self.cr50.send_command_retry_get_output( 108 cmd, [regexp], safe=True, compare_output=True, 109 retries=10)[0][1].strip() 110 111 # Record the original command output 112 results_path = os.path.join(self.resultsdir, cmd) 113 with open(results_path, 'w') as f: 114 f.write(output) 115 116 output = self.parse_output(output, split_str) 117 if sort: 118 # Sort the output ignoring any '-'s at the start of the command. 119 output.sort(key=lambda cmd: cmd.lstrip('-')) 120 if not len(output): 121 raise error.TestFail('Could not get %s output' % cmd) 122 return '\n'.join(output) + '\n' 123 124 125 def get_expected_output(self, cmd, split_str): 126 """Return the expected cr50 console output""" 127 file_dir = os.path.dirname(os.path.realpath(__file__)) 128 path = os.path.join(file_dir, cmd + self._ext) 129 if not os.path.isfile(path): 130 path = os.path.join(file_dir, cmd) 131 if not os.path.isfile(path): 132 raise error.TestFail('Could not find %s file %s' % (cmd, path)) 133 logging.info('reading %s', path) 134 135 with open(path, 'r') as f: 136 contents = f.read() 137 138 return self.parse_output(contents, split_str) 139 140 141 def check_command(self, cmd, regexp, split_str, sort): 142 """Compare the actual console command output to the expected output""" 143 expected_output = self.get_expected_output(cmd, split_str) 144 output = self.get_output(cmd, regexp, split_str, sort) 145 diff_info = difflib.unified_diff(expected_output, output.splitlines()) 146 logging.debug('%s DIFF:\n%s', cmd, '\n'.join(diff_info)) 147 missing = [] 148 extra = [] 149 for regexp in expected_output: 150 match = re.search(regexp, output) 151 if match: 152 # Update the past_matches dict with the matches from this line. 153 # 154 # Make sure if matches for any keys existed before, they exist 155 # now and if they didn't exist, they don't exist now. 156 for k, v in match.groupdict().items(): 157 old_val = self.past_matches.get(k, [v, v])[0] 158 159 # If there's an optional key, then the value may or may not 160 # match. Save any value that's not none, so the test can 161 # verify boards correctly exclude optional lines. 162 if self.OPTIONAL_EXT in k: 163 self.past_matches[k] = [old_val or v, regexp] 164 elif old_val and not v: 165 missing.append('%s:%s' % (k, regexp)) 166 elif not old_val and v: 167 extra.append('%s:%s' % (k, v)) 168 else: 169 self.past_matches[k] = [v, regexp] 170 171 # Remove the matching string from the output. 172 output, n = re.subn('%s\s*' % regexp, '', output, 1) 173 if not n: 174 missing.append(regexp) 175 176 177 if missing: 178 self.missing.append('%s-(%s)' % (cmd, ', '.join(missing))) 179 output = output.strip() 180 if output: 181 extra.extend(output.split('\n')) 182 if extra: 183 self.extra.append('%s-(%s)' % (cmd, ', '.join(extra))) 184 185 186 def get_image_properties(self): 187 """Save the board properties 188 189 The saved board property flags will not include oboslete flags or the wp 190 setting. These won't change the gpio or pinmux settings. 191 """ 192 self.include = [] 193 self.exclude = [] 194 for prop, include, exclude in self.BOARD_PROPERTIES: 195 if self.cr50.uses_board_property(prop): 196 self.include.extend(include.split(',')) 197 if exclude: 198 self.exclude.extend(exclude.split(',')) 199 else: 200 self.exclude.append(include) 201 version = self.cr50.get_full_version() 202 # Factory images end with 22. Expect guc attributes if the version 203 # ends in 22. 204 if self.GUC_BRANCH_STR in version: 205 self._ext = '.guc' 206 self.include.append('guc') 207 self.exclude.append('tot') 208 self.exclude.append('prepvt') 209 elif self.is_tot_run or self.TOT_STR in version: 210 # TOT isn't that controlled. It may include prepvt, mp, or guc 211 # changes. Don't exclude any branches. 212 self.include.append('tot') 213 elif self.MP_BRANCH_STR in version: 214 self.include.append('mp') 215 216 self.exclude.append('prepvt') 217 self.exclude.append('guc') 218 self.exclude.append('tot') 219 elif self.PREPVT_BRANCH_STR in version: 220 self.include.append('prepvt') 221 222 self.exclude.append('mp') 223 self.exclude.append('guc') 224 self.exclude.append('tot') 225 else: 226 raise error.TestNAError('Unsupported branch %s', version) 227 brdprop = self.cr50.get_board_properties() 228 logging.info('brdprop: 0x%x', brdprop) 229 logging.info('include: %s', ', '.join(self.include)) 230 logging.info('exclude: %s', ', '.join(self.exclude)) 231 232 233 def run_once(self, host): 234 """Verify the Cr50 gpiocfg, pinmux, and help output.""" 235 err = [] 236 test_err = [] 237 self.get_image_properties() 238 for command, regexp, split_str, sort in self.TESTS: 239 self.check_command(command, regexp, split_str, sort) 240 241 if len(self.missing): 242 err.append('MISSING OUTPUT: ' + ', '.join(self.missing)) 243 if len(self.extra): 244 err.append('EXTRA OUTPUT: ' + ', '.join(self.extra)) 245 logging.debug("Past matches:\n%s", pprint.pformat(self.past_matches)) 246 247 if len(err): 248 raise error.TestFail('\t'.join(err)) 249 250 # Check all of the labels we did/didn't match. Make sure they match the 251 # expected cr50 settings. Raise a test error if there are any mismatches 252 missing_labels = [] 253 for label in self.include: 254 if label in self.past_matches and not self.past_matches[label][0]: 255 missing_labels.append(label) 256 extra_labels = [] 257 for label in self.exclude: 258 if label in self.past_matches and self.past_matches[label][0]: 259 extra_labels.append(label) 260 label = label + self.OPTIONAL_EXT 261 if label in self.past_matches and self.past_matches[label][0]: 262 extra_labels.append(label) 263 if missing_labels: 264 test_err.append('missing: %s' % ', '.join(missing_labels)) 265 if extra_labels: 266 test_err.append('matched: %s' % ', '.join(extra_labels)) 267 if test_err: 268 raise error.TestError('\t'.join(test_err)) 269