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 re
9import time
10
11from autotest_lib.client.common_lib import error
12from autotest_lib.server.cros.faft.cr50_test import Cr50Test
13
14
15class firmware_Cr50ConsoleCommands(Cr50Test):
16    """
17    Verify the cr50 console output for important commands.
18
19    This test verifies the output of pinmux, help, gpiocfg. These are the main
20    console commands we can use to check cr50 configuration.
21    """
22    version = 1
23
24    # The board properties that are actively being used. This also excludes all
25    # ccd board properties, because they might change based on whether ccd is
26    # enabled.
27    #
28    # This information is in ec/board/cr50/scratch_reg1.h
29    RELEVANT_PROPERTIES = 0x63
30    COMPARE_LINES = '\n'
31    COMPARE_WORDS = None
32    SORTED = True
33    TESTS = [
34        ['pinmux', 'pinmux(.*)>', COMPARE_LINES, not SORTED],
35        ['help', 'Known commands:(.*)HELP LIST.*>', COMPARE_WORDS, SORTED],
36        ['gpiocfg', 'gpiocfg(.*)>', COMPARE_LINES, not SORTED],
37    ]
38    CCD_HOOK_WAIT = 2
39    # Lists connecting the board property values to the labels.
40    #   [ board property, match label, exclude label ]
41    # exclude can be none if there is no label that shoud be excluded based on
42    # the property.
43    BOARD_PROPERTIES = [
44        [0x1, 'sps', 'i2cs'],
45        [0x2, 'i2cs', 'sps'],
46        [0x40, 'plt_rst', 'sys_rst'],
47    ]
48
49    def initialize(self, host, cmdline_args, full_args):
50        super(firmware_Cr50ConsoleCommands, self).initialize(host, cmdline_args,
51                full_args)
52        self.host = host
53        self.missing = []
54        self.extra = []
55        self.past_matches = {}
56
57        # Make sure the console is restricted
58        if self.cr50.get_cap('GscFullConsole')[self.cr50.CAP_REQ] == 'Always':
59            logging.info('Restricting console')
60            self.fast_open(enable_testlab=True)
61            self.cr50.set_cap('GscFullConsole', 'IfOpened')
62            time.sleep(self.CCD_HOOK_WAIT)
63            self.cr50.set_ccd_level('lock')
64
65
66    def parse_output(self, output, split_str):
67        """Split the output with the given delimeter and remove empty strings"""
68        output = output.split(split_str) if split_str else output.split()
69        cleaned_output = []
70        for line in output:
71            # Replace whitespace characters with one space.
72            line = ' '.join(line.strip().split())
73            if line:
74                cleaned_output.append(line)
75        return cleaned_output
76
77
78    def get_output(self, cmd, regexp, split_str, sort):
79        """Return the cr50 console output"""
80        output = self.cr50.send_safe_command_get_output(cmd,
81                                                        [regexp])[0][1].strip()
82        logging.debug('%s output:%s\n', cmd, output)
83
84        # Record the original command output
85        results_path = os.path.join(self.resultsdir, cmd)
86        with open(results_path, 'w') as f:
87            f.write(output)
88
89        output = self.parse_output(output, split_str)
90        if sort:
91            # Sort the output ignoring any '-'s at the start of the command.
92            output.sort(key=lambda cmd: cmd.lstrip('-'))
93        if not len(output):
94            raise error.TestFail('Could not get %s output' % cmd)
95        return '\n'.join(output) + '\n'
96
97
98    def get_expected_output(self, cmd, split_str):
99        """Return the expected cr50 console output"""
100        path = os.path.join(os.path.dirname(os.path.realpath(__file__)), cmd)
101        logging.info('reading %s', path)
102        if not os.path.isfile(path):
103            raise error.TestFail('Could not find %s file %s' % (cmd, path))
104
105        with open(path, 'r') as f:
106            contents = f.read()
107
108        return self.parse_output(contents, split_str)
109
110
111    def check_command(self, cmd, regexp, split_str, sort):
112        """Compare the actual console command output to the expected output"""
113        expected_output = self.get_expected_output(cmd, split_str)
114        output = self.get_output(cmd, regexp, split_str, sort)
115        diff_info = difflib.unified_diff(expected_output, output.splitlines())
116        logging.debug('%s DIFF:\n%s', cmd, '\n'.join(diff_info))
117        missing = []
118        extra = []
119        for regexp in expected_output:
120            match = re.search(regexp, output)
121            if match:
122                # Update the past_matches dict with the matches from this line.
123                #
124                # Make sure if matches for any keys existed before, they exist
125                # now and if they didn't exist, they don't exist now.
126                for k, v in match.groupdict().iteritems():
127                    old_val = self.past_matches.get(k, [v, v])[0]
128                    if old_val and not v:
129                        missing.append('%s:%s' % (k, regexp))
130                    elif not old_val and v:
131                        extra.append('%s:%s' % (k, v))
132                    else:
133                        self.past_matches[k] = [v, regexp]
134
135            # Remove the matching string from the output.
136            output, n = re.subn('%s\s*' % regexp, '', output, 1)
137            if not n:
138                missing.append(regexp)
139
140
141        if missing:
142            self.missing.append('%s-(%s)' % (cmd, ', '.join(missing)))
143        output = output.strip()
144        if output:
145            extra.extend(output.split('\n'))
146        if extra:
147            self.extra.append('%s-(%s)' % (cmd, ', '.join(extra)))
148
149
150    def get_image_properties(self):
151        """Save the board properties
152
153        The saved board property flags will not include oboslete flags or the wp
154        setting. These won't change the gpio or pinmux settings.
155        """
156        brdprop = self.cr50.get_board_properties()
157        self.include = []
158        self.exclude = []
159        for prop, include, exclude in self.BOARD_PROPERTIES:
160            if prop & brdprop:
161                self.include.append(include)
162                if exclude:
163                    self.exclude.append(exclude)
164            else:
165                self.exclude.append(include)
166        # use the major version to determine prePVT or MP. prePVT have even
167        # major versions. prod have odd
168        version = self.cr50.get_version().split('.')[1]
169        if 'mp' in self.servo.get('cr50_version'):
170            self.include.append('mp')
171            self.exclude.append('prepvt')
172        else:
173            self.exclude.append('mp')
174            self.include.append('prepvt')
175        logging.info('%s brdprop 0x%x: %s', self.servo.get('ec_board'),
176                     brdprop, ', '.join(self.include))
177
178
179    def run_once(self, host):
180        """Verify the Cr50 gpiocfg, pinmux, and help output."""
181        err = []
182        test_err = []
183        self.get_image_properties()
184        for command, regexp, split_str, sort in self.TESTS:
185            self.check_command(command, regexp, split_str, sort)
186
187        if (not self.past_matches.get('ccd_has_been_enabled', 0) and
188            self.past_matches.get('ccd_enabled', 0)):
189            err.append('Inconsistent ccd settings')
190
191        if len(self.missing):
192            err.append('MISSING OUTPUT: ' + ', '.join(self.missing))
193        if len(self.extra):
194            err.append('EXTRA OUTPUT: ' + ', '.join(self.extra))
195        logging.info(self.past_matches)
196
197        if len(err):
198            raise error.TestFail('\t'.join(err))
199
200        # Check all of the labels we did/didn't match. Make sure they match the
201        # expected cr50 settings. Raise a test error if there are any mismatches
202        missing_labels = []
203        for label in self.include:
204            if label in self.past_matches and not self.past_matches[label][0]:
205                missing_labels.append(label)
206        extra_labels = []
207        for label in self.exclude:
208            if label in self.past_matches and self.past_matches[label][0]:
209                extra_labels.append(label)
210        if missing_labels:
211            test_err.append('missing: %s' % ', '.join(missing_labels))
212        if extra_labels:
213            test_err.append('matched: %s' % ', '.join(extra_labels))
214        if test_err:
215            raise error.TestError('\t'.join(test_err))
216