diff options
author | Steve Fung <stevefung@google.com> | 2016-04-18 02:33:27 -0700 |
---|---|---|
committer | Steve Fung <stevefung@google.com> | 2016-04-18 18:46:14 +0000 |
commit | b608a3dc11dfd9e9f6e82c52b162c30228e8fa72 (patch) | |
tree | d8dc2dba0e548d48306e3c5be407d7876d9c37a7 | |
parent | f17f2c66532aa2386f6c9fe9d8e9ab2810b0dafb (diff) | |
download | bdk-b608a3dc11dfd9e9f6e82c52b162c30228e8fa72.tar.gz |
Convert cli/ to 4 space indent
Convert all files in the cli/ folder to a 4 space indent
to comply with PEP8 style rules.
Bug: 28007659
Test: `python test_runner.py` passes.
Test: pylint passes.
Change-Id: I1820ae24afc85cecc65e8b3813dee023aa1682d3
-rw-r--r-- | cli/lib/cli/__init__.py | 2 | ||||
-rw-r--r-- | cli/lib/cli/clicommand.py | 428 | ||||
-rw-r--r-- | cli/lib/cli/clicommand_unittest.py | 298 | ||||
-rw-r--r-- | cli/lib/cli/climanager.py | 453 | ||||
-rw-r--r-- | cli/lib/cli/climanager_unittest.py | 570 | ||||
-rw-r--r-- | cli/lib/cli/importutils.py | 60 | ||||
-rw-r--r-- | cli/lib/cli/test_cli/__init__.py | 2 | ||||
-rw-r--r-- | cli/lib/cli/test_cli/commands.py | 54 |
8 files changed, 944 insertions, 923 deletions
diff --git a/cli/lib/cli/__init__.py b/cli/lib/cli/__init__.py index c5ce79b..419a8b6 100644 --- a/cli/lib/cli/__init__.py +++ b/cli/lib/cli/__init__.py @@ -14,4 +14,4 @@ # limitations under the License. # -"""Bdk CLI Package""" +"""Bdk CLI Package.""" diff --git a/cli/lib/cli/clicommand.py b/cli/lib/cli/clicommand.py index 1d25267..4fec8e3 100644 --- a/cli/lib/cli/clicommand.py +++ b/cli/lib/cli/clicommand.py @@ -29,239 +29,241 @@ from project import target class Error(error.Error): - pass + pass class SpecError(Error): - """Raised when there is a problem with a product spec.""" + """Raised when there is a problem with a product spec.""" class TargetError(Error): - """"Raised when an invalid target is given.""" + """"Raised when an invalid target is given.""" class ValidationError(Error): - """Raised when specified values are confirmed to be invalid.""" - description = 'Error validating project spec' + """Raised when specified values are confirmed to be invalid.""" + description = 'Error validating project spec' class NotDownloadedError(ValidationError): - """Raised when a specified target OS or BSP isn't downloaded.""" + """Raised when a specified target OS or BSP isn't downloaded.""" class CompatibilityError(ValidationError): - """Raised when a specified target OS and BSP aren't compatible.""" + """Raised when a specified target OS and BSP aren't compatible.""" # pylint: disable=abstract-class-not-used class Command(object): - """A general cli command.""" - - # Any subclass that wants to capture all remaining args should override - # this with the desired arg (name, description) tuple. Do not use - # argparse.REMAINDER directly as it has some unexpected behaviors. - remainder_arg = None - - @staticmethod - def Args(parser): - pass - - @staticmethod - def AddDefaultArgs(parser): - parser.add_argument('--debug', action='store_true', - help='Output debug logging.') - - @staticmethod - def AddProductArgs(parser, targeted=True): - parser.add_argument('--specification', default=util.FindProjectSpec(), - help='The project specification file to read from. ' - '(default {} in current or parent dirs)'.format( - util.PROJECT_SPEC_FILENAME)) - if targeted: - parser.add_argument('--target', help='The target to build the image of.') - - @classmethod - def set_group(cls, grp): - cls._group = grp - - @classmethod - def set_parser(cls, parser): - cls.parser = parser - - @classmethod - def group(cls): - try: - return cls._group - except AttributeError: - return None - - def __init__(self): - self._timer = timer.Timer(type(self).__name__) - - def ValidateTarget(self, target_, os=True, os_downloaded=True, - board=True, board_downloaded=True): - """Confirm that a target_ specifies valid board and os. - - If validating both os and board, also confirms that the two are - compatible. - - Args: - target_: The target_ to validate. - os: If True, validate that the desired os exists. - os_downloaded: If True, validate that the desired os is downloaded. - Ignored if os = False. - board: If True, validate that the desired board exists. - board_downloaded: If True, validate that the desired board is downloaded. - Ignored if board = False. - - Raises: - ValidationError: if a required OS or Board version is not installed. - """ - validation_message_start = '{}: Invalid target "{}": '.format( - target_.origin, target_.name) - if os: - if target_.os != 'brillo': - raise ValidationError(validation_message_start + - 'The BDK only supports "brillo" ' - 'as an operating system ' - '(requested "{}").'.format( - target_.os)) - if os_downloaded and target_.os_version != util.GetOSVersion(): - # TODO(b/27677026): Validate download status once OSes are - # downloaded like BSPs. Throw NotDownloadedError. - raise ValidationError(validation_message_start + - 'This version of the BDK only supports ' - 'Brillo version {} (requested {}).'.format( - util.GetOSVersion(), target_.os_version)) - if board: - try: - device = target_.get_device() - except target.Error as e: - raise ValidationError(validation_message_start + - '{}'.format(e)) - if board_downloaded and not device.is_available(): - raise NotDownloadedError(validation_message_start + - 'Support package for "{0}" is not downloaded. ' - 'Run `bdk bsp download {0}`.'.format( - target_.board)) - - if os and board: - # TODO(b/27654613): Check for compatibility. - compatible = True - if not compatible: - raise CompatibilityError('Requested OS version {} and board "{}.{}", ' - 'which are not compatible.'.format( - target_.os_version, target_.board, - target_.board_version)) - - def GetSpecAndTarget(self, args, **kwargs): - """Creates and verifies the spec and target_ to use. - - Args: - args: parsed arg namespace; spec and target_ are based on - args.specification and args.target. - **kwargs: args to pass to ValidateTarget(). - - Returns: - A tuple of (ProjectSpec, Target) objects. - - Raises: - SpecError: If no project spec can be found. - TargetError: If no target can be found. - ValidationError: If the target fails to validate. - """ - if not args.specification: - raise SpecError('No project spec could be found. Either run from a ' - 'directory containing a project spec named "{}" ' - '(or run from within a subdirectory thereof), ' - 'or specify a project spec file with ' - '`--specification`'.format(util.PROJECT_SPEC_FILENAME)) - try: - spec = project_spec.ProjectSpec.from_xml(args.specification) - except (IOError, common.LoadError) as e: - raise SpecError('Error parsing product spec {}: {}'.format( - args.specification, e)) - - try: - target_ = spec.get_target(args.target) - except KeyError as e: - raise TargetError(e) - - self.ValidateTarget(target_, **kwargs) - - return (spec, target_) - - def SetMetricsClass(self, class_name): - """Only necessary if you want to override the default metrics class. - - By default the class will be the command's class name. - - Args: - class_name: The new name for metrics to send. - """ - self._timer.name = class_name - - def SetMetricsLabel(self, label): - """Add an optional label to the metrics being sent by this command.""" - self._timer.label = label - - def RunWithMetrics(self, args): - """Wraps Run() with metrics collection and upload. - - This function times the call to Run(), and after completion - uploads metrics (if the user has opted in) containing the - command and timing data. - - Currently we upload metrics for all cases except KeyboardInterrupt, - in order to make Ctrl+C as responsive as possible. If we get async - metrics uploads going, it would probably be helpful to upload those - as well (http://b/27703295). - - Args: - args: list of args to pass to Run(). - - Returns: - Run() return value. - - Raises: - Passes up any exceptions thrown by Run(). - """ - result = None - try: - self._timer.Start() - result = self.Run(args) - except KeyboardInterrupt: - # Leave result as None to skip metrics upload for Ctrl+C. - raise - except error.Error as e: - result = e.errno - raise - except: - result = error.GENERIC_ERRNO - raise - finally: - if result is not None: - self._timer.Stop() - metrics_util.send_hit_and_retries( - timing.Timing.FromTimer(self._timer, result)) - - return result - - def Run(self, args): - """Called when the command is used. - - Args: - args: An argparse.Namespace of arguments for the command. - - Returns: - 0 if successful, an exit code otherwise. - """ - raise NotImplementedError('Run() must be overridden') + """A general cli command.""" + + # Any subclass that wants to capture all remaining args should override + # this with the desired arg (name, description) tuple. Do not use + # argparse.REMAINDER directly as it has some unexpected behaviors. + remainder_arg = None + + @staticmethod + def Args(parser): + pass + + @staticmethod + def AddDefaultArgs(parser): + parser.add_argument('--debug', action='store_true', + help='Output debug logging.') + + @staticmethod + def AddProductArgs(parser, targeted=True): + parser.add_argument('--specification', default=util.FindProjectSpec(), + help='The project specification file to read from. ' + '(default {} in current or parent dirs)'.format( + util.PROJECT_SPEC_FILENAME)) + if targeted: + parser.add_argument('--target', + help='The target to build the image of.') + + @classmethod + def set_group(cls, grp): + cls._group = grp + + @classmethod + def set_parser(cls, parser): + cls.parser = parser + + @classmethod + def group(cls): + try: + return cls._group + except AttributeError: + return None + + def __init__(self): + self._timer = timer.Timer(type(self).__name__) + + def ValidateTarget(self, target_, os=True, os_downloaded=True, + board=True, board_downloaded=True): + """Confirm that a target_ specifies valid board and os. + + If validating both os and board, also confirms that the two are + compatible. + + Args: + target_: The target_ to validate. + os: If True, validate that the desired os exists. + os_downloaded: If True, validate that the desired os is downloaded. + Ignored if os = False. + board: If True, validate that the desired board exists. + board_downloaded: If True, validate that the desired board is + downloaded. Ignored if board = False. + + Raises: + ValidationError: if a required OS or Board version is not installed. + """ + validation_message_start = '{}: Invalid target "{}": '.format( + target_.origin, target_.name) + if os: + if target_.os != 'brillo': + raise ValidationError(validation_message_start + + 'The BDK only supports "brillo" ' + 'as an operating system ' + '(requested "{}").'.format(target_.os)) + if os_downloaded and target_.os_version != util.GetOSVersion(): + # TODO(b/27677026): Validate download status once OSes are + # downloaded like BSPs. Throw NotDownloadedError. + raise ValidationError( + validation_message_start + + 'This version of the BDK only supports ' + 'Brillo version {} (requested {}).'.format( + util.GetOSVersion(), target_.os_version)) + if board: + try: + device = target_.get_device() + except target.Error as e: + raise ValidationError(validation_message_start + + '{}'.format(e)) + if board_downloaded and not device.is_available(): + raise NotDownloadedError( + validation_message_start + + 'Support package for "{0}" is not downloaded. ' + 'Run `bdk bsp download {0}`.'.format(target_.board)) + + if os and board: + # TODO(b/27654613): Check for compatibility. + compatible = True + if not compatible: + raise CompatibilityError( + 'Requested OS version {} and board "{}.{}", ' + 'which are not compatible.'.format(target_.os_version, + target_.board, + target_.board_version)) + + def GetSpecAndTarget(self, args, **kwargs): + """Creates and verifies the spec and target_ to use. + + Args: + args: parsed arg namespace; spec and target_ are based on + args.specification and args.target. + **kwargs: args to pass to ValidateTarget(). + + Returns: + A tuple of (ProjectSpec, Target) objects. + + Raises: + SpecError: If no project spec can be found. + TargetError: If no target can be found. + ValidationError: If the target fails to validate. + """ + if not args.specification: + raise SpecError( + 'No project spec could be found. Either run from a directory ' + 'containing a project spec named "{}" (or run from within ' + 'a subdirectory thereof), or specify a project spec file with ' + '`--specification`'.format(util.PROJECT_SPEC_FILENAME)) + try: + spec = project_spec.ProjectSpec.from_xml(args.specification) + except (IOError, common.LoadError) as e: + raise SpecError('Error parsing product spec {}: {}'.format( + args.specification, e)) + + try: + target_ = spec.get_target(args.target) + except KeyError as e: + raise TargetError(e) + + self.ValidateTarget(target_, **kwargs) + + return (spec, target_) + + def SetMetricsClass(self, class_name): + """Only necessary if you want to override the default metrics class. + + By default the class will be the command's class name. + + Args: + class_name: The new name for metrics to send. + """ + self._timer.name = class_name + + def SetMetricsLabel(self, label): + """Add an optional label to the metrics being sent by this command.""" + self._timer.label = label + + def RunWithMetrics(self, args): + """Wraps Run() with metrics collection and upload. + + This function times the call to Run(), and after completion + uploads metrics (if the user has opted in) containing the + command and timing data. + + Currently we upload metrics for all cases except KeyboardInterrupt, + in order to make Ctrl+C as responsive as possible. If we get async + metrics uploads going, it would probably be helpful to upload those + as well (http://b/27703295). + + Args: + args: list of args to pass to Run(). + + Returns: + Run() return value. + + Raises: + Passes up any exceptions thrown by Run(). + """ + result = None + try: + self._timer.Start() + result = self.Run(args) + except KeyboardInterrupt: + # Leave result as None to skip metrics upload for Ctrl+C. + raise + except error.Error as e: + result = e.errno + raise + except: + result = error.GENERIC_ERRNO + raise + finally: + if result is not None: + self._timer.Stop() + metrics_util.send_hit_and_retries( + timing.Timing.FromTimer(self._timer, result)) + + return result + + def Run(self, args): + """Called when the command is used. + + Args: + args: An argparse.Namespace of arguments for the command. + + Returns: + 0 if successful, an exit code otherwise. + """ + raise NotImplementedError('Run() must be overridden') class Group(object): - @staticmethod - def GroupArgs(parser): - """Set args that are available for all commands in a group.""" - pass + @staticmethod + def GroupArgs(parser): + """Set args that are available for all commands in a group.""" + pass diff --git a/cli/lib/cli/clicommand_unittest.py b/cli/lib/cli/clicommand_unittest.py index 67cfacd..9b5bc9b 100644 --- a/cli/lib/cli/clicommand_unittest.py +++ b/cli/lib/cli/clicommand_unittest.py @@ -32,162 +32,164 @@ from project import target_stub class ValidateTargetTest(unittest.TestCase): - _BSP = 'devicename' - - def setUp(self): - self.stub_util = util_stub.StubUtil() - - clicommand.util = self.stub_util - - self.dev = device_stub.StubDevice(self._BSP, downloaded=True) - self.command = clicommand.Command() - self.target = target_stub.StubTarget( - name='test_target', os='brillo', - os_version=self.stub_util.GetOSVersion(), board=self._BSP, - board_version=self.dev.version, device=self.dev) - - def test_validate_valid(self): - self.command.ValidateTarget(self.target) - self.command.ValidateTarget(self.target, os=False) - self.command.ValidateTarget(self.target, board=False) - self.command.ValidateTarget(self.target, os=False, board=False) - - def test_validate_invalid_os(self): - self.target.os = 'not_brillo' - with self.assertRaises(clicommand.ValidationError): - self.command.ValidateTarget(self.target) - self.command.ValidateTarget(self.target, os=False) - with self.assertRaises(clicommand.ValidationError): - # Even if we ignore download state, the os still is invalid. - self.command.ValidateTarget(self.target, os_downloaded=False) - with self.assertRaises(clicommand.ValidationError): - self.command.ValidateTarget(self.target, board=False) - self.command.ValidateTarget(self.target, os=False, board=False) - - def test_validate_invalid_bsp(self): - self.target.device_raises = True - with self.assertRaises(clicommand.ValidationError): - self.command.ValidateTarget(self.target) - with self.assertRaises(clicommand.ValidationError): - self.command.ValidateTarget(self.target, os=False) - self.command.ValidateTarget(self.target, board=False) - with self.assertRaises(clicommand.ValidationError): - # Even if we ignore download state, the bsp still doesn't exist. - self.command.ValidateTarget(self.target, board_downloaded=False) - self.command.ValidateTarget(self.target, os=False, board=False) - - def test_validate_os_not_downloaded(self): - self.target.os_version = 'not_a_version' - with self.assertRaises(clicommand.ValidationError): - self.command.ValidateTarget(self.target) - self.command.ValidateTarget(self.target, os_downloaded=False) - # Doesn't matter if os_downloaded is True as long as os is False - self.command.ValidateTarget(self.target, os=False, os_downloaded=True) - with self.assertRaises(clicommand.ValidationError): - self.command.ValidateTarget(self.target, board=False) - self.command.ValidateTarget(self.target, os=False, board=False) - - def test_validate_bsp_not_downloaded(self): - self.dev.downloaded = False - with self.assertRaises(clicommand.ValidationError): - self.command.ValidateTarget(self.target) - with self.assertRaises(clicommand.ValidationError): - self.command.ValidateTarget(self.target, os=False) - self.command.ValidateTarget(self.target, board_downloaded=False) - # Doesn't matter if board_downloaded is True as long as board is False - self.command.ValidateTarget(self.target, board=False, board_downloaded=True) - self.command.ValidateTarget(self.target, os=False, board=False) - - def test_validate_bsp_invalid_version(self): - self.target.device_raises = True - with self.assertRaises(clicommand.ValidationError): - self.command.ValidateTarget(self.target) - with self.assertRaises(clicommand.ValidationError): - self.command.ValidateTarget(self.target, os=False) - self.command.ValidateTarget(self.target, board=False) - with self.assertRaises(clicommand.ValidationError): - # Even if we ignore download state, the bsp still doesn't exist. - self.command.ValidateTarget(self.target, board_downloaded=False) - self.command.ValidateTarget(self.target, os=False, board=False) + _BSP = 'devicename' + + def setUp(self): + self.stub_util = util_stub.StubUtil() + + clicommand.util = self.stub_util + + self.dev = device_stub.StubDevice(self._BSP, downloaded=True) + self.command = clicommand.Command() + self.target = target_stub.StubTarget( + name='test_target', os='brillo', + os_version=self.stub_util.GetOSVersion(), board=self._BSP, + board_version=self.dev.version, device=self.dev) + + def test_validate_valid(self): + self.command.ValidateTarget(self.target) + self.command.ValidateTarget(self.target, os=False) + self.command.ValidateTarget(self.target, board=False) + self.command.ValidateTarget(self.target, os=False, board=False) + + def test_validate_invalid_os(self): + self.target.os = 'not_brillo' + with self.assertRaises(clicommand.ValidationError): + self.command.ValidateTarget(self.target) + self.command.ValidateTarget(self.target, os=False) + with self.assertRaises(clicommand.ValidationError): + # Even if we ignore download state, the os still is invalid. + self.command.ValidateTarget(self.target, os_downloaded=False) + with self.assertRaises(clicommand.ValidationError): + self.command.ValidateTarget(self.target, board=False) + self.command.ValidateTarget(self.target, os=False, board=False) + + def test_validate_invalid_bsp(self): + self.target.device_raises = True + with self.assertRaises(clicommand.ValidationError): + self.command.ValidateTarget(self.target) + with self.assertRaises(clicommand.ValidationError): + self.command.ValidateTarget(self.target, os=False) + self.command.ValidateTarget(self.target, board=False) + with self.assertRaises(clicommand.ValidationError): + # Even if we ignore download state, the bsp still doesn't exist. + self.command.ValidateTarget(self.target, board_downloaded=False) + self.command.ValidateTarget(self.target, os=False, board=False) + + def test_validate_os_not_downloaded(self): + self.target.os_version = 'not_a_version' + with self.assertRaises(clicommand.ValidationError): + self.command.ValidateTarget(self.target) + self.command.ValidateTarget(self.target, os_downloaded=False) + # Doesn't matter if os_downloaded is True as long as os is False + self.command.ValidateTarget(self.target, os=False, os_downloaded=True) + with self.assertRaises(clicommand.ValidationError): + self.command.ValidateTarget(self.target, board=False) + self.command.ValidateTarget(self.target, os=False, board=False) + + def test_validate_bsp_not_downloaded(self): + self.dev.downloaded = False + with self.assertRaises(clicommand.ValidationError): + self.command.ValidateTarget(self.target) + with self.assertRaises(clicommand.ValidationError): + self.command.ValidateTarget(self.target, os=False) + self.command.ValidateTarget(self.target, board_downloaded=False) + # Doesn't matter if board_downloaded is True as long as board is False. + self.command.ValidateTarget(self.target, board=False, + board_downloaded=True) + self.command.ValidateTarget(self.target, os=False, board=False) + + def test_validate_bsp_invalid_version(self): + self.target.device_raises = True + with self.assertRaises(clicommand.ValidationError): + self.command.ValidateTarget(self.target) + with self.assertRaises(clicommand.ValidationError): + self.command.ValidateTarget(self.target, os=False) + self.command.ValidateTarget(self.target, board=False) + with self.assertRaises(clicommand.ValidationError): + # Even if we ignore download state, the bsp still doesn't exist. + self.command.ValidateTarget(self.target, board_downloaded=False) + self.command.ValidateTarget(self.target, os=False, board=False) def _keyboard_interrupt_func(_args): - """Helper function to raise KeyboardInterrupt.""" - raise KeyboardInterrupt() + """Helper function to raise KeyboardInterrupt.""" + raise KeyboardInterrupt() def _base_exception_func(_args): - """Helper function to raise BaseException.""" - raise BaseException() + """Helper function to raise BaseException.""" + raise BaseException() class RunWithMetricsTest(unittest.TestCase): - """Tests to make sure RunWithMetrics() behaves as expected.""" - - def setUp(self): - self.config_stub = config_stub.StubConfig(metrics_opt_in='1') - self.hit_stub = hit_stub.StubHit() - self.hit_store_stub = hit_store_stub.StubHitStore() - self.user_store_stub = self.config_stub.user_store - - clicommand.metrics_util.config = self.config_stub - clicommand.metrics_util.hit = self.hit_stub - clicommand.metrics_util.hit_store = self.hit_store_stub - # pylint: disable=protected-access - clicommand.metrics_util._MetricsUtilState.user_store = self.user_store_stub - - self.command = clicommand.Command() - - def last_uploaded_result(self): - """Helper to return the most recent uploaded result or None.""" - if hit_stub.StubHit.last_send is None: - return None - return hit_stub.StubHit.get_custom_data_field( - # pylint: disable=protected-access - metrics_util._BRILLO_CD_RESULT) - - def test_upload_result(self): - """Tests successfully uploading a Run() result.""" - for result in (0, 1, 130): - self.command.Run = lambda _args, ret=result: ret - - self.assertEqual(result, self.command.RunWithMetrics([])) - self.assertEqual(result, self.last_uploaded_result()) - - def test_keyboard_interrupt(self): - """Tests metrics are not uploaded after Ctrl+C.""" - self.command.Run = _keyboard_interrupt_func - - with self.assertRaises(KeyboardInterrupt): - self.command.RunWithMetrics([]) - self.assertIsNone(self.last_uploaded_result()) - - def test_base_exception(self): - """Tests metrics are uploaded for any other exception.""" - self.command.Run = _base_exception_func + """Tests to make sure RunWithMetrics() behaves as expected.""" - with self.assertRaises(BaseException): - self.command.RunWithMetrics([]) - self.assertEqual(1, self.last_uploaded_result()) + def setUp(self): + self.config_stub = config_stub.StubConfig(metrics_opt_in='1') + self.hit_stub = hit_stub.StubHit() + self.hit_store_stub = hit_store_stub.StubHitStore() + self.user_store_stub = self.config_stub.user_store - def test_metrics_opt_out(self): - """Tests metrics are never sent if the user has opted out.""" - self.config_stub.user_store.metrics_opt_in = '0' - - self.command.Run = lambda _args: 0 - self.command.RunWithMetrics([]) - self.assertIsNone(self.last_uploaded_result()) - - self.command.Run = lambda _args: 1 - self.command.RunWithMetrics([]) - self.assertIsNone(self.last_uploaded_result()) - - self.command.Run = _keyboard_interrupt_func - with self.assertRaises(KeyboardInterrupt): - self.command.RunWithMetrics([]) - self.assertIsNone(self.last_uploaded_result()) - - self.command.Run = _base_exception_func - with self.assertRaises(BaseException): - self.command.RunWithMetrics([]) - self.assertIsNone(self.last_uploaded_result()) + clicommand.metrics_util.config = self.config_stub + clicommand.metrics_util.hit = self.hit_stub + clicommand.metrics_util.hit_store = self.hit_store_stub + # pylint: disable=protected-access + clicommand.metrics_util._MetricsUtilState.user_store = ( + self.user_store_stub) + + self.command = clicommand.Command() + + def last_uploaded_result(self): + """Helper to return the most recent uploaded result or None.""" + if hit_stub.StubHit.last_send is None: + return None + return hit_stub.StubHit.get_custom_data_field( + # pylint: disable=protected-access + metrics_util._BRILLO_CD_RESULT) + + def test_upload_result(self): + """Tests successfully uploading a Run() result.""" + for result in (0, 1, 130): + self.command.Run = lambda _args, ret=result: ret + + self.assertEqual(result, self.command.RunWithMetrics([])) + self.assertEqual(result, self.last_uploaded_result()) + + def test_keyboard_interrupt(self): + """Tests metrics are not uploaded after Ctrl+C.""" + self.command.Run = _keyboard_interrupt_func + + with self.assertRaises(KeyboardInterrupt): + self.command.RunWithMetrics([]) + self.assertIsNone(self.last_uploaded_result()) + + def test_base_exception(self): + """Tests metrics are uploaded for any other exception.""" + self.command.Run = _base_exception_func + + with self.assertRaises(BaseException): + self.command.RunWithMetrics([]) + self.assertEqual(1, self.last_uploaded_result()) + + def test_metrics_opt_out(self): + """Tests metrics are never sent if the user has opted out.""" + self.config_stub.user_store.metrics_opt_in = '0' + + self.command.Run = lambda _args: 0 + self.command.RunWithMetrics([]) + self.assertIsNone(self.last_uploaded_result()) + + self.command.Run = lambda _args: 1 + self.command.RunWithMetrics([]) + self.assertIsNone(self.last_uploaded_result()) + + self.command.Run = _keyboard_interrupt_func + with self.assertRaises(KeyboardInterrupt): + self.command.RunWithMetrics([]) + self.assertIsNone(self.last_uploaded_result()) + + self.command.Run = _base_exception_func + with self.assertRaises(BaseException): + self.command.RunWithMetrics([]) + self.assertIsNone(self.last_uploaded_result()) diff --git a/cli/lib/cli/climanager.py b/cli/lib/cli/climanager.py index d880614..ca072b6 100644 --- a/cli/lib/cli/climanager.py +++ b/cli/lib/cli/climanager.py @@ -27,236 +27,241 @@ import error class Error(error.Error): - """Raised when CommandGroup directories and files don't match expectations.""" + """Raised when CommandGroup directories and files don't match + expectations. + """ class CommandGroup(object): - """A CLI Command Group.""" - - def __init__(self, name, path, parent=None): - self.name = name - self.parent = parent - self.path = path - self.group_class = self.GetGroupClassType() - - if parent is None: - self.parser = argparse.ArgumentParser( - prog=name, description=self.group_class.__doc__) - else: - self.parser = self._AddSubparser(parent.subparsers, name, - self.group_class) - self.subparsers = self.parser.add_subparsers() - - self.FindCommands() - - def _AddSubparser(self, subparsers, name, command_class): - """Adds a new subparser. - - Handles some common functionality such as help and description - text generation. - - We expect Command and CommandGroup classes to provide help text in - their docstring. The first line should be a single sentence overview, - and additional text may be provided for a more complete description. - - Args: - subparsers: the subparsers object to add the new subparser to. - name: subparser name. - command_class: subparser Command or CommandGroup class. - - Returns: - The new subparser object. - """ - # We need to do a little bit of additional work with the text to account - # for the fact that all docstring lines except the first will be indented - # some unknown amount. - # For cross-platform support, keep the line endings with splitlines() so - # we can easily join them together again in the same way. - description = command_class.__doc__.splitlines(True) - short_help = description[0].strip() - description = description[0] + textwrap.dedent(''.join(description[1:])) - return subparsers.add_parser( - name, description=description, help=short_help, - formatter_class=argparse.RawDescriptionHelpFormatter) - - def AddCommandGroup(self, name, path): - CommandGroup(name, path, parent=self) - - def AddCommand(self, command_type): - com_parser = self._AddSubparser( - self.subparsers, command_type.__name__.lower(), command_type) - com_parser.set_defaults(command_type=command_type) - - # Add arguments to the parser. REMAINDER must come last. - command_type.AddDefaultArgs(com_parser) - command_type.Args(com_parser) - self.group_class.GroupArgs(com_parser) - if command_type.remainder_arg: - com_parser.add_argument(command_type.remainder_arg[0], - nargs=argparse.REMAINDER, - help=command_type.remainder_arg[1]) - else: - # Add a hidden and unused remainder arg. This enables better error - # messages by making parse_known_args() more forgiving instead of just - # exiting immediately on failure, and ensures commands with and without - # explicit remainder args error out the same ways. - com_parser.add_argument('--_remainder_args', - nargs=argparse.REMAINDER, - help=argparse.SUPPRESS) - - # Update the command with its parentage and parser. - command_type.set_group(self) - command_type.set_parser(com_parser) - - def FindTypes(self, module, find_type): - class_types = [] - for item in module.__dict__.values(): - if issubclass(type(item), type): - if issubclass(item, find_type): - class_types.append(item) - return class_types - - def FindCommands(self): - count = 0 - # Iterate modules looking for commands. - for module in importutils.ModuleIter(self.path): - class_types = self.FindTypes(module, clicommand.Command) - for class_type in class_types: - self.AddCommand(class_type) - count += 1 - if count == 0: - # TODO(b/25951591): Change to use metric reporting Exception. - raise Error('No commands in command group {}'.format(self.name)) - - def GetGroupClassType(self): - package = importutils.LoadPackage(self.path) - class_types = self.FindTypes(package, clicommand.Group) - if len(class_types) != 1: - # TODO(b/25951591): Change to use metric reporting Exceptions. - raise Error('Expected exactly 1 class type for group {}. Found {}'.format( - self.name, class_types)) - return class_types[0] + """A CLI Command Group.""" + + def __init__(self, name, path, parent=None): + self.name = name + self.parent = parent + self.path = path + self.group_class = self.GetGroupClassType() + + if parent is None: + self.parser = argparse.ArgumentParser( + prog=name, description=self.group_class.__doc__) + else: + self.parser = self._AddSubparser(parent.subparsers, name, + self.group_class) + self.subparsers = self.parser.add_subparsers() + + self.FindCommands() + + def _AddSubparser(self, subparsers, name, command_class): + """Adds a new subparser. + + Handles some common functionality such as help and description + text generation. + + We expect Command and CommandGroup classes to provide help text in + their docstring. The first line should be a single sentence overview, + and additional text may be provided for a more complete description. + + Args: + subparsers: the subparsers object to add the new subparser to. + name: subparser name. + command_class: subparser Command or CommandGroup class. + + Returns: + The new subparser object. + """ + # We need to do a little bit of additional work with the text to account + # for the fact that all docstring lines except the first will be + # indented some unknown amount. + # For cross-platform support, keep the line endings with splitlines() so + # we can easily join them together again in the same way. + description = command_class.__doc__.splitlines(True) + short_help = description[0].strip() + description = description[0] + textwrap.dedent(''.join(description[1:])) + return subparsers.add_parser( + name, description=description, help=short_help, + formatter_class=argparse.RawDescriptionHelpFormatter) + + def AddCommandGroup(self, name, path): + CommandGroup(name, path, parent=self) + + def AddCommand(self, command_type): + com_parser = self._AddSubparser( + self.subparsers, command_type.__name__.lower(), command_type) + com_parser.set_defaults(command_type=command_type) + + # Add arguments to the parser. REMAINDER must come last. + command_type.AddDefaultArgs(com_parser) + command_type.Args(com_parser) + self.group_class.GroupArgs(com_parser) + if command_type.remainder_arg: + com_parser.add_argument(command_type.remainder_arg[0], + nargs=argparse.REMAINDER, + help=command_type.remainder_arg[1]) + else: + # Add a hidden and unused remainder arg. This enables better error + # messages by making parse_known_args() more forgiving instead of + # just exiting immediately on failure, and ensures commands with and + # without explicit remainder args error out the same ways. + com_parser.add_argument('--_remainder_args', + nargs=argparse.REMAINDER, + help=argparse.SUPPRESS) + + # Update the command with its parentage and parser. + command_type.set_group(self) + command_type.set_parser(com_parser) + + def FindTypes(self, module, find_type): + class_types = [] + for item in module.__dict__.values(): + if issubclass(type(item), type): + if issubclass(item, find_type): + class_types.append(item) + return class_types + + def FindCommands(self): + count = 0 + # Iterate modules looking for commands. + for module in importutils.ModuleIter(self.path): + class_types = self.FindTypes(module, clicommand.Command) + for class_type in class_types: + self.AddCommand(class_type) + count += 1 + if count == 0: + # TODO(b/25951591): Change to use metric reporting Exception. + raise Error('No commands in command group {}'.format(self.name)) + + def GetGroupClassType(self): + package = importutils.LoadPackage(self.path) + class_types = self.FindTypes(package, clicommand.Group) + if len(class_types) != 1: + # TODO(b/25951591): Change to use metric reporting Exceptions. + raise Error( + 'Expected exactly 1 class type for group {}. Found {}'.format( + self.name, class_types)) + return class_types[0] class Cli(object): - """CLI management. - - Attributes: - args: the parsed arg namespace (after Execute() has been run). - """ + """CLI management. - def __init__(self, name, root_path): - """Initializes the Cli object. - - Args: - name: top-level CLI name. - root_path: path to the package containing top-level commands. + Attributes: + args: the parsed arg namespace (after Execute() has been run). """ - self.name = name - self.root_path = root_path - self.root_group = CommandGroup(name, root_path) - self.args = None - - def AddCommandGroup(self, name, path): - """Adds a command group to the CLI. - - Args: - name: command name. - path: path to the package containing the grouped subcommands. - """ - self.root_group.AddCommandGroup(name, path) - - def _ParseArgs(self, args=None): - """Parses the commandline. - Checks some corner cases to make argparse behavior a little more - sane. Most significantly, when parsing a command that has set the - remainder_arg class attribute, we do the following: - 1. Start the remainder args at the first unknown arg whether it's - positional or optional; by default argparse only starts the - remainder at a positional arg. - 2. Remove a leading '--' from the remainder if it was used. - - The result is that any of these will now work as expected: - bdk build platform -j40 - bdk adb -s 12345 shell - bdk adb -- -s 12345 shell - - Args: - args: list of string args to parse, or None to use sys.argv. - - Returns: - The parsed argument namespace. - """ - if args is None: - args = sys.argv[1:] - - # parse_args() has some corner cases that have undesired behavior (see - # http://b/27795688), so instead we use parse_known_args() then manually - # add any leftovers into the remainder argument. - (parsed, leftover) = self.root_group.parser.parse_known_args(args) - - # parse_known_args() will skip unknown optionals to grab positionals or - # known optionals, which can cause some problems e.g.: - # bdk adb -s foo bar ==> remainder=[foo bar], leftover=[-s] - # Given this parsing, it's impossible to generically determine where -s - # should be placed when passed through. - # To avoid this, we find the first unknown arg whether it's positional or - # optional, and then treat everything from there on as passthrough args. - if leftover and leftover != args[-len(leftover):]: - # Iterate backwards over args until we've seen everything in leftover, - # and that's our actual remainder. - arg_counts = {arg: leftover.count(arg) for arg in leftover} - i = len(args) - for i in range(len(args) - 1, -1, -1): - arg = args[i] - count = arg_counts.get(arg) - if count == 1: - arg_counts.pop(arg) - elif count > 1: - arg_counts[arg] = count - 1 - if not arg_counts: - break - - # Re-parse while explicitly blocking off the actual remainder. - parsed = self.root_group.parser.parse_args(args[:i]) - leftover = args[i:] - - if parsed.command_type.remainder_arg: - # Add any leftovers to the remainder arg and remove '--' if it was used. - remainder_arg_name = parsed.command_type.remainder_arg[0] - leftover = getattr(parsed, remainder_arg_name) + leftover - if leftover and leftover[0] == '--': - leftover.pop(0) - setattr(parsed, remainder_arg_name, leftover) - elif leftover: - # If got remainder args but didn't want them, we have a bogus input. - # Print an error message and exit. - parsed.command_type.parser.error( - 'unknown arguments: {}'.format(' '.join(leftover))) - - return parsed - - def Execute(self, args=None): - """Runs the selected command. - - Args: - args: list of string args to parse, or None to use sys.argv. - - Returns: - The result of the selected command's Run() function. - - Raises: - TypeError: command Run() function returned a non-integer value. - """ - self.args = self._ParseArgs(args) - command = self.args.command_type() - result = command.RunWithMetrics(self.args) - - # Enforce the return type with a useful message. - if not isinstance(result, int): - raise TypeError('Non-integer return code from "{0} {1}". ' - 'Please report this bug!'.format( - self.args.command_type.group().name, - self.args.command_type.__name__.lower())) - return result + def __init__(self, name, root_path): + """Initializes the Cli object. + + Args: + name: top-level CLI name. + root_path: path to the package containing top-level commands. + """ + self.name = name + self.root_path = root_path + self.root_group = CommandGroup(name, root_path) + self.args = None + + def AddCommandGroup(self, name, path): + """Adds a command group to the CLI. + + Args: + name: command name. + path: path to the package containing the grouped subcommands. + """ + self.root_group.AddCommandGroup(name, path) + + def _ParseArgs(self, args=None): + """Parses the commandline. + + Checks some corner cases to make argparse behavior a little more + sane. Most significantly, when parsing a command that has set the + remainder_arg class attribute, we do the following: + 1. Start the remainder args at the first unknown arg whether it's + positional or optional; by default argparse only starts the + remainder at a positional arg. + 2. Remove a leading '--' from the remainder if it was used. + + The result is that any of these will now work as expected: + bdk build platform -j40 + bdk adb -s 12345 shell + bdk adb -- -s 12345 shell + + Args: + args: list of string args to parse, or None to use sys.argv. + + Returns: + The parsed argument namespace. + """ + if args is None: + args = sys.argv[1:] + + # parse_args() has some corner cases that have undesired behavior (see + # http://b/27795688), so instead we use parse_known_args() then manually + # add any leftovers into the remainder argument. + (parsed, leftover) = self.root_group.parser.parse_known_args(args) + + # parse_known_args() will skip unknown optionals to grab positionals or + # known optionals, which can cause some problems e.g.: + # bdk adb -s foo bar ==> remainder=[foo bar], leftover=[-s] + # Given this parsing, it's impossible to generically determine where -s + # should be placed when passed through. + # To avoid this, we find the first unknown arg whether it's positional + # or optional, and then treat everything from there on as passthrough + # args. + if leftover and leftover != args[-len(leftover):]: + # Iterate backwards over args until we've seen everything in + # leftover, and that's our actual remainder. + arg_counts = {arg: leftover.count(arg) for arg in leftover} + i = len(args) + for i in range(len(args) - 1, -1, -1): + arg = args[i] + count = arg_counts.get(arg) + if count == 1: + arg_counts.pop(arg) + elif count > 1: + arg_counts[arg] = count - 1 + if not arg_counts: + break + + # Re-parse while explicitly blocking off the actual remainder. + parsed = self.root_group.parser.parse_args(args[:i]) + leftover = args[i:] + + if parsed.command_type.remainder_arg: + # Add any leftovers to the remainder arg and remove '--' if it was + # used. + remainder_arg_name = parsed.command_type.remainder_arg[0] + leftover = getattr(parsed, remainder_arg_name) + leftover + if leftover and leftover[0] == '--': + leftover.pop(0) + setattr(parsed, remainder_arg_name, leftover) + elif leftover: + # If got remainder args but didn't want them, we have a bogus input. + # Print an error message and exit. + parsed.command_type.parser.error( + 'unknown arguments: {}'.format(' '.join(leftover))) + + return parsed + + def Execute(self, args=None): + """Runs the selected command. + + Args: + args: list of string args to parse, or None to use sys.argv. + + Returns: + The result of the selected command's Run() function. + + Raises: + TypeError: command Run() function returned a non-integer value. + """ + self.args = self._ParseArgs(args) + command = self.args.command_type() + result = command.RunWithMetrics(self.args) + + # Enforce the return type with a useful message. + if not isinstance(result, int): + raise TypeError('Non-integer return code from "{0} {1}". ' + 'Please report this bug!'.format( + self.args.command_type.group().name, + self.args.command_type.__name__.lower())) + return result diff --git a/cli/lib/cli/climanager_unittest.py b/cli/lib/cli/climanager_unittest.py index db62cdd..4043741 100644 --- a/cli/lib/cli/climanager_unittest.py +++ b/cli/lib/cli/climanager_unittest.py @@ -36,345 +36,351 @@ TEST_COMMAND_PACKAGE = os.path.join(os.path.dirname(__file__), 'test_cli') class ParsingTest(unittest.TestCase): - """Provides some common parse checking functionality.""" + """Provides some common parse checking functionality.""" - # Each test must provide the command name and a cli object to parse. - command = None - cli = None + # Each test must provide the command name and a cli object to parse. + command = None + cli = None - # Test can provide a set of default kwargs to use if unspecified. - defaults = {} + # Test can provide a set of default kwargs to use if unspecified. + defaults = {} - def check_parse(self, input_args, **kwargs): - """Parses args and verifies they match kwargs. + def check_parse(self, input_args, **kwargs): + """Parses args and verifies they match kwargs. - Also checks that self.command matches the name of the CliCommand - class that was selected. + Also checks that self.command matches the name of the CliCommand + class that was selected. - This may be called multiple times from a single test, it doesn't - change any state. + This may be called multiple times from a single test, it doesn't + change any state. - Args: - input_args: list of string arguments to the parser. - **kwargs: expected parsed namespace. + Args: + input_args: list of string arguments to the parser. + **kwargs: expected parsed namespace. - Raises: - On failure, raises unittest exceptions. - """ - # pylint: disable=protected-access - parsed = vars(self.cli._ParseArgs(input_args)) - - # Make sure command_type is the correct class and that we added the hidden - # remainder arg if necessary. - command_type = parsed.pop('command_type') - self.assertEqual(self.command, command_type.__name__.lower()) - if not command_type.remainder_arg: - # The hidden remainder arg must not actually grab any args, it's just - # there to improve error handling. - self.assertIsNone(parsed.pop('_remainder_args')) - - for k, v in self.defaults.iteritems(): - kwargs.setdefault(k, v) - self.assertDictEqual(kwargs, parsed) - - def check_parse_error(self, input_args, command, invalid_choice=None, - unknown_arguments=None, too_few_arguments=False): - """Parses args and verifies an error is given. - - Callers may specify one of the keyword args to also verify that - the error message prints out the indicated reason for failure. - - Args: - input_args: list of string arguments to the parser. - command: CLI command that should print the error. - invalid_choice: an invalid choice error to check. - unknown_arguments: list of unknown arguments to check. - too_few_arguments: if True, checks for "too few arguments" error. - - Raises: - On failure, raises unittest exceptions. - """ - with test_util.OutputCapture() as output: - with self.assertRaises(SystemExit): + Raises: + On failure, raises unittest exceptions. + """ # pylint: disable=protected-access - self.cli._ParseArgs(input_args) - - # Parsing errors should give a usage string as well. - self.assertIn('usage:', output.stderr) - self.assertRegexpMatches(output.stderr, '{}: error'.format(command)) - - # Check for specific error messages. Some of the messages come from - # argparse, which may turn out to be a little brittle if error strings - # change between versions, but is useful for now as a sanity check that - # we're producing the correct error message. - error_text = None - if invalid_choice: - error_text = "invalid choice: '{}'".format(invalid_choice) - elif unknown_arguments: - error_text = 'unknown arguments: {}'.format(' '.join(unknown_arguments)) - elif too_few_arguments: - error_text = 'too few arguments' - - if error_text: - self.assertRegexpMatches(output.stderr, 'error: {}'.format(error_text)) + parsed = vars(self.cli._ParseArgs(input_args)) + + # Make sure command_type is the correct class and that we added the + # hidden remainder arg if necessary. + command_type = parsed.pop('command_type') + self.assertEqual(self.command, command_type.__name__.lower()) + if not command_type.remainder_arg: + # The hidden remainder arg must not actually grab any args, it's + # just there to improve error handling. + self.assertIsNone(parsed.pop('_remainder_args')) + + for k, v in self.defaults.iteritems(): + kwargs.setdefault(k, v) + self.assertDictEqual(kwargs, parsed) + + def check_parse_error(self, input_args, command, invalid_choice=None, + unknown_arguments=None, too_few_arguments=False): + """Parses args and verifies an error is given. + + Callers may specify one of the keyword args to also verify that + the error message prints out the indicated reason for failure. + + Args: + input_args: list of string arguments to the parser. + command: CLI command that should print the error. + invalid_choice: an invalid choice error to check. + unknown_arguments: list of unknown arguments to check. + too_few_arguments: if True, checks for "too few arguments" error. + + Raises: + On failure, raises unittest exceptions. + """ + with test_util.OutputCapture() as output: + with self.assertRaises(SystemExit): + # pylint: disable=protected-access + self.cli._ParseArgs(input_args) + + # Parsing errors should give a usage string as well. + self.assertIn('usage:', output.stderr) + self.assertRegexpMatches(output.stderr, '{}: error'.format(command)) + + # Check for specific error messages. Some of the messages come from + # argparse, which may turn out to be a little brittle if error strings + # change between versions, but is useful for now as a sanity check that + # we're producing the correct error message. + error_text = None + if invalid_choice: + error_text = "invalid choice: '{}'".format(invalid_choice) + elif unknown_arguments: + error_text = 'unknown arguments: {}'.format( + ' '.join(unknown_arguments)) + elif too_few_arguments: + error_text = 'too few arguments' + + if error_text: + self.assertRegexpMatches(output.stderr, + 'error: {}'.format(error_text)) class FakeCliTest(ParsingTest): - """Tests argument parsing using our test CLI.""" + """Tests argument parsing using our test CLI.""" - def setUp(self): - self.cli = climanager.Cli('climanager_unittest_cli', TEST_COMMAND_PACKAGE) + def setUp(self): + self.cli = climanager.Cli('climanager_unittest_cli', + TEST_COMMAND_PACKAGE) - def check_parse(self, input_args, **kwargs): - """Overrides check_parse() to add some helper functionality. + def check_parse(self, input_args, **kwargs): + """Overrides check_parse() to add some helper functionality. - For convenience, this override automatically prepends self.command - so it doesn't have to be repeatedly specified in tests. It also runs - the checker both with and without the --debug flag, unless debug - was explicitly given in kwargs. + For convenience, this override automatically prepends self.command + so it doesn't have to be repeatedly specified in tests. It also runs + the checker both with and without the --debug flag, unless debug + was explicitly given in kwargs. - These conveniences only work for the simple fake CLI with no - subparsers, otherwise it becomes too complicated and error-prone - to try to find the correct command and where to put --debug. + These conveniences only work for the simple fake CLI with no + subparsers, otherwise it becomes too complicated and error-prone + to try to find the correct command and where to put --debug. - Args: - input_args: list of string arguments to the parser. - **kwargs: expected parsed namespace. - """ - check_func = super(FakeCliTest, self).check_parse - if 'debug' in kwargs: - check_func([self.command] + input_args, **kwargs) - else: - check_func([self.command] + input_args, debug=False, **kwargs) - check_func([self.command, '--debug'] + input_args, debug=True, **kwargs) + Args: + input_args: list of string arguments to the parser. + **kwargs: expected parsed namespace. + """ + check_func = super(FakeCliTest, self).check_parse + if 'debug' in kwargs: + check_func([self.command] + input_args, **kwargs) + else: + check_func([self.command] + input_args, debug=False, **kwargs) + check_func([self.command, '--debug'] + input_args, debug=True, + **kwargs) class PositionalOptionalTest(FakeCliTest): - """Tests parsing the PositionalOptional command.""" + """Tests parsing the PositionalOptional command.""" - command = 'positionaloptional' + command = 'positionaloptional' - def test_positional(self): - self.check_parse(['foo'], positional='foo', optional=None) + def test_positional(self): + self.check_parse(['foo'], positional='foo', optional=None) - def test_both(self): - for args in (['foo', '--optional', 'bar'], - ['--optional', 'bar', 'foo']): - self.check_parse(args, positional='foo', optional='bar') + def test_both(self): + for args in (['foo', '--optional', 'bar'], + ['--optional', 'bar', 'foo']): + self.check_parse(args, positional='foo', optional='bar') - def test_invalid(self): - for args in ([], - ['foo', 'unknown_positional'], - ['foo', '--unknown_option']): - with self.assertRaises(SystemExit): - # pylint: disable=protected-access - self.cli._ParseArgs(args) + def test_invalid(self): + for args in ([], + ['foo', 'unknown_positional'], + ['foo', '--unknown_option']): + with self.assertRaises(SystemExit): + # pylint: disable=protected-access + self.cli._ParseArgs(args) class RemainderTest(FakeCliTest): - """Tests parsing the Remainder command.""" - - command = 'remainder' - - def test_empty(self): - for args in ([], - ['--']): - self.check_parse(args, remainder=[]) - - def test_multiple_separators(self): - """Tests that only the first '--' is removed.""" - self.check_parse(['--', '--'], remainder=['--']) - self.check_parse(['--', 'foo', '--'], remainder=['foo', '--']) - - def test_remainder(self): - for args in (['foo'], - ['--bar'], - ['foo', '--bar'], - ['--bar', 'foo']): - self.check_parse(args, remainder=args) - self.check_parse(['--'] + args, remainder=args) - - def test_optional_overlap(self): - """Tests parsing optional overlap in remainder.""" - # If an optional could match either the command or the remainder, it goes - # to the command unless '--' is used to separate. - self.check_parse(['--', '--debug'], remainder=['--debug']) - - # Once a remainder arg is found, the rest of the args are assumed to be - # remainder even if they overlap with explicit options. - for args in (['foo', '--debug'], - ['foo', '--debug', '--debug'], - ['--bar', '--debug'], - ['--bar', '--debug', '--debug']): - self.check_parse(args, remainder=args) + """Tests parsing the Remainder command.""" + + command = 'remainder' + + def test_empty(self): + for args in ([], ['--']): + self.check_parse(args, remainder=[]) + + def test_multiple_separators(self): + """Tests that only the first '--' is removed.""" + self.check_parse(['--', '--'], remainder=['--']) + self.check_parse(['--', 'foo', '--'], remainder=['foo', '--']) + + def test_remainder(self): + for args in (['foo'], + ['--bar'], + ['foo', '--bar'], + ['--bar', 'foo']): + self.check_parse(args, remainder=args) + self.check_parse(['--'] + args, remainder=args) + + def test_optional_overlap(self): + """Tests parsing optional overlap in remainder.""" + # If an optional could match either the command or the remainder, it + # goes to the command unless '--' is used to separate. + self.check_parse(['--', '--debug'], remainder=['--debug']) + + # Once a remainder arg is found, the rest of the args are assumed to be + # remainder even if they overlap with explicit options. + for args in (['foo', '--debug'], + ['foo', '--debug', '--debug'], + ['--bar', '--debug'], + ['--bar', '--debug', '--debug']): + self.check_parse(args, remainder=args) class PositionalRemainderTest(FakeCliTest): - """Tests parsing the PositionalRemainder command.""" + """Tests parsing the PositionalRemainder command.""" - command = 'positionalremainder' + command = 'positionalremainder' - def test_base(self): - self.check_parse(['foo'], positional='foo', remainder=[]) - self.check_parse(['foo', 'bar'], positional='foo', remainder=['bar']) - self.check_parse(['foo', '--baz'], positional='foo', remainder=['--baz']) + def test_base(self): + self.check_parse(['foo'], positional='foo', remainder=[]) + self.check_parse(['foo', 'bar'], positional='foo', remainder=['bar']) + self.check_parse(['foo', '--baz'], positional='foo', + remainder=['--baz']) - def test_invalid(self): - for args in ([], - ['--bar'], - ['--bar', 'foo']): - with self.assertRaises(SystemExit): - self.check_parse(args) + def test_invalid(self): + for args in ([], ['--bar'], ['--bar', 'foo']): + with self.assertRaises(SystemExit): + self.check_parse(args) class OptionalsRemainderTest(FakeCliTest): - """Tests parsing the OptionalsRemainder command.""" + """Tests parsing the OptionalsRemainder command.""" - command = 'optionalsremainder' + command = 'optionalsremainder' - def test_base(self): - self.check_parse(['foo'], flag_a=False, flag_b=False, option_x=None, - remainder=['foo']) + def test_base(self): + self.check_parse(['foo'], flag_a=False, flag_b=False, option_x=None, + remainder=['foo']) - def test_short_options(self): - """Tests a few different ways to give short options.""" - for args in (['-a', '-b', '-x', 'X', 'foo'], - ['-ab', '-x=X', 'foo'], - ['-abxX', 'foo']): - self.check_parse(args, flag_a=True, flag_b=True, option_x='X', - remainder=['foo']) + def test_short_options(self): + """Tests a few different ways to give short options.""" + for args in (['-a', '-b', '-x', 'X', 'foo'], + ['-ab', '-x=X', 'foo'], + ['-abxX', 'foo']): + self.check_parse(args, flag_a=True, flag_b=True, option_x='X', + remainder=['foo']) - def test_optional_overlap(self): - """Tests that overlapping options stay in the remainder.""" - remainder = ['-b', '-x', 'Y', '-xZ', '-q'] + def test_optional_overlap(self): + """Tests that overlapping options stay in the remainder.""" + remainder = ['-b', '-x', 'Y', '-xZ', '-q'] - self.check_parse(['-abxX', '--'] + remainder, - flag_a=True, flag_b=True, option_x='X', - remainder=remainder) - self.check_parse(['-abxX', 'foo'] + remainder, - flag_a=True, flag_b=True, option_x='X', - remainder=['foo'] + remainder) - self.check_parse(['-abxX', '--bar'] + remainder, - flag_a=True, flag_b=True, option_x='X', - remainder=['--bar'] + remainder) + self.check_parse(['-abxX', '--'] + remainder, + flag_a=True, flag_b=True, option_x='X', + remainder=remainder) + self.check_parse(['-abxX', 'foo'] + remainder, + flag_a=True, flag_b=True, option_x='X', + remainder=['foo'] + remainder) + self.check_parse(['-abxX', '--bar'] + remainder, + flag_a=True, flag_b=True, option_x='X', + remainder=['--bar'] + remainder) class BdkTest(ParsingTest): - """Tests for the actual BDK CLI. + """Tests for the actual BDK CLI. - These tests should only verify argument parsing, and should be - kept somewhat minimal to avoid problems keeping them in sync with - the CLI. Actual behavior/logic tests for the CLI should live next - to the implementation instead. - """ + These tests should only verify argument parsing, and should be + kept somewhat minimal to avoid problems keeping them in sync with + the CLI. Actual behavior/logic tests for the CLI should live next + to the implementation instead. + """ - _PROJECT_SPEC = 'bdk_test_project_spec.xml' + _PROJECT_SPEC = 'bdk_test_project_spec.xml' - defaults = { - 'debug': False, - 'specification': _PROJECT_SPEC, - 'target': None - } + defaults = { + 'debug': False, + 'specification': _PROJECT_SPEC, + 'target': None + } - def setUp(self): - # Set BDK_V2_DEVELOPMENT environment var to enable the V2 commands. - self.stub_os = stubs.StubOs() - self.stub_os.environ['BDK_V2_DEVELOPMENT'] = 'true' + def setUp(self): + # Set BDK_V2_DEVELOPMENT environment var to enable the V2 commands. + self.stub_os = stubs.StubOs() + self.stub_os.environ['BDK_V2_DEVELOPMENT'] = 'true' - self.stub_util = util_stub.StubUtil() - self.stub_util.project_spec = self._PROJECT_SPEC + self.stub_util = util_stub.StubUtil() + self.stub_util.project_spec = self._PROJECT_SPEC - clicommand.util = self.stub_util + clicommand.util = self.stub_util - bdk.os = self.stub_os - self.cli = bdk.GetBdkCli() + bdk.os = self.stub_os + self.cli = bdk.GetBdkCli() class BdkAdbTest(BdkTest): - """Tests bdk adb parsing.""" + """Tests bdk adb parsing.""" - command = 'adb' + command = 'adb' - def test_base(self): - for passthrough in ([], - ['devices'], - ['-s', 'foo', 'shell']): - self.check_parse(['root', 'adb'] + passthrough, args=passthrough) + def test_base(self): + for passthrough in ([], + ['devices'], + ['-s', 'foo', 'shell']): + self.check_parse(['root', 'adb'] + passthrough, args=passthrough) - def test_debug(self): - """Tests when --debug applies to the bdk and when it's passed through.""" - self.check_parse(['root', 'adb', '--debug'], debug=True, args=[]) - self.check_parse(['root', 'adb', '--', '--debug'], args=['--debug']) - self.check_parse(['root', 'adb', 'shell', '--debug'], - args=['shell', '--debug']) + def test_debug(self): + """Debug flag test. - # --debug is currently an option of the final leaf commands, so trying - # to specify it earlier is an error. - self.check_parse_error(['--debug', 'root', 'adb'], 'bdk') - self.check_parse_error(['root', '--debug', 'adb'], 'bdk root') + Tests when --debug applies to the bdk and when it's passed through. + """ + self.check_parse(['root', 'adb', '--debug'], debug=True, args=[]) + self.check_parse(['root', 'adb', '--', '--debug'], args=['--debug']) + self.check_parse(['root', 'adb', 'shell', '--debug'], + args=['shell', '--debug']) + + # --debug is currently an option of the final leaf commands, so trying + # to specify it earlier is an error. + self.check_parse_error(['--debug', 'root', 'adb'], 'bdk') + self.check_parse_error(['root', '--debug', 'adb'], 'bdk root') class BdkErrorTest(BdkTest): - """Tests bdk parsing error handling.""" - - def test_no_command(self): - """Tests when no leaf command is specified.""" - self.check_parse_error([], 'bdk', too_few_arguments=True) - self.check_parse_error(['root'], 'bdk root', too_few_arguments=True) - - def test_unknown_positional(self): - """Tests when an unknown positional arg is given.""" - # When the arg is in the subparsers, expect "invalid choice" errors. - for args in (['X'], - ['X', 'build'], - ['X', 'build', 'image']): - self.check_parse_error(args, 'bdk', invalid_choice='X') - - for args in (['build', 'X'], - ['build', 'X', 'image']): - self.check_parse_error(args, 'bdk build', invalid_choice='X') - - # When the arg is at a leaf command, expect an "unknown argument" error. - self.check_parse_error(['build', 'image', 'X'], 'bdk build image', - unknown_arguments=['X']) - - def test_unknown_optional(self): - """Tests when an unknown optional arg is given. - - In most cases we're able to successfully modify the parsing to make - sure the correct subparser prints the error message, however one - case still doesn't work: - bdk --X build - - Ideally this should print a "bdk" error to match how the rest of - the parsing works, i.e. stop as soon as we see an unknown optional. - However, this particular situation errors out in the very first - call to parse_known_args() and produces a "bdk build" error, so - there's not much we can do outside of trying to parse manually. - - This doesn't happen if a full command is given, for example - `bdk --X build image`, because in that case parse_known_args() is - willing to skip over the unknown optional and we can do our custom - error handling with the result. - """ - # Ideally we'd get "unknown argument" errors here instead of "too few - # arguments", but this isn't feasible while still printing the correct - # subparser, which is the most important thing. - for args in (['--X'], - # ['--X', 'build'] does not work here, see above. - ['--X', 'build', 'image'], - ['--X', 'build', 'platform']): - self.check_parse_error(args, 'bdk', too_few_arguments=True) - - for args in (['build', '--X'], - ['build', '--X', 'image'], - ['build', '--X', 'platform']): - self.check_parse_error(args, 'bdk build', too_few_arguments=True) - - self.check_parse_error(['build', 'image', '--X'], 'bdk build image', - unknown_arguments=['--X']) - - # Final sanity check to make sure 'build platform' does expect a remainder, - # to make sure we're checking both a remainder and non-remainder command. - self.command = 'platform' - self.check_parse(['build', 'platform', '--X'], args=['--X']) + """Tests bdk parsing error handling.""" + + def test_no_command(self): + """Tests when no leaf command is specified.""" + self.check_parse_error([], 'bdk', too_few_arguments=True) + self.check_parse_error(['root'], 'bdk root', too_few_arguments=True) + + def test_unknown_positional(self): + """Tests when an unknown positional arg is given.""" + # When the arg is in the subparsers, expect "invalid choice" errors. + for args in (['X'], + ['X', 'build'], + ['X', 'build', 'image']): + self.check_parse_error(args, 'bdk', invalid_choice='X') + + for args in (['build', 'X'], + ['build', 'X', 'image']): + self.check_parse_error(args, 'bdk build', invalid_choice='X') + + # When the arg is at a leaf command, expect an "unknown argument" error. + self.check_parse_error(['build', 'image', 'X'], 'bdk build image', + unknown_arguments=['X']) + + def test_unknown_optional(self): + """Tests when an unknown optional arg is given. + + In most cases we're able to successfully modify the parsing to make + sure the correct subparser prints the error message, however one + case still doesn't work: + bdk --X build + + Ideally this should print a "bdk" error to match how the rest of + the parsing works, i.e. stop as soon as we see an unknown optional. + However, this particular situation errors out in the very first + call to parse_known_args() and produces a "bdk build" error, so + there's not much we can do outside of trying to parse manually. + + This doesn't happen if a full command is given, for example + `bdk --X build image`, because in that case parse_known_args() is + willing to skip over the unknown optional and we can do our custom + error handling with the result. + """ + # Ideally we'd get "unknown argument" errors here instead of "too few + # arguments", but this isn't feasible while still printing the correct + # subparser, which is the most important thing. + for args in (['--X'], + # ['--X', 'build'] does not work here, see above. + ['--X', 'build', 'image'], + ['--X', 'build', 'platform']): + self.check_parse_error(args, 'bdk', too_few_arguments=True) + + for args in (['build', '--X'], + ['build', '--X', 'image'], + ['build', '--X', 'platform']): + self.check_parse_error(args, 'bdk build', too_few_arguments=True) + + self.check_parse_error(['build', 'image', '--X'], 'bdk build image', + unknown_arguments=['--X']) + + # Final sanity check to make sure 'build platform' does expect a + # remainder, to make sure we're checking both a remainder and + # non-remainder command. + self.command = 'platform' + self.check_parse(['build', 'platform', '--X'], args=['--X']) diff --git a/cli/lib/cli/importutils.py b/cli/lib/cli/importutils.py index bc52428..36ea079 100644 --- a/cli/lib/cli/importutils.py +++ b/cli/lib/cli/importutils.py @@ -16,44 +16,50 @@ """Bdk CLI import utils""" + import imp import inspect import os import uuid + def LoadModule(import_name, path): - module_dir, module_name = os.path.split(path) - mod_info = imp.find_module(module_name, [module_dir]) - try: - f, file_path, items = mod_info - module = imp.load_module(import_name, f, file_path, items) - return module - finally: - if f: - f.close() + module_dir, module_name = os.path.split(path) + mod_info = imp.find_module(module_name, [module_dir]) + try: + f, file_path, items = mod_info + module = imp.load_module(import_name, f, file_path, items) + return module + finally: + if f: + f.close() + def LoadModuleRandName(path): - rand_name = uuid.uuid4().hex - return LoadModule(rand_name, path) + rand_name = uuid.uuid4().hex + return LoadModule(rand_name, path) + def LoadPackage(path): - return LoadModuleRandName(path) + return LoadModuleRandName(path) + def GetPyFilesInPath(path): - for root, _, files in os.walk(path): - for f in files: - if f.endswith('.py'): - yield os.path.join(root, f) + for root, _, files in os.walk(path): + for f in files: + if f.endswith('.py'): + yield os.path.join(root, f) + def ModuleIter(path): - modules = [] - for f in GetPyFilesInPath(path): - modname = inspect.getmodulename(f) - if not modname: - continue - if modname == '__init__': - continue - if modname in modules: - continue - modules.append(modname) - yield LoadModuleRandName(os.path.join(path, modname)) + modules = [] + for f in GetPyFilesInPath(path): + modname = inspect.getmodulename(f) + if not modname: + continue + if modname == '__init__': + continue + if modname in modules: + continue + modules.append(modname) + yield LoadModuleRandName(os.path.join(path, modname)) diff --git a/cli/lib/cli/test_cli/__init__.py b/cli/lib/cli/test_cli/__init__.py index a3d1843..3c47c86 100644 --- a/cli/lib/cli/test_cli/__init__.py +++ b/cli/lib/cli/test_cli/__init__.py @@ -21,4 +21,4 @@ from cli import clicommand class TestGroup(clicommand.Group): - """Test command group.""" + """Test command group.""" diff --git a/cli/lib/cli/test_cli/commands.py b/cli/lib/cli/test_cli/commands.py index c34b608..e204c37 100644 --- a/cli/lib/cli/test_cli/commands.py +++ b/cli/lib/cli/test_cli/commands.py @@ -21,49 +21,49 @@ from cli import clicommand class PositionalOptional(clicommand.Command): - """A command with one positional and one optional arg.""" + """A command with one positional and one optional arg.""" - @staticmethod - def Args(parser): - parser.add_argument('positional') - parser.add_argument('--optional') + @staticmethod + def Args(parser): + parser.add_argument('positional') + parser.add_argument('--optional') - def Run(self, _args): - pass + def Run(self, _args): + pass class Remainder(clicommand.Command): - """A command with remainder args.""" + """A command with remainder args.""" - remainder_arg = ('remainder', 'help text') + remainder_arg = ('remainder', 'help text') - def Run(self, _args): - pass + def Run(self, _args): + pass class PositionalRemainder(clicommand.Command): - """A command with one positional and remainder args.""" + """A command with one positional and remainder args.""" - remainder_arg = ('remainder', 'help text') + remainder_arg = ('remainder', 'help text') - @staticmethod - def Args(parser): - parser.add_argument('positional') + @staticmethod + def Args(parser): + parser.add_argument('positional') - def Run(self, _args): - pass + def Run(self, _args): + pass class OptionalsRemainder(clicommand.Command): - """A command with multiple optionals and a remainder.""" + """A command with multiple optionals and a remainder.""" - remainder_arg = ('remainder', 'help text') + remainder_arg = ('remainder', 'help text') - @staticmethod - def Args(parser): - parser.add_argument('-a', '--flag_a', action='store_true') - parser.add_argument('-b', '--flag_b', action='store_true') - parser.add_argument('-x', '--option_x') + @staticmethod + def Args(parser): + parser.add_argument('-a', '--flag_a', action='store_true') + parser.add_argument('-b', '--flag_b', action='store_true') + parser.add_argument('-x', '--option_x') - def Run(self, _args): - pass + def Run(self, _args): + pass |