aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSteve Fung <stevefung@google.com>2016-04-18 02:33:27 -0700
committerSteve Fung <stevefung@google.com>2016-04-18 18:46:14 +0000
commitb608a3dc11dfd9e9f6e82c52b162c30228e8fa72 (patch)
treed8dc2dba0e548d48306e3c5be407d7876d9c37a7
parentf17f2c66532aa2386f6c9fe9d8e9ab2810b0dafb (diff)
downloadbdk-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__.py2
-rw-r--r--cli/lib/cli/clicommand.py428
-rw-r--r--cli/lib/cli/clicommand_unittest.py298
-rw-r--r--cli/lib/cli/climanager.py453
-rw-r--r--cli/lib/cli/climanager_unittest.py570
-rw-r--r--cli/lib/cli/importutils.py60
-rw-r--r--cli/lib/cli/test_cli/__init__.py2
-rw-r--r--cli/lib/cli/test_cli/commands.py54
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