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
5"""This is a FAFT test to check if TCPCs are up-to-date.
6
7This test figures out which TCPCs exist on a DUT and matches
8these up with corresponding firmware blobs in the system
9image shellball.  If mismatches are detected, the test fails.
10
11The test can optionally be invoked with --args bios=... to
12specify an alternate reference firmware image.
13"""
14
15import logging
16import os
17
18from autotest_lib.client.common_lib import error
19from autotest_lib.client.common_lib import utils
20from autotest_lib.client.common_lib.cros import chip_utils
21from autotest_lib.server.cros.faft.firmware_test import FirmwareTest
22
23
24class firmware_CompareChipFwToShellBall(FirmwareTest):
25
26    """Compares the active DUT chip firmware with reference.
27
28    FAFT test to verify that a DUT runs the expected chip
29    firmware based on the system shellball or a specified
30    reference image.
31    """
32    version = 1
33
34    BIOS = 'bios.bin'
35    MAXPORTS = 100
36
37    def initialize(self, host, cmdline_args):
38        super(firmware_CompareChipFwToShellBall,
39              self).initialize(host, cmdline_args)
40        dict_args = utils.args_to_dict(cmdline_args)
41        self.new_bios_path = dict_args['bios'] if 'bios' in dict_args else None
42        self.cbfs_work_dir = None
43        self.dut_bios_path = None
44
45    def cleanup(self):
46        try:
47            if self.cbfs_work_dir:
48                self.faft_client.system.remove_dir(self.cbfs_work_dir)
49        except Exception as e:
50            logging.error("Caught exception: %s", str(e))
51        super(firmware_CompareChipFwToShellBall, self).cleanup()
52
53    def dut_get_chip(self, port):
54        """Gets the chip info for a port.
55
56        Args:
57            port: TCPC port number on DUT
58
59        Returns:
60            A chip object if available, else None.
61        """
62
63        cmd = 'mosys -s product_id pd chip %d' % port
64        chip_id = self.faft_client.system.run_shell_command_get_output(cmd)
65        if not chip_id:
66            # chip probably does not exist
67            return None
68        chip_id = chip_id[0]
69
70        if chip_id not in chip_utils.chip_id_map:
71            logging.info('chip type %s not recognized', chip_id)
72            return chip_utils.generic_chip()
73        chip = chip_utils.chip_id_map[chip_id]()
74
75        cmd = 'mosys -s fw_version pd chip %d' % port
76        fw_rev = self.faft_client.system.run_shell_command_get_output(cmd)
77        if not fw_rev:
78            # chip probably does not exist
79            return None
80        fw_rev = fw_rev[0]
81        chip.set_fw_ver_from_string(fw_rev)
82        return chip
83
84    def dut_scan_chips(self):
85        """Scans for TCPC chips on DUT.
86
87        Returns:
88            A tuple (S, L) consisting of a set S of chip types and a list L
89            of chips indexed by port number found on on the DUT.
90
91        Raises:
92            TestFail: DUT has >= MAXPORTS pd ports.
93        """
94
95        chip_types = set()
96        port2chip = []
97        for port in xrange(self.MAXPORTS):
98            chip = self.dut_get_chip(port)
99            if not chip:
100                return (chip_types, port2chip)
101            port2chip.append(chip)
102            chip_types.add(type(chip))
103        logging.error('found at least %u TCPC ports '
104                      '- please update test to handle more ports '
105                      'if this is expected.', self.MAXPORTS)
106        raise error.TestFail('MAXPORTS exceeded' % self.MAXPORTS)
107
108    def dut_locate_bios_bin(self):
109        """Finds bios.bin on DUT.
110
111        Figures out where FAFT unpacked the shellball
112        and return path to extracted bios.bin.
113
114        Returns:
115            Full path of bios.bin on DUT.
116        """
117
118        work_path = self.faft_client.updater.get_work_path()
119        bios_relative_path = self.faft_client.updater.get_bios_relative_path()
120        bios_bin = os.path.join(work_path, bios_relative_path)
121        return bios_bin
122
123    def dut_prep_cbfs(self):
124        """Sets up cbfs on DUT.
125
126        Finds bios.bin on the DUT and sets up a temp dir to operate on
127        bios.bin.  If a bios.bin was specified, it is copied to the DUT
128        and used instead of the native bios.bin.
129        """
130
131        cbfs_path = self.faft_client.updater.cbfs_setup_work_dir()
132        bios_relative_path = self.faft_client.updater.get_bios_relative_path()
133        self.cbfs_work_dir = cbfs_path
134        self.dut_bios_path = os.path.join(cbfs_path, bios_relative_path)
135
136    def dut_cbfs_extract_chips(self, chip_types):
137        """Extracts firmware hash blobs from cbfs.
138
139        Iterates over requested chip types and looks for corresponding
140        firmware hash blobs in cbfs.  These firmware hash blobs are
141        extracted into cbfs_work_dir.
142
143        Args:
144            chip_types:
145                A set of chip types for which the hash blobs will be
146                extracted.
147
148        Returns:
149            A dict mapping found chip names to chip instances.
150        """
151
152        cbfs_chip_info = {}
153        for chip_type in chip_types:
154            chip = chip_type()
155            fw = chip.fw_name
156            if not fw:
157                # must be an unfamiliar chip
158                continue
159
160            if not self.faft_client.updater.cbfs_extract_chip(chip.fw_name):
161                logging.warning('%s firmware not bundled in %s',
162                                chip.chip_name, self.BIOS)
163                continue
164
165            hashblob = self.faft_client.updater.cbfs_get_chip_hash(
166                chip.fw_name)
167            if not hashblob:
168                logging.warning('%s firmware hash not extracted from %s',
169                                chip.chip_name, self.BIOS)
170                continue
171
172            bundled_fw_ver = chip.fw_ver_from_hash(hashblob)
173            if not bundled_fw_ver:
174                raise error.TestFail(
175                    'could not decode %s firmware hash: %s' % (
176                        chip.chip_name, hashblob))
177
178            chip.set_fw_ver_from_string(bundled_fw_ver)
179            cbfs_chip_info[chip.chip_name] = chip
180            logging.info('%s bundled firmware for %s is version %s',
181                         self.BIOS, chip.chip_name, bundled_fw_ver)
182        return cbfs_chip_info
183
184    def check_chip_versions(self, port2chip, ref_chip_info):
185        """Verifies DUT chips have expected firmware.
186
187        Iterates over found DUT chips and verifies their firmware version
188        matches the chips found in the reference ref_chip_info map.
189
190        Args:
191            port2chip: A list of chips to verify against ref_chip_info.
192            ref_chip_info: A dict of reference chip chip instances indexed
193                by chip name.
194        """
195
196        for p, pinfo in enumerate(port2chip):
197            if not pinfo.fw_ver:
198                # must be an unknown chip
199                continue
200            msg = 'DUT port %s is a %s running firmware 0x%02x' % (
201                p, pinfo.chip_name, pinfo.fw_ver)
202            if pinfo.chip_name not in ref_chip_info:
203                logging.warning('%s but there is no reference version', msg)
204                continue
205            expected_fw_ver = ref_chip_info[pinfo.chip_name].fw_ver
206            logging.info('%s%s', msg,
207                         ('' if pinfo.fw_ver == expected_fw_ver else
208                          ' (expected 0x%02x)' % expected_fw_ver))
209
210            if pinfo.fw_ver != expected_fw_ver:
211                msg = '%s firmware was not updated to 0x%02x' % (
212                    pinfo.chip_name, expected_fw_ver)
213                raise error.TestFail(msg)
214
215    def run_once(self, host):
216        # Make sure the client library is on the device so that the proxy
217        # code is there when we try to call it.
218
219        (dut_chip_types, dut_chips) = self.dut_scan_chips()
220        if not dut_chip_types:
221            logging.info('mosys reported no chips on DUT, skipping test')
222            return
223
224        self.dut_prep_cbfs()
225        if self.new_bios_path:
226            host.send_file(self.new_bios_path, self.dut_bios_path)
227
228        ref_chip_info = self.dut_cbfs_extract_chips(dut_chip_types)
229        self.check_chip_versions(dut_chips, ref_chip_info)
230