# 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 import operator import re import sys import xmlrpclib from autotest_lib.client.common_lib import error from autotest_lib.client.common_lib.cros import chip_utils from autotest_lib.server.cros.faft.firmware_test import FirmwareTest NO_ARGS = tuple() ONE_INT_ARG = (1, ) ONE_STR_ARG = ("foo", ) SAMPLE_FILE = "/tmp/foo" CHIP_FW_NAMES = (chip.fw_name for chip in chip_utils.chip_id_map.itervalues()) SAMPLE_CGPT_A = { "UUID": "93EF7B23-606B-014B-A10C-E9D7CF53DFD3", "successful": 1, "partition": 2, "priority": 1, "tries": 0, "Type": "ChromeOS kernel", } SAMPLE_CGPT_B = { "UUID": "C6604D6B-5563-EE4E-9915-0C50530B158A", "successful": 0, "partition": 4, "priority": 0, "tries": 15, "Type": "ChromeOS kernel", } # RPC_CATEGORIES contains the test cases for all RPCs. # For readability, the real definition is at the bottom of this file. RPC_CATEGORIES = [{}] def get_rpc_category_by_name(name): """Find a category from RPC_CATEGORIES by its category_name.""" for rpc_category in RPC_CATEGORIES: if rpc_category["category_name"] == name: return rpc_category raise ValueError("No RPC category defined with category_name=%s" % name) def get_rpc_method_names_from_test_case(test_case): """ Extract the method_name or method_names from a test case configuration. @param test_case: An element from a test_cases array, like those in RPC_CATEGORIES @return: A list of names of RPC methods in that test case. """ if (("method_name" in test_case) ^ ("method_names" in test_case)): if "method_name" in test_case: return [test_case["method_name"]] elif "method_names" in test_case: return test_case["method_names"] else: err_msg = "Something strange happened while parsing RPC methods" raise ValueError(err_msg) else: err_msg = "test_case must contain EITHER method_name OR method_names" raise ValueError(err_msg) class firmware_FAFTRPC(FirmwareTest): """ This test checks that all RPC commands work as intended. For now, we only need to verify that the RPC framework is intact, so we only verify that all RPCs can be called with the expected arguments. It would be good to expand this test to verify that all RPCs yields the expected results. """ version = 1 _stored_values = {} def initialize(self, host, cmdline_args, dev_mode=False): """Runs before test begins.""" super(firmware_FAFTRPC, self).initialize(host, cmdline_args) self.backup_firmware() self.faft_client.rpc_settings.enable_test_mode() def cleanup(self): """Runs after test completion.""" self.faft_client.rpc_settings.disable_test_mode() try: if self.is_firmware_saved(): self.restore_firmware() if self.reboot_after_completion: logging.info("Rebooting DUT, as specified in control file") self.switcher.mode_aware_reboot() except Exception as e: logging.error("Caught exception: %s", str(e)) super(firmware_FAFTRPC, self).cleanup() def _log_success(self, rpc_name, params, success_message): """Report on an info level that a test passed.""" logging.info("RPC test for %s%s successfully %s", rpc_name, params, success_message) def _fail(self, rpc_name, params, error_msg): """Raise a TestFail error explaining why a test failed.""" raise error.TestFail("RPC function %s%s had an unexpected result: %s" % (rpc_name, params, error_msg)) def _retrieve_stored_values(self, params): """ Replace any operator.itemgetter params with corresponding stored values. @param params: A tuple of args that might be passed into an RPC method, some of which might be operator.itemgetter objects. @return: A tuple of pargs to be passed into an RPC method, with stored values swapped in for operator.itemgetters. """ new_params = [] for old_param in params: if isinstance(old_param, operator.itemgetter): retrieved_value = old_param(self._stored_values) new_params.append(retrieved_value) else: new_params.append(old_param) new_params = tuple(new_params) return new_params def _assert_passes(self, category, method, params, allow_error_msg=None, expected_return_type=None, silence_result=False): """ Check whether an RPC function with given input passes, and fail if it does not. If an expected_return_type is passed in, then require the RPC function to return a value matching that type, or else fail. @param category: The RPC subsystem category; ex. kernel, bios @param method: The name of the RPC function within the subsystem @param params: A tuple containing params to pass into the RPC function @param allow_error_msg: If a regexy string is passed in, and the RPC returns an RPC error matching this regex, then the test will pass instead of failing. @param expected_return_type: If not None, then the RPC return value must be this type, else the test fails. @param silence_result: If True, then the RPC return value will not be logged. @raise error.TestFail: If the RPC raises any error (unless handled by allow_error_msg). @raise error.TestFail: If expected_return_type is not None, and the RPC return value is not expected_return_type. @return: Not meaningful. """ rpc_function = self.get_rpc_function(category, method) if category: rpc_name = '%s.%s' % (category, method) else: rpc_name = method try: result = rpc_function(*params) except xmlrpclib.Fault as e: if allow_error_msg is not None and \ re.search(allow_error_msg, str(e)): success_msg = "raised an acceptable error during RPC handling" self._log_success(rpc_name, params, success_msg) return e error_msg = "Unexpected RPC error: %s" % e self._fail(rpc_name, params, error_msg) except: error_msg = "Unexpected misc error: %s" % sys.exc_info()[0] self._fail(rpc_name, params, error_msg) else: if expected_return_type is None: if silence_result: success_msg = "passed with a silenced result" else: success_msg = "passed with result %s" % result self._log_success(rpc_name, params, success_msg) return result elif isinstance(result, expected_return_type): if silence_result: success_msg = "passed with a silenced result of " \ "expected type %s" % type(result) else: success_msg = "passed with result %s of expected type %s" \ % (result, type(result)) self._log_success(rpc_name, params, success_msg) return result else: error_msg = ("Expected a result of type %s, but got %s " + "of type %s)") \ % (expected_return_type, result, type(result)) self._fail(rpc_name, params, error_msg) def _assert_fails(self, category, method, params): """ Check whether an RPC function with given input throws an RPC error, and fail if it does not. @param category: The RPC subsystem category; ex. kernel, bios @param method: The name of the RPC function within the subsystem @param params: A tuple containing params to pass into the RPC function @raise error.TestFail: If the RPC raises no error, or if it raises any error other than xmlrpclib.Fault @return: Not meaningful. """ rpc_function = self.get_rpc_function(category, method) if category: rpc_name = '%s.%s' % (category, method) else: rpc_name = method try: result = rpc_function(*params) except xmlrpclib.Fault as e: self._log_success(rpc_name, params, "raised RPC error") except: error_msg = "Unexpected misc error: %s" % sys.exc_info()[0] self._fail(rpc_name, params, error_msg) else: error_msg = "Should have raised an RPC error, but did not" self._fail(rpc_name, params, error_msg) def _assert_output(self, category, method, params, expected_output, allow_error_msg=None): """ Check whether an RPC function with given input returns a particular value, and fail if it does not. @param category: The RPC subsystem category; ex. kernel, bios @param method: The name of the RPC function within the subsystem @param params: A tuple containing params to pass into the RPC function @param expected_output: The value that the RPC function should return @param allow_error_msg: If a regexy string is passed in, and the RPC returns an RPC error containing this string, then the test will pass instead of failing. @raise error.TestFail: If self._assert_passes(...) fails, or if the RPC return value does not match expected_output @return: Not meaningful. """ rpc_name = ".".join([category, method]) actual_output = self._assert_passes(category, method, params, allow_error_msg=allow_error_msg) if expected_output == actual_output: success_message = "returned the expected value <%s>" \ % expected_output self._log_success(rpc_name, params, success_message) else: error_msg = "Expected output <%s>, but actually returned <%s>" \ % (expected_output, actual_output) self._fail(rpc_name, params, error_msg) def get_rpc_function(self, category, method): """ Find a callable RPC function given its name. @param category: The name of an RPC subsystem category; ex. kernel, ec @param method: The name of an RPC function within the subsystem @return: A callable method of the RPC proxy """ if category: rpc_function_handler = getattr(self.faft_client, category) else: rpc_function_handler = self.faft_client rpc_function = getattr(rpc_function_handler, method) return rpc_function def run_once(self, category_under_test="*", reboot_after_completion=False): """ Main test logic. For all RPC categories being tested, iterate through all test cases defined in RPC_CATEGORIES. @param category_under_test: The name of an RPC category to be tested, such as ec, bios, or kernel. Default is '*', which tests all categories. """ if category_under_test == "*": logging.info("Testing all %d RPC categories", len(RPC_CATEGORIES)) rpc_categories_to_test = RPC_CATEGORIES else: rpc_categories_to_test = [ get_rpc_category_by_name(category_under_test)] logging.info("Testing RPC category '%s'", category_under_test) self.reboot_after_completion = reboot_after_completion for rpc_category in rpc_categories_to_test: category_name = rpc_category["category_name"] if category_name == "ec" and not self.check_ec_capability(): logging.info("No EC found on DUT. Skipping EC category.") continue # Re-enable test mode, in case another category's tests disabled it. self.faft_client.rpc_settings.enable_test_mode() test_cases = rpc_category["test_cases"] logging.info("Testing %d cases for RPC category %s", len(test_cases), repr(category_name)) for test_case in test_cases: method_names = get_rpc_method_names_from_test_case(test_case) passing_args = test_case.get("passing_args", []) failing_args = test_case.get("failing_args", []) allow_error_msg = test_case.get("allow_error_msg", None) expected_return_type = test_case.get("expected_return_type", None) store_result_as = test_case.get("store_result_as", None) silence_result = test_case.get("silence_result", False) for method_name in method_names: for passing_arg_tuple in passing_args: passing_arg_tuple = self._retrieve_stored_values( passing_arg_tuple) result = self._assert_passes(category_name, method_name, passing_arg_tuple, allow_error_msg, expected_return_type, silence_result) if store_result_as is not None: self._stored_values[store_result_as] = result for failing_arg_tuple in failing_args: failing_arg_tuple = self._retrieve_stored_values( failing_arg_tuple) self._assert_fails(category_name, method_name, failing_arg_tuple) """ RPC_CATEGORIES contains all the test cases for our RPC tests. Each element of RPC_CATEGORIES must be a dict containing the following keys: @key category_name: A string naming the RPC category, such as bios or kernel. @key test_cases: A list of test cases, each of which must be a dict containing the following keys: @key method_name (optional): A string naming an RPC method within this category. Either this key or method_names is required (but not both). @key method_names (optional): An array of strings naming RPC methods within this category. Either this key or method_name is required (but not both). @key passing_args: A list of tuples, each of which could be unpacked and then passed into the RPC method as a valid set of parameters. Each tuple might contain instances of operator.itemgetter. If so, those instances will be replaced with values from firmware_FAFTRPC._stored_values before being passed into the RPC method. @key failing_args: A list of tuples, each of which could be unpacked and then passed into the RPC method as a set of parameters which should yield an RPC error. Each tuple might contain instances of operator.itemgetter. If so, those instances will be replaced with values from firmware_FAFTRPC._stored_values before being passed into the RPC method. @key silence_result: Normally, the RPC return value is logged. However, if this key is truthy, then the result is not logged. @key allow_error_msg (optional): String representing a regex pattern. If the RPC method is called with a passing_args tuple, but it yields an RPC error whose message is matched by re.search(allow_error_msg, error_msg), then the test will be considered a pass. @key store_result_as (optional): String. If this field is specified, then the result from the RPC call will be stored in firmware_FAFTRPC._stored_values. This allows us to later reference the result via an operator.itemgetter, as described above in the docstrings for passing_args and failing_args. """ RPC_CATEGORIES = [ { "category_name": "system", "test_cases": [ { "method_names": [ "is_available", "get_platform_name", "get_model_name", "dev_tpm_present", "get_root_dev", "get_root_part", "get_fw_vboot2", "request_recovery_boot", "is_removable_device_boot", "get_internal_device", ], "passing_args": [NO_ARGS], "failing_args": [ONE_INT_ARG, ONE_STR_ARG], }, { "method_name": "dump_log", "passing_args": [ NO_ARGS, (True, ), (False, ), ], "failing_args": [ (True, False), ], "expected_return_type": str, "silence_result": True, }, { "method_name": "run_shell_command", "passing_args": [("ls -l", ), ("ls -l", False), ("ls -l", True)], "failing_args": [ NO_ARGS, ("ls", "-l", 'foo'), ], }, { "method_name": "run_shell_command_get_status", "passing_args": [ ("ls", ), ], "failing_args": [ NO_ARGS, ("ls", "-l", 'foo'), ], }, { "method_name": "run_shell_command_get_status", "passing_args": [ ("ls ''", ), ], }, { "method_name": "run_shell_command", "failing_args": [ ("ls ''", ), ], }, { "method_name": "run_shell_command_check_output", "passing_args": [ ("ls -l", "total"), ], "failing_args": [ NO_ARGS, ], }, { "method_name": "run_shell_command_get_output", "passing_args": [ ("ls -l", True), ], "failing_args": [ NO_ARGS, ], }, { "method_name": "get_crossystem_value", "passing_args": [ ("fwid", ), ], "failing_args": [NO_ARGS], }, { "method_name": "set_try_fw_b", "passing_args": [ NO_ARGS, (1, ), ], "failing_args": [ (1, 1), ], }, { "method_name": "set_fw_try_next", "passing_args": [ ("A", ), ("A", 1), ], "failing_args": [ NO_ARGS, ("A", 1, "B"), ], }, { "method_name": "get_dev_boot_usb", "passing_args": [NO_ARGS], "failing_args": [ONE_INT_ARG, ONE_STR_ARG], "store_result_as": "dev_boot_usb", }, { "method_name": "set_dev_boot_usb", "passing_args": [ (operator.itemgetter("dev_boot_usb"), ), ], "failing_args": [ NO_ARGS, (True, False), ], }, { "method_name": "create_temp_dir", "passing_args": [ NO_ARGS, ONE_STR_ARG, ], "failing_args": [ ONE_INT_ARG, ("foo", "bar"), ], "expected_return_type": str, "store_result_as": "temp_dir", }, { "method_name": "remove_file", "passing_args": [ (SAMPLE_FILE, ), ], "failing_args": [ NO_ARGS, (1, 2), ], }, { "method_name": "remove_dir", "passing_args": [ (operator.itemgetter("temp_dir"), ), ], "failing_args": [ NO_ARGS, (1, 2), ] }, { "method_name": "check_keys", "passing_args": [ ([], ), ([116], ), ([28, 29, 32], ), ], "failing_args": [ NO_ARGS, ([], [116]), ], "expected_return_type": int, }, ] }, { "category_name": "bios", "test_cases": [ { "method_names": [ "reload", ], "passing_args": [NO_ARGS], "failing_args": [ONE_INT_ARG, ONE_STR_ARG] }, { "method_name": "get_gbb_flags", "passing_args": [NO_ARGS], "failing_args": [ONE_INT_ARG, ONE_STR_ARG], "expected_return_type": int, "store_result_as": "gbb_flags", }, { "method_name": "set_gbb_flags", "passing_args": [ (operator.itemgetter("gbb_flags"), ), ], "failing_args": [NO_ARGS], }, { "method_name": "get_preamble_flags", "passing_args": [ ("a", ), ], "failing_args": [NO_ARGS, ONE_INT_ARG], "store_result_as": "preamble_flags", }, { "method_name": "set_preamble_flags", "passing_args": [ ( "a", operator.itemgetter( "preamble_flags"), ), ], "failing_args": [ NO_ARGS, ONE_INT_ARG, ONE_STR_ARG, ( "c", operator.itemgetter( "preamble_flags"), ), ], }, { "method_names": [ "get_body_sha", "get_sig_sha", "get_section_fwid", "get_version", "get_datakey_version", "get_kernel_subkey_version", ], "passing_args": [ ("a", ), ("b", ), ], "failing_args": [ NO_ARGS, ONE_INT_ARG, (("a", "b"), ), ("c", ), ] }, { "method_names": [ "corrupt_sig", "restore_sig", "corrupt_body", "restore_body", "move_version_backward", "move_version_forward", ], "passing_args": [ ("a", ), ("b", ), ], "failing_args": [ NO_ARGS, ONE_INT_ARG, ("c", ), ] }, { "method_names": [ "dump_whole", "write_whole", ], "passing_args": [ (SAMPLE_FILE, ), ], "failing_args": [NO_ARGS], }, { "method_name": "strip_modified_fwids", "passing_args": [NO_ARGS], "failing_args": [ONE_INT_ARG, ONE_STR_ARG], "expected_return_type": dict }, { "method_name": "set_write_protect_region", "passing_args": [("WP_RO", ), ("WP_RO", None), ("WP_RO", True), ("WP_RO", False)], "failing_args": [NO_ARGS, (None, ), ("WP_RO", None, "EXTRA")], }, { "method_name": "get_write_protect_status", "passing_args": [NO_ARGS], "failing_args": [ONE_INT_ARG, ONE_STR_ARG], "expected_return_type": dict }, { "method_name": "get_write_cmd", "passing_args": [ NO_ARGS, (""), ("bios.bin", ), ], "failing_args": [("bios.bin", []), ("bios.bin", 1), ("bios.bin", [], 'extra')], "expected_return_type": str }, ], }, { "category_name": "ec", "test_cases": [ { "method_names": [ "reload", "get_version", "get_active_hash", "is_efs", ], "passing_args": [NO_ARGS], "failing_args": [ONE_INT_ARG, ONE_STR_ARG], "allow_error_msg": "list index out of range", }, { "method_names": ["dump_whole", "write_whole", "dump_firmware"], "passing_args": [ (SAMPLE_FILE, ), ], "failing_args": [NO_ARGS], }, { "method_name": "corrupt_body", "passing_args": [ ("rw", ), ], "failing_args": [ NO_ARGS, ONE_INT_ARG, ("ro", ), ("rw", "rw"), ], }, { "method_name": "set_write_protect", "passing_args": [ (True, ), (False, ), ], "failing_args": [ NO_ARGS, (True, False), ] }, { "method_name": "copy_rw", "passing_args": [ ("rw", "rw"), ], "failing_args": [ NO_ARGS, ("rw", "ro"), ("ro", "rw"), ("rw", ), ], }, { "method_name": "reboot_to_switch_slot", "passing_args": [NO_ARGS], "failing_args": [ONE_INT_ARG, ONE_STR_ARG], "allow_error_msg": "CmdError", }, { "method_name": "get_write_cmd", "passing_args": [ NO_ARGS, (""), ("ec.bin", ), ], "failing_args": [("ec.bin", []), ("ec.bin", 1), ("ec.bin", [], 'extra')], "expected_return_type": str }, ], }, { "category_name": "kernel", "test_cases": [{ "method_names": [ "corrupt_sig", "restore_sig", "move_version_backward", "move_version_forward", ], "passing_args": [ ("a", ), ("b", ), ], "failing_args": [ NO_ARGS, ONE_INT_ARG, ("c", ), ("a", "b"), ], }, { "method_names": [ "get_version", "get_datakey_version", "get_sha", ], "passing_args": [ ("a", ), ("b", ), ], "failing_args": [ (("a", "b"), ), ("c", ), NO_ARGS, ONE_INT_ARG, ], }, { "method_name": "diff_a_b", "passing_args": [NO_ARGS], "failing_args": [ ONE_INT_ARG, ONE_STR_ARG, ], "expected_return_type": bool, }, { "method_name": "resign_with_keys", "passing_args": [ ("a", ), ("b", ), ("b", SAMPLE_FILE), ], "failing_args": [ (("a", "b"), ), ("c", ), NO_ARGS, ONE_INT_ARG, ], }, { "method_names": [ "dump", "write", ], "passing_args": [ ("a", SAMPLE_FILE), ("b", SAMPLE_FILE), ], "failing_args": [ (("a", "b"), SAMPLE_FILE), ("c", SAMPLE_FILE), ("a", ), NO_ARGS, ] }], }, { "category_name": "tpm", "test_cases": [ { "method_names": [ "get_firmware_version", "get_firmware_datakey_version", "get_kernel_version", "get_kernel_datakey_version", "get_tpm_version", "stop_daemon", "restart_daemon", ], "passing_args": [NO_ARGS], "failing_args": [ONE_INT_ARG, ONE_STR_ARG], }, ] }, { "category_name": "cgpt", "test_cases": [{ "method_name": "get_attributes", "passing_args": [NO_ARGS], "failing_args": [ ONE_INT_ARG, ONE_STR_ARG, ], }, { "method_name": "set_attributes", "passing_args": [ NO_ARGS, (SAMPLE_CGPT_A, ), (None, SAMPLE_CGPT_B), (SAMPLE_CGPT_A, SAMPLE_CGPT_B), (None, None), ], "failing_args": [ (None, None, None), ], }] }, { "category_name": "updater", "test_cases": [ # TODO (gredelston): # Uncomment the methods which write to flash memory, # once we are able to set the firmware_updater to "emulate" mode. { "method_names": [ "cleanup", "stop_daemon", "start_daemon", # "modify_ecid_and_flash_to_bios", "get_ec_hash", "reset_shellball", # "run_factory_install", # "run_recovery", "cbfs_setup_work_dir", # "cbfs_sign_and_flash", "get_temp_path", "get_keys_path", "get_work_path", "get_bios_relative_path", "get_ec_relative_path", "get_ec_hash" ], "passing_args": [ NO_ARGS, ], "failing_args": [ ONE_INT_ARG, ONE_STR_ARG, ], "allow_error_msg": ("command cp -rf " "/usr/local/tmp/faft/autest/work " "/usr/local/tmp/faft/autest/cbfs failed|" "Could not detect a usable ec flash device") }, { "method_name": "get_section_fwid", "passing_args": [ NO_ARGS, ("bios", ), ("ec", ), ("bios", "b"), ("ec", "rw"), ], "failing_args": [ ("foo", ), ("bios", "foo"), ("ec", "foo"), ], "expected_return_type": str, "allow_error_msg": r"is empty|does not contain", }, { "method_names": [ "get_device_fwids", "get_image_fwids", ], "passing_args": [ NO_ARGS, ("bios", ), ("ec", ), ], "failing_args": [ ("foo", ), ], "expected_return_type": dict, "allow_error_msg": (r"is already modified|" r"is empty|" r"does not contain"), }, { "method_name": "modify_image_fwids", "passing_args": [ NO_ARGS, ("bios", ), ("ec", ), ("bios", ("b", "rec")), ("ec", ("rw_b", )), ], "failing_args": [ ("foo", ), ("bios", ("foo", )), ("ec", ("foo", )), ], "expected_return_type": dict, "allow_error_msg": (r"is already modified|" r"is empty|" r"does not contain"), }, { "method_name": "resign_firmware", "passing_args": [ ONE_INT_ARG, (None, ), ], "failing_args": [ NO_ARGS, ONE_STR_ARG, (1, 1), ], }, { "method_names": [ "repack_shellball", "extract_shellball", ], "passing_args": [ NO_ARGS, ("test", ), (None, ), ], "failing_args": [ ("foo", "bar"), ] }, { "method_name": "run_firmwareupdate", "passing_args": [ ("autoupdate", ), ("recovery", ), ("bootok", ), ("factory_install", ), ("bootok", None), ("bootok", "test"), ("bootok", "test", ()), ("bootok", "test", ("--noupdate_ec", "--wp=1")), ], "failing_args": [NO_ARGS], }, { "method_name": "get_firmwareupdate_command", "passing_args": [ ("autoupdate", ), ("recovery", ), ("factory_install", ), ], "failing_args": [NO_ARGS], "expected_return_type": str }, { "method_names": [ "run_autoupdate", "run_bootok", ], "passing_args": [ ("test", ), ], "failing_args": [ NO_ARGS, ("foo", "bar"), ], }, { "method_names": [ "cbfs_extract_chip", "cbfs_get_chip_hash", "cbfs_replace_chip", ], "passing_args": [(chip_fw_name, ) for chip_fw_name in CHIP_FW_NAMES], "failing_args": [ NO_ARGS, ONE_INT_ARG, ], "allow_error_msg": "cbfstool /usr/local/tmp/faft/" }, { "method_name": "copy_bios", "passing_args": [('/tmp/fake-bios.bin', )], "failing_args": [NO_ARGS, ('/tmp/fake-bios.bin', "foo")], "expected_return_type": str }, { "method_name": "get_image_gbb_flags", "passing_args": [NO_ARGS, ('/tmp/fake-bios.bin', )], "failing_args": [('/tmp/fake-bios.bin', 'bogus')], "store_result_as": "gbb_flags" }, { "method_name": "set_image_gbb_flags", "passing_args": [ (operator.itemgetter('gbb_flags'), ), (operator.itemgetter('gbb_flags'), '/tmp/fake-bios.bin'), ], "failing_args": [NO_ARGS, ('too', 'many', 'args')] } ] }, { "category_name": "rootfs", "test_cases": [ { "method_name": "verify_rootfs", "passing_args": [ ("A", ), ("B", ), ], "failing_args": [ NO_ARGS, ONE_INT_ARG, ("C", ), ("A", "B"), ], }, ] }, { "category_name": '', "test_cases": [ # explicit connect { "method_name": "quit", "passing_args": [NO_ARGS] }, { "method_name": "connect", "passing_args": [NO_ARGS] }, { "method_name": "ready", "passing_args": [NO_ARGS] }, { "method_name": "disconnect", "passing_args": [NO_ARGS] }, { "method_name": "connect", "passing_args": [NO_ARGS] }, { "method_name": "ready", "passing_args": [NO_ARGS] }, # implicit connect { "method_name": "quit", "passing_args": [NO_ARGS] }, { "method_name": "ready", "passing_args": [NO_ARGS] }, { "method_name": "disconnect", "passing_args": [NO_ARGS] }, { "method_name": "ready", "passing_args": [NO_ARGS] }, ] } ]