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(
161                    chip.fw_name, chip.extension, chip.hash_extension):
162                logging.warning('%s firmware not bundled in %s',
163                                chip.chip_name, self.BIOS)
164                continue
165
166            hashblob = self.faft_client.updater.cbfs_get_chip_hash(
167                    chip.fw_name, chip.hash_extension)
168            if not hashblob:
169                logging.warning('%s firmware hash not extracted from %s',
170                                chip.chip_name, self.BIOS)
171                continue
172
173            bundled_fw_ver = chip.fw_ver_from_hash(hashblob)
174            if not bundled_fw_ver:
175                raise error.TestFail(
176                    'could not decode %s firmware hash: %s' % (
177                        chip.chip_name, hashblob))
178
179            chip.set_fw_ver_from_string(bundled_fw_ver)
180            cbfs_chip_info[chip.chip_name] = chip
181            logging.info('%s bundled firmware for %s is version %s',
182                         self.BIOS, chip.chip_name, bundled_fw_ver)
183        return cbfs_chip_info
184
185    def check_chip_versions(self, port2chip, ref_chip_info):
186        """Verifies DUT chips have expected firmware.
187
188        Iterates over found DUT chips and verifies their firmware version
189        matches the chips found in the reference ref_chip_info map.
190
191        Args:
192            port2chip: A list of chips to verify against ref_chip_info.
193            ref_chip_info: A dict of reference chip chip instances indexed
194                by chip name.
195        """
196
197        for p, pinfo in enumerate(port2chip):
198            if not pinfo.fw_ver:
199                # must be an unknown chip
200                continue
201            msg = 'DUT port %s is a %s running firmware 0x%02x' % (
202                p, pinfo.chip_name, pinfo.fw_ver)
203            if pinfo.chip_name not in ref_chip_info:
204                logging.warning('%s but there is no reference version', msg)
205                continue
206            expected_fw_ver = ref_chip_info[pinfo.chip_name].fw_ver
207            logging.info('%s%s', msg,
208                         ('' if pinfo.fw_ver == expected_fw_ver else
209                          ' (expected 0x%02x)' % expected_fw_ver))
210
211            if pinfo.fw_ver != expected_fw_ver:
212                msg = '%s firmware was not updated to 0x%02x' % (
213                    pinfo.chip_name, expected_fw_ver)
214                raise error.TestFail(msg)
215
216    def run_once(self, host):
217        """Runs a single iteration of the test."""
218        # Make sure the client library is on the device so that the proxy
219        # code is there when we try to call it.
220
221        (dut_chip_types, dut_chips) = self.dut_scan_chips()
222        if not dut_chip_types:
223            logging.info('mosys reported no chips on DUT, skipping test')
224            return
225
226        self.dut_prep_cbfs()
227        if self.new_bios_path:
228            host.send_file(self.new_bios_path, self.dut_bios_path)
229
230        ref_chip_info = self.dut_cbfs_extract_chips(dut_chip_types)
231        self.check_chip_versions(dut_chips, ref_chip_info)
232