faft-cr50-experimental: add firmware_Cr50PartialBoardId test

Cr50 allows setting the RLZ if only the board id flags are set. This
adds a test to verify the behavior.

BUG=b:143649068
TEST=run on octopus with 4.24

Change-Id: I8f8c034b7199181787b0ba867060ed0bffa20fe8
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/autotest/+/1949013
Tested-by: Mary Ruthven <mruthven@chromium.org>
Reviewed-by: Dana Goyette <dgoyette@chromium.org>
diff --git a/client/common_lib/cros/cr50_utils.py b/client/common_lib/cros/cr50_utils.py
index 67bf9ec..5e96297 100644
--- a/client/common_lib/cros/cr50_utils.py
+++ b/client/common_lib/cros/cr50_utils.py
@@ -75,6 +75,7 @@
 UPDATE_OK = 1
 
 ERASED_BID_INT = 0xffffffff
+ERASED_BID_STR = hex(ERASED_BID_INT)
 # With an erased bid, the flags and board id will both be erased
 ERASED_CHIP_BID = (ERASED_BID_INT, ERASED_BID_INT, ERASED_BID_INT)
 # Any image with this board id will run on any device
diff --git a/server/site_tests/firmware_Cr50PartialBoardId/control b/server/site_tests/firmware_Cr50PartialBoardId/control
new file mode 100644
index 0000000..a300df0
--- /dev/null
+++ b/server/site_tests/firmware_Cr50PartialBoardId/control
@@ -0,0 +1,37 @@
+# Copyright 2019 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.
+
+from autotest_lib.server import utils
+
+AUTHOR = "mruthven"
+NAME = "firmware_Cr50PartialBoardId"
+PURPOSE = "Verify the board id flags can be set then the board id type."
+ATTRIBUTES = "suite:faft_cr50_experimental"
+TIME = "MEDIUM"
+TEST_TYPE = "server"
+DEPENDENCIES = "servo"
+
+DOC = """
+Verify Cr50 handles partial board ids correctly. If only the flags are set, the
+board id type can be set.
+
+@param iterations: Number of times to run the test
+@param cr50_dbg_image_path: local path for the DBG image
+"""
+
+if 'args_dict' not in locals():
+    args_dict = {}
+
+args_dict.update(utils.args_to_dict(args))
+servo_args = hosts.CrosHost.get_servo_arguments(args_dict)
+
+def run(machine):
+    host = hosts.create_host(machine, servo_args=servo_args)
+
+    iterations = int(args_dict.get("iterations", 1))
+
+    job.run_test("firmware_Cr50PartialBoardId", host=host, cmdline_args=args,
+                 full_args=args_dict, iterations=iterations)
+
+parallel_simple(run, machines)
diff --git a/server/site_tests/firmware_Cr50PartialBoardId/firmware_Cr50PartialBoardId.py b/server/site_tests/firmware_Cr50PartialBoardId/firmware_Cr50PartialBoardId.py
new file mode 100644
index 0000000..e0c4b8e
--- /dev/null
+++ b/server/site_tests/firmware_Cr50PartialBoardId/firmware_Cr50PartialBoardId.py
@@ -0,0 +1,165 @@
+# Copyright 2019 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 logging
+
+from autotest_lib.client.common_lib import error
+from autotest_lib.client.common_lib.cros import cr50_utils
+from autotest_lib.server.cros.faft.cr50_test import Cr50Test
+
+
+class firmware_Cr50PartialBoardId(Cr50Test):
+    """Verify cr50 partial board id.
+
+
+    Verify the board id flags can be set alone and the brand can be set later.
+    """
+    version = 1
+
+    # Brand used for testing. It doesn't matter what this is.
+    TEST_BRAND = 'ZZAF'
+
+    WHITELABEL_FLAGS = 0x3f80
+    OTHER_FLAGS = 0x7f7f
+
+    SUCCESS = ''
+    ERR_ALREADY_SET = 'Error 7 while setting board id'
+    ERR_BID_MISMATCH = 'Error 5 while setting board id'
+
+
+    def initialize(self, host, cmdline_args, full_args, bid=''):
+        """Generate the test flags and verify the device setup."""
+        # Restore the original image, rlz code, and board id during cleanup.
+        super(firmware_Cr50PartialBoardId, self).initialize(
+            host, cmdline_args, full_args, restore_cr50_image=True,
+            restore_cr50_board_id=True)
+        if self.servo.main_device_is_ccd():
+            raise error.TestNAError('Use a flex cable instead of CCD cable.')
+
+        running_ver = self.get_saved_cr50_original_version()
+        logging.info('Cr50 Version: %s', running_ver)
+        bid = running_ver[2]
+        logging.info('Test Brand: %r', self.TEST_BRAND)
+        self.image_flags = int(bid.rsplit(':', 1)[-1], 16) if bid else 0
+        # The image may have non-zero flags. Use test flags as close to the
+        # whitelabel flags as possible, but make sure they can be used with
+        # the running image.
+        self.test_flags = self.WHITELABEL_FLAGS | self.image_flags
+        self.other_flags = self.OTHER_FLAGS | self.image_flags
+
+
+    def eraseflashinfo(self):
+        """Eraseflashinfo if the board id is set."""
+        if cr50_utils.GetChipBoardId(self.host) == cr50_utils.ERASED_CHIP_BID:
+            return
+        # Erase the board id so we can change it.
+        self.eraseflashinfo_and_restore_image()
+
+
+    def set_bid_with_dbg_image(self, bid):
+        """Use the console command on the DBG image to set the board id."""
+        self.eraseflashinfo_and_restore_image(self.get_saved_dbg_image_path())
+        self.cr50.set_board_id(int(bid[0], 16), bid[2])
+        self.cr50_update(self.get_saved_cr50_original_path(), rollback=True)
+
+
+    @staticmethod
+    def get_bid_str(bid):
+        """Returns a string representation of the board id tuple."""
+        bid_str_fields = []
+        for field in bid:
+            if isinstance(field, str):
+                bid_str_fields.append(field)
+            elif isinstance(field, int):
+                bid_str_fields.append(hex(field))
+            else:
+                bid_str_fields.append(str(field))
+        return ':'.join(bid_str_fields)
+
+
+    def set_board_id_check_response(self, set_bid, expected_bid, expected_msg):
+        """Try to set the board id and verify the response.
+
+        @param set_bid: a tuple of the board id to set (brand, brand_inv,
+                        flags). brand_inv is ignored, but it's included to be
+                        consistent with expected_bid. The flags should be an
+                        integer.
+        @param expected_bid: a tuple of the board id cr50 should have (brand,
+                             brand_inv, flags). brand_inv is ignored if it's
+                             None. The flags should be an integer.
+        @param expected_msg: The expected response from setting the board id
+                             with gsctool.
+        @raises TestFail if the cr50 board id doesn't match expected_bid or
+                the gsctool doesn't contain expected_msg.
+        """
+        logging.info('Set BID %s.', self.get_bid_str(set_bid))
+        logging.info('Expect BID %s (%s)', self.get_bid_str(expected_bid),
+                     expected_msg)
+        board_id_arg = '%s:0x%08x' % (set_bid[0], set_bid[2])
+
+        result = cr50_utils.GSCTool(self.host, ['-a', '-i', board_id_arg],
+                                    ignore_status=True)
+
+        stderr = result.stderr.strip()
+        result_msg = stderr if stderr else result.stdout.strip()
+        logging.info('Response: BID %s %s', board_id_arg,
+                     ('(%s)' % result_msg) if result_msg else '')
+        if expected_msg and expected_msg not in result_msg:
+            err = ('Unexpected response setting %r (%d): got %r expected %r' %
+                   (board_id_arg, result.exit_status, result_msg,
+                    expected_msg))
+            raise error.TestFail(err)
+        cr50_utils.CheckChipBoardId(self.host, expected_bid[0], expected_bid[2],
+                                    board_id_inv=expected_bid[1])
+
+
+    def run_once(self):
+        """Verify partial board id"""
+        self.eraseflashinfo()
+        # Basic check. Setting board id fails if it's already been fully set.
+        bid = (self.TEST_BRAND, None, self.test_flags)
+        self.set_board_id_check_response(bid, bid, self.SUCCESS)
+        self.set_board_id_check_response(bid, bid, self.ERR_ALREADY_SET)
+
+        self.eraseflashinfo()
+        # No special behavior for flags that are 0xffffffff. The flags cannot
+        # be changed if the board id is set even if the flags are 0xffffffff.
+        original_bid = (self.TEST_BRAND, None, cr50_utils.ERASED_BID_INT)
+        second_bid = (self.TEST_BRAND, None, self.test_flags)
+        self.set_board_id_check_response(original_bid, original_bid,
+                                         self.SUCCESS)
+        self.set_board_id_check_response(second_bid, original_bid,
+                                         self.ERR_ALREADY_SET)
+
+        self.eraseflashinfo()
+        # Flags can be set if board_id_type and board_id_type_inv are 0xffffffff
+        partial_bid = (cr50_utils.ERASED_BID_STR, None, self.test_flags)
+        stored_partial_bid = (cr50_utils.ERASED_BID_STR,
+                              cr50_utils.ERASED_BID_STR, self.test_flags)
+        self.set_board_id_check_response(partial_bid, stored_partial_bid,
+                                         self.SUCCESS)
+        set_brand = (self.TEST_BRAND, None, self.other_flags)
+        updated_brand_bid = (self.TEST_BRAND, None, self.test_flags)
+        self.set_board_id_check_response(set_brand, updated_brand_bid,
+                                         self.SUCCESS)
+
+        # Setting the board id type to 0xffffffff on the console will set the
+        # type to 0xffffffff and type_inv to 0. This isn't considered a partial
+        # board id. Setting the board id a second time will fail.
+        bid = (cr50_utils.ERASED_BID_STR, '00000000', self.test_flags)
+        new_bid = (self.TEST_BRAND, None, self.other_flags)
+        self.set_bid_with_dbg_image(bid)
+        self.set_board_id_check_response(new_bid, bid, self.ERR_ALREADY_SET)
+        if not self.image_flags:
+            logging.info('Image is not board id locked. Done')
+            return
+
+        self.eraseflashinfo()
+        # Plain whitelabel flags will run on any board id locked image.
+        bid = (cr50_utils.ERASED_BID_STR, None, self.WHITELABEL_FLAGS)
+        self.set_board_id_check_response(bid, cr50_utils.ERASED_CHIP_BID,
+                                         self.ERR_BID_MISMATCH)
+        # Previous board id was rejected. The board id can still be set.
+        basic_bid = (self.TEST_BRAND, None, self.image_flags)
+        self.set_board_id_check_response(basic_bid, basic_bid, self.SUCCESS)