コード例 #1
0
ファイル: test_hammer.py プロジェクト: elyezer/robottelo
    def _traverse_command_tree(self, command):
        """Recursively walk through the hammer commands tree and assert that
        the expected options are present.

        """
        output = hammer.parse_help(
            ssh.command('{0} --help'.format(command)).stdout
        )
        command_options = set([option['name'] for option in output['options']])
        command_subcommands = set(
            [subcommand['name'] for subcommand in output['subcommands']]
        )
        if 'discovery_rule' in command and bz_bug_is_open(1219610):
            # Adjust the discovery_rule subcommand name. The expected data is
            # already with the naming convetion name
            expected = _fetch_command_info(
                command.replace('discovery_rule', 'discovery-rule'))
        else:
            expected = _fetch_command_info(command)
        expected_options = set()
        expected_subcommands = set()

        if expected is not None:
            expected_options = set(
                [option['name'] for option in expected['options']]
            )
            expected_subcommands = set(
                [subcommand['name'] for subcommand in expected['subcommands']]
            )

        if command == 'hammer' and bz_bug_is_open(1219610):
            # Adjust the discovery_rule subcommand name
            command_subcommands.discard('discovery_rule')
            command_subcommands.add('discovery-rule')

        added_options = tuple(command_options - expected_options)
        removed_options = tuple(expected_options - command_options)
        added_subcommands = tuple(command_subcommands - expected_subcommands)
        removed_subcommands = tuple(expected_subcommands - command_subcommands)

        if (added_options or added_subcommands or removed_options or
                removed_subcommands):
            diff = {
                'added_command': expected is None,
            }
            if added_options:
                diff['added_options'] = added_options
            if removed_options:
                diff['removed_options'] = removed_options
            if added_subcommands:
                diff['added_subcommands'] = added_subcommands
            if removed_subcommands:
                diff['removed_subcommands'] = removed_subcommands
            self.differences[command] = diff

        if len(output['subcommands']) > 0:
            for subcommand in output['subcommands']:
                self._traverse_command_tree(
                    '{0} {1}'.format(command, subcommand['name'])
                )
コード例 #2
0
ファイル: test_hammer.py プロジェクト: ares/robottelo
    def _traverse_command_tree(self, command):
        """Recursively walk through the hammer commands tree and assert that
        the expected options are present.

        """
        output = hammer.parse_help(
            ssh.command('{0} --help'.format(command)).stdout
        )
        command_options = set([option['name'] for option in output['options']])
        command_subcommands = set(
            [subcommand['name'] for subcommand in output['subcommands']]
        )
        if 'discovery_rule' in command and bz_bug_is_open(1219610):
            # Adjust the discovery_rule subcommand name. The expected data is
            # already with the naming convetion name
            expected = self._fetch_command_info(
                command.replace('discovery_rule', 'discovery-rule'))
        else:
            expected = self._fetch_command_info(command)
        expected_options = set()
        expected_subcommands = set()

        if expected is not None:
            expected_options = set(
                [option['name'] for option in expected['options']]
            )
            expected_subcommands = set(
                [subcommand['name'] for subcommand in expected['subcommands']]
            )

        if command == 'hammer' and bz_bug_is_open(1219610):
            # Adjust the discovery_rule subcommand name
            command_subcommands.discard('discovery_rule')
            command_subcommands.add('discovery-rule')

        added_options = tuple(command_options - expected_options)
        removed_options = tuple(expected_options - command_options)
        added_subcommands = tuple(command_subcommands - expected_subcommands)
        removed_subcommands = tuple(expected_subcommands - command_subcommands)

        if (added_options or added_subcommands or removed_options or
                removed_subcommands):
            diff = {
                'added_command': expected is None,
            }
            if added_options:
                diff['added_options'] = added_options
            if removed_options:
                diff['removed_options'] = removed_options
            if added_subcommands:
                diff['added_subcommands'] = added_subcommands
            if removed_subcommands:
                diff['removed_subcommands'] = removed_subcommands
            self.differences[command] = diff

        if len(output['subcommands']) > 0:
            for subcommand in output['subcommands']:
                self._traverse_command_tree(
                    '{0} {1}'.format(command, subcommand['name'])
                )
コード例 #3
0
    def _traverse_command_tree(self):
        """Walk through the hammer commands tree and assert that the expected
        options are present.

        """
        raw_output = ssh.command(
            'hammer full-help', output_format='plain').stdout
        commands = re.split('.*\n(?=hammer.*\n^[-]+)', raw_output, flags=re.M)
        commands.pop(0)  # remove "Hammer CLI help" line
        for raw_command in commands:
            raw_command = raw_command.splitlines()
            command = raw_command.pop(0).replace(' >', '')
            output = hammer.parse_help(raw_command)
            command_options = set([
                option['name'] for option in output['options']])
            command_subcommands = set(
                [subcommand['name'] for subcommand in output['subcommands']]
            )
            expected = _fetch_command_info(command)
            expected_options = set()
            expected_subcommands = set()

            if expected is not None:
                expected_options = set(
                    [option['name'] for option in expected['options']]
                )
                expected_subcommands = set([
                    subcommand['name']
                    for subcommand in expected['subcommands']
                ])
            if is_open('BZ:1666687'):
                cmds = ['hammer report-template create', 'hammer report-template update']
                if command in cmds:
                    command_options.add('interactive')
                if 'hammer virt-who-config fetch' in command:
                    command_options.add('output')
            added_options = tuple(command_options - expected_options)
            removed_options = tuple(expected_options - command_options)
            added_subcommands = tuple(
                command_subcommands - expected_subcommands)
            removed_subcommands = tuple(
                expected_subcommands - command_subcommands)

            if (added_options or added_subcommands or removed_options or
                    removed_subcommands):
                diff = {
                    'added_command': expected is None,
                }
                if added_options:
                    diff['added_options'] = added_options
                if removed_options:
                    diff['removed_options'] = removed_options
                if added_subcommands:
                    diff['added_subcommands'] = added_subcommands
                if removed_subcommands:
                    diff['removed_subcommands'] = removed_subcommands
                self.differences[command] = diff
コード例 #4
0
def generate_command_tree(command):
    """Recursively walk trhough the hammer commands and subcommands and fetch
    their help. Return a dictionary with the contents.

    """
    output = ssh.command(f'{command} --help').stdout
    contents = hammer.parse_help(output)
    if len(contents['subcommands']) > 0:
        for subcommand in contents['subcommands']:
            subcommand.update(generate_command_tree('{} {}'.format(command, subcommand['name'])))
    return contents
コード例 #5
0
ファイル: test_hammer.py プロジェクト: ColeHiggins2/robottelo
def test_positive_all_options(default_sat):
    """check all provided options for every hammer command

    :id: 1203ab9f-896d-4039-a166-9e2d36925b5b

    :expectedresults: All expected options are present

    :CaseImportance: Critical
    """
    differences = {}
    raw_output = default_sat.execute('hammer full-help').stdout
    commands = re.split(r'.*\n(?=hammer.*\n^[-]+)', raw_output, flags=re.M)
    commands.pop(0)  # remove "Hammer CLI help" line
    for raw_command in commands:
        raw_command = raw_command.splitlines()
        command = raw_command.pop(0).replace(' >', '')
        output = hammer.parse_help(raw_command)
        command_options = {option['name'] for option in output['options']}
        command_subcommands = {
            subcommand['name']
            for subcommand in output['subcommands']
        }
        expected = fetch_command_info(command)
        expected_options = set()
        expected_subcommands = set()

        if expected is not None:
            expected_options = {
                option['name']
                for option in expected['options']
            }
            expected_subcommands = {
                subcommand['name']
                for subcommand in expected['subcommands']
            }
        added_options = tuple(command_options - expected_options)
        removed_options = tuple(expected_options - command_options)
        added_subcommands = tuple(command_subcommands - expected_subcommands)
        removed_subcommands = tuple(expected_subcommands - command_subcommands)

        if added_options or added_subcommands or removed_options or removed_subcommands:
            diff = {'added_command': expected is None}
            if added_options:
                diff['added_options'] = added_options
            if removed_options:
                diff['removed_options'] = removed_options
            if added_subcommands:
                diff['added_subcommands'] = added_subcommands
            if removed_subcommands:
                diff['removed_subcommands'] = removed_subcommands
            differences[command] = diff

    if differences:
        pytest.fail(format_commands_diff(diff))
コード例 #6
0
ファイル: test_hammer.py プロジェクト: BlackSmith/robottelo
    def _traverse_command_tree(self):
        """Walk through the hammer commands tree and assert that the expected
        options are present.

        """
        raw_output = ssh.command(
            'hammer full-help', output_format='plain').stdout
        commands = re.split('.*\n(?=hammer.*\n^[-]+)', raw_output, flags=re.M)
        commands.pop(0)  # remove "Hammer CLI help" line
        for raw_command in commands:
            raw_command = raw_command.splitlines()
            command = raw_command.pop(0).replace(' >', '')
            output = hammer.parse_help(raw_command)
            command_options = set([
                option['name'] for option in output['options']])
            command_subcommands = set(
                [subcommand['name'] for subcommand in output['subcommands']]
            )
            expected = _fetch_command_info(command)
            expected_options = set()
            expected_subcommands = set()

            if expected is not None:
                expected_options = set(
                    [option['name'] for option in expected['options']]
                )
                expected_subcommands = set([
                    subcommand['name']
                    for subcommand in expected['subcommands']
                ])

            added_options = tuple(command_options - expected_options)
            removed_options = tuple(expected_options - command_options)
            added_subcommands = tuple(
                command_subcommands - expected_subcommands)
            removed_subcommands = tuple(
                expected_subcommands - command_subcommands)

            if (added_options or added_subcommands or removed_options or
                    removed_subcommands):
                diff = {
                    'added_command': expected is None,
                }
                if added_options:
                    diff['added_options'] = added_options
                if removed_options:
                    diff['removed_options'] = removed_options
                if added_subcommands:
                    diff['added_subcommands'] = added_subcommands
                if removed_subcommands:
                    diff['removed_subcommands'] = removed_subcommands
                self.differences[command] = diff
コード例 #7
0
def generate_command_tree(command):
    """Recursively walk trhough the hammer commands and subcommands and fetch
    their help. Return a dictionary with the contents.

    """
    output = ssh.command('{0} --help'.format(command)).stdout
    contents = hammer.parse_help(output)
    if len(contents['subcommands']) > 0:
        for subcommand in contents['subcommands']:
            subcommand.update(generate_command_tree(
                '{0} {1}'.format(command, subcommand['name'])
            ))
    return contents
コード例 #8
0
ファイル: test_hammer.py プロジェクト: lpramuk/robottelo
def traverse_command_tree():
    """Walk through the hammer commands tree and assert that the expected
    options are present.

    """
    differences = {}
    raw_output = ssh.command('hammer full-help', output_format='plain').stdout
    commands = re.split(r'.*\n(?=hammer.*\n^[-]+)', raw_output, flags=re.M)
    commands.pop(0)  # remove "Hammer CLI help" line
    for raw_command in commands:
        raw_command = raw_command.splitlines()
        command = raw_command.pop(0).replace(' >', '')
        output = hammer.parse_help(raw_command)
        command_options = {option['name'] for option in output['options']}
        command_subcommands = {subcommand['name'] for subcommand in output['subcommands']}
        expected = fetch_command_info(command)
        expected_options = set()
        expected_subcommands = set()

        if expected is not None:
            expected_options = {option['name'] for option in expected['options']}
            expected_subcommands = {subcommand['name'] for subcommand in expected['subcommands']}
        added_options = tuple(command_options - expected_options)
        removed_options = tuple(expected_options - command_options)
        added_subcommands = tuple(command_subcommands - expected_subcommands)
        removed_subcommands = tuple(expected_subcommands - command_subcommands)

        if added_options or added_subcommands or removed_options or removed_subcommands:
            diff = {'added_command': expected is None}
            if added_options:
                diff['added_options'] = added_options
            if removed_options:
                diff['removed_options'] = removed_options
            if added_subcommands:
                diff['added_subcommands'] = added_subcommands
            if removed_subcommands:
                diff['removed_subcommands'] = removed_subcommands
            differences[command] = diff

    return differences
コード例 #9
0
ファイル: test_hammer.py プロジェクト: blrm/robottelo
    def test_parse_help(self):
        """Can parse hammer help output"""
        self.maxDiff = None
        output = [
            'Usage:',
            '    hammer [OPTIONS] SUBCOMMAND [ARG] ...',
            '',
            'Parameters:',
            'SUBCOMMAND                    subcommand',
            '[ARG] ...                     subcommand arguments',
            '',
            'Subcommands:',
            ' activation-key                Manipulate activation keys.',
            ' capsule                       Manipulate capsule',
            ' compute-resource              Manipulate compute resources.',
            ' content-host                  Manipulate content hosts on the',
            '                               server',
            ' gpg                           Manipulate GPG Key actions on the',
            '                               server',

            'Options:',
            ' --autocomplete LINE           Get list of possible endings',
            ' --name, --deprecation-name    An option with a deprecation name',
            ' --csv                         Output as CSV (same as',
            '                               --output=csv)',
            ' --csv-separator SEPARATOR     Character to separate the values',
            ' --output ADAPTER              Set output format. One of [base,',
            '                               table, silent, csv, yaml, json]',
            ' -p, --password PASSWORD       password to access the remote',
            '                               system',
            ' -r, --reload-cache            force reload of Apipie cache',
        ]
        self.assertEqual(
            hammer.parse_help(output),
            {
                'subcommands': [
                    {
                        'name': 'activation-key',
                        'description': 'Manipulate activation keys.',
                    },
                    {
                        'name': 'capsule',
                        'description': 'Manipulate capsule',
                    },
                    {
                        'name': 'compute-resource',
                        'description': 'Manipulate compute resources.',
                    },
                    {
                        'name': 'content-host',
                        'description': (
                            'Manipulate content hosts on the server'
                        ),
                    },
                    {
                        'name': 'gpg',
                        'description': (
                            'Manipulate GPG Key actions on the server'
                        ),
                    },
                ],
                'options': [
                    {
                        'name': 'autocomplete',
                        'shortname': None,
                        'value': 'LINE',
                        'help': 'Get list of possible endings',
                    },
                    {
                        'name': 'name',
                        'shortname': None,
                        'value': None,
                        'help': 'An option with a deprecation name',
                    },
                    {
                        'name': 'csv',
                        'shortname': None,
                        'value': None,
                        'help': 'Output as CSV (same as --output=csv)',
                    },
                    {
                        'name': 'csv-separator',
                        'shortname': None,
                        'value': 'SEPARATOR',
                        'help': 'Character to separate the values',
                    },
                    {
                        'name': 'output',
                        'shortname': None,
                        'value': 'ADAPTER',
                        'help': (
                            'Set output format. One of [base, table, silent, '
                            'csv, yaml, json]'
                        ),
                    },
                    {
                        'name': 'password',
                        'shortname': 'p',
                        'value': 'PASSWORD',
                        'help': 'password to access the remote system',
                    },
                    {
                        'name': 'reload-cache',
                        'shortname': 'r',
                        'value': None,
                        'help': 'force reload of Apipie cache',
                    },
                ],
            }
        )
コード例 #10
0
ファイル: test_hammer.py プロジェクト: tstrych/robottelo
 def test_parse_help(self):
     """Can parse hammer help output"""
     self.maxDiff = None
     output = [
         'Usage:',
         '    hammer [OPTIONS] SUBCOMMAND [ARG] ...',
         '',
         'Parameters:',
         'SUBCOMMAND                    subcommand',
         '[ARG] ...                     subcommand arguments',
         '',
         'Subcommands:',
         ' activation-key                Manipulate activation keys.',
         ' capsule                       Manipulate capsule',
         ' compute-resource              Manipulate compute resources.',
         ' content-host                  Manipulate content hosts on the',
         '                               server',
         ' gpg                           Manipulate GPG Key actions on the',
         '                               server',
         ' list, index                   List all architectures',
         'Options:',
         ' --autocomplete LINE           Get list of possible endings',
         ' --name, --deprecation-name    An option with a deprecation name',
         ' --csv                         Output as CSV (same as',
         '                               --output=csv)',
         ' --csv-separator SEPARATOR     Character to separate the values',
         ' --output ADAPTER              Set output format. One of [base,',
         '                               table, silent, csv, yaml, json]',
         ' -p, --password PASSWORD       password to access the remote',
         '                               system',
         ' -r, --reload-cache            force reload of Apipie cache',
         ' -v, --[no-]verbose            Be verbose (or not). True by default',
         (' --location[-id|-title]        Set the current location context for'
          ' the request. Name/Title/Id can be used'),
         ' --location[s|-ids|-titles]    REPLACE locations with given Names/Titles/Ids',
         '                               Comma separated list of values.',
         (' --lifecycle-environment[-id]  Set the current environment context'
          ' for the request. Name/Id can be used'),
     ]
     assert hammer.parse_help(output) == {
         'subcommands': [
             {
                 'name': 'activation-key',
                 'description': 'Manipulate activation keys.'
             },
             {
                 'name': 'capsule',
                 'description': 'Manipulate capsule'
             },
             {
                 'name': 'compute-resource',
                 'description': 'Manipulate compute resources.'
             },
             {
                 'name': 'content-host',
                 'description': ('Manipulate content hosts on the server'),
             },
             {
                 'name': 'gpg',
                 'description': ('Manipulate GPG Key actions on the server')
             },
             {
                 'name': 'list',
                 'description': ('List all architectures')
             },
         ],
         'options': [
             {
                 'name': 'autocomplete',
                 'shortname': None,
                 'value': 'LINE',
                 'help': 'Get list of possible endings',
             },
             {
                 'name': 'name',
                 'shortname': None,
                 'value': None,
                 'help': 'An option with a deprecation name',
             },
             {
                 'name': 'csv',
                 'shortname': None,
                 'value': None,
                 'help': 'Output as CSV (same as --output=csv)',
             },
             {
                 'name': 'csv-separator',
                 'shortname': None,
                 'value': 'SEPARATOR',
                 'help': 'Character to separate the values',
             },
             {
                 'name':
                 'output',
                 'shortname':
                 None,
                 'value':
                 'ADAPTER',
                 'help':
                 ('Set output format. One of [base, table, silent, csv, yaml, json]'
                  ),
             },
             {
                 'name': 'password',
                 'shortname': 'p',
                 'value': 'PASSWORD',
                 'help': 'password to access the remote system',
             },
             {
                 'name': 'reload-cache',
                 'shortname': 'r',
                 'value': None,
                 'help': 'force reload of Apipie cache',
             },
             {
                 'name': 'verbose',
                 'shortname': 'v',
                 'value': None,
                 'help': 'Be verbose (or not). True by default',
             },
             {
                 'name':
                 'location',
                 'shortname':
                 None,
                 'value':
                 None,
                 'help':
                 ('Set the current location context for the request.'
                  ' Name/Title/Id can be used'),
             },
             {
                 'name':
                 'location-id',
                 'shortname':
                 None,
                 'value':
                 None,
                 'help':
                 ('Set the current location context for the request.'
                  ' Name/Title/Id can be used'),
             },
             {
                 'name':
                 'location-title',
                 'shortname':
                 None,
                 'value':
                 None,
                 'help':
                 ('Set the current location context for the request.'
                  ' Name/Title/Id can be used'),
             },
             {
                 'name':
                 'locations',
                 'shortname':
                 None,
                 'value':
                 None,
                 'help': ('REPLACE locations with given Names/Titles/Ids'
                          ' Comma separated list of values.'),
             },
             {
                 'name':
                 'location-ids',
                 'shortname':
                 None,
                 'value':
                 None,
                 'help': ('REPLACE locations with given Names/Titles/Ids'
                          ' Comma separated list of values.'),
             },
             {
                 'name':
                 'location-titles',
                 'shortname':
                 None,
                 'value':
                 None,
                 'help': ('REPLACE locations with given Names/Titles/Ids'
                          ' Comma separated list of values.'),
             },
             {
                 'name':
                 'lifecycle-environment',
                 'shortname':
                 None,
                 'value':
                 None,
                 'help':
                 ('Set the current environment context for the request.'
                  ' Name/Id can be used'),
             },
             {
                 'name':
                 'lifecycle-environment-id',
                 'shortname':
                 None,
                 'value':
                 None,
                 'help':
                 ('Set the current environment context for the request.'
                  ' Name/Id can be used'),
             },
         ],
     }
コード例 #11
0
 def test_parse_help(self):
     """Can parse hammer help output"""
     self.maxDiff = None
     output = [
         'Usage:',
         '    hammer [OPTIONS] SUBCOMMAND [ARG] ...',
         '',
         'Parameters:',
         'SUBCOMMAND                    subcommand',
         '[ARG] ...                     subcommand arguments',
         '',
         'Subcommands:',
         ' activation-key                Manipulate activation keys.',
         ' capsule                       Manipulate capsule',
         ' compute-resource              Manipulate compute resources.',
         ' content-host                  Manipulate content hosts on the',
         '                               server',
         ' gpg                           Manipulate GPG Key actions on the',
         '                               server',
         'Options:',
         ' --autocomplete LINE           Get list of possible endings',
         ' --name, --deprecation-name    An option with a deprecation name',
         ' --csv                         Output as CSV (same as',
         '                               --output=csv)',
         ' --csv-separator SEPARATOR     Character to separate the values',
         ' --output ADAPTER              Set output format. One of [base,',
         '                               table, silent, csv, yaml, json]',
         ' -p, --password PASSWORD       password to access the remote',
         '                               system',
         ' -r, --reload-cache            force reload of Apipie cache',
     ]
     self.assertEqual(
         hammer.parse_help(output), {
             'subcommands': [
                 {
                     'name': 'activation-key',
                     'description': 'Manipulate activation keys.',
                 },
                 {
                     'name': 'capsule',
                     'description': 'Manipulate capsule',
                 },
                 {
                     'name': 'compute-resource',
                     'description': 'Manipulate compute resources.',
                 },
                 {
                     'name': 'content-host',
                     'description':
                     ('Manipulate content hosts on the server'),
                 },
                 {
                     'name':
                     'gpg',
                     'description':
                     ('Manipulate GPG Key actions on the server'),
                 },
             ],
             'options': [
                 {
                     'name': 'autocomplete',
                     'shortname': None,
                     'value': 'LINE',
                     'help': 'Get list of possible endings',
                 },
                 {
                     'name': 'name',
                     'shortname': None,
                     'value': None,
                     'help': 'An option with a deprecation name',
                 },
                 {
                     'name': 'csv',
                     'shortname': None,
                     'value': None,
                     'help': 'Output as CSV (same as --output=csv)',
                 },
                 {
                     'name': 'csv-separator',
                     'shortname': None,
                     'value': 'SEPARATOR',
                     'help': 'Character to separate the values',
                 },
                 {
                     'name':
                     'output',
                     'shortname':
                     None,
                     'value':
                     'ADAPTER',
                     'help':
                     ('Set output format. One of [base, table, silent, '
                      'csv, yaml, json]'),
                 },
                 {
                     'name': 'password',
                     'shortname': 'p',
                     'value': 'PASSWORD',
                     'help': 'password to access the remote system',
                 },
                 {
                     'name': 'reload-cache',
                     'shortname': 'r',
                     'value': None,
                     'help': 'force reload of Apipie cache',
                 },
             ],
         })
コード例 #12
0
    def _traverse_command_tree(self):
        """Walk through the hammer commands tree and assert that the expected
        options are present.

        """
        raw_output = ssh.command(
            'hammer full-help', output_format='plain').stdout
        commands = re.split('.*\n(?=hammer.*\n^[-]+)', raw_output, flags=re.M)
        commands.pop(0)  # remove "Hammer CLI help" line
        for raw_command in commands:
            raw_command = raw_command.splitlines()
            command = raw_command.pop(0).replace(' >', '')
            output = hammer.parse_help(raw_command)
            command_options = set([
                option['name'] for option in output['options']])
            command_subcommands = set(
                [subcommand['name'] for subcommand in output['subcommands']]
            )
            expected = _fetch_command_info(command)
            expected_options = set()
            expected_subcommands = set()

            if expected is not None:
                expected_options = set(
                    [option['name'] for option in expected['options']]
                )
                expected_subcommands = set([
                    subcommand['name']
                    for subcommand in expected['subcommands']
                ])
            # Below code is added as workaround for Bug 1666687
            if bz_bug_is_open(1666687):
                cmds = ['hammer report-template create', 'hammer report-template update']
                if command in cmds:
                    command_options.add('interactive')
                if 'hammer virt-who-config fetch' in command:
                    command_options.add('output')
            # Below code is added as workaround for Bug 1655513 on Sat 6.4 release
            # This will neglect null entry added for hammer ansible roles command
            if bz_bug_is_open(1655513) and 'hammer ansible roles ' in command and 'help' in (
                    command_options - expected_options):
                expected_options.add("help")
            added_options = tuple(command_options - expected_options)
            removed_options = tuple(expected_options - command_options)
            added_subcommands = tuple(
                command_subcommands - expected_subcommands)
            removed_subcommands = tuple(
                expected_subcommands - command_subcommands)

            if (added_options or added_subcommands or removed_options or
                    removed_subcommands):
                diff = {
                    'added_command': expected is None,
                }
                if added_options:
                    diff['added_options'] = added_options
                if removed_options:
                    diff['removed_options'] = removed_options
                if added_subcommands:
                    diff['added_subcommands'] = added_subcommands
                if removed_subcommands:
                    diff['removed_subcommands'] = removed_subcommands
                self.differences[command] = diff