flaky test: src/tests/maasperf/cli/test_machines.py::test_perf_list_machines_CLI

Bug #2020865 reported by Alberto Donato
6
This bug affects 1 person
Affects Status Importance Assigned to Milestone
MAAS
Fix Released
Medium
Adam Collard

Bug Description

The test fails if the random profile name starts with a `-`, which is interpreted as an option:

_________________________ test_perf_list_machines_CLI __________________________
 [gw3] linux -- Python 3.10.6 /run/build/checkout/.ve/bin/python3

 self = ArgumentParser(prog='maas', usage=None, description='', formatter_class=<class 'argparse.RawDescriptionHelpFormatter'>, conflict_handler='error', add_help=True)
 args = ['-uNIDFeflx', 'machines', 'read'], namespace = Namespace(debug=False)

     def parse_known_args(self, args=None, namespace=None):
         if args is None:
             # args default to the system args
             args = _sys.argv[1:]
         else:
             # make sure that args are mutable
             args = list(args)

         # default Namespace built from parser defaults
         if namespace is None:
             namespace = Namespace()

         # add any action defaults that aren't present
         for action in self._actions:
             if action.dest is not SUPPRESS:
                 if not hasattr(namespace, action.dest):
                     if action.default is not SUPPRESS:
                         setattr(namespace, action.dest, action.default)

         # add any parser defaults that aren't present
         for dest in self._defaults:
             if not hasattr(namespace, dest):
                 setattr(namespace, dest, self._defaults[dest])

         # parse the arguments and exit if there are any errors
         if self.exit_on_error:
             try:
 > namespace, args = self._parse_known_args(args, namespace)

 /usr/lib/python3.10/argparse.py:1871:
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

 self = ArgumentParser(prog='maas', usage=None, description='', formatter_class=<class 'argparse.RawDescriptionHelpFormatter'>, conflict_handler='error', add_help=True)
 arg_strings = ['-uNIDFeflx', 'machines', 'read']
 namespace = Namespace(debug=False)

     def _parse_known_args(self, arg_strings, namespace):
         # replace arg strings that are file references
         if self.fromfile_prefix_chars is not None:
             arg_strings = self._read_args_from_files(arg_strings)

         # map all mutually exclusive arguments to the other arguments
         # they can't occur with
         action_conflicts = {}
         for mutex_group in self._mutually_exclusive_groups:
             group_actions = mutex_group._group_actions
             for i, mutex_action in enumerate(mutex_group._group_actions):
                 conflicts = action_conflicts.setdefault(mutex_action, [])
                 conflicts.extend(group_actions[:i])
                 conflicts.extend(group_actions[i + 1:])

         # find all option indices, and determine the arg_string_pattern
         # which has an 'O' if there is an option at an index,
         # an 'A' if there is an argument, or a '-' if there is a '--'
         option_string_indices = {}
         arg_string_pattern_parts = []
         arg_strings_iter = iter(arg_strings)
         for i, arg_string in enumerate(arg_strings_iter):

             # all args after -- are non-options
             if arg_string == '--':
                 arg_string_pattern_parts.append('-')
                 for arg_string in arg_strings_iter:
                     arg_string_pattern_parts.append('A')

             # otherwise, add the arg to the arg strings
             # and note the index if it was an option
             else:
                 option_tuple = self._parse_optional(arg_string)
                 if option_tuple is None:
                     pattern = 'A'
                 else:
                     option_string_indices[i] = option_tuple
                     pattern = 'O'
                 arg_string_pattern_parts.append(pattern)

         # join the pieces together to form the pattern
         arg_strings_pattern = ''.join(arg_string_pattern_parts)

         # converts arg strings to the appropriate and then takes the action
         seen_actions = set()
         seen_non_default_actions = set()

         def take_action(action, argument_strings, option_string=None):
             seen_actions.add(action)
             argument_values = self._get_values(action, argument_strings)

             # error if this argument is not allowed with other previously
             # seen arguments, assuming that actions that use the default
             # value don't really count as "present"
             if argument_values is not action.default:
                 seen_non_default_actions.add(action)
                 for conflict_action in action_conflicts.get(action, []):
                     if conflict_action in seen_non_default_actions:
                         msg = _('not allowed with argument %s')
                         action_name = _get_action_name(conflict_action)
                         raise ArgumentError(action, msg % action_name)

             # take the action if we didn't receive a SUPPRESS value
             # (e.g. from a default)
             if argument_values is not SUPPRESS:
                 action(self, namespace, argument_values, option_string)

         # function to convert arg_strings into an optional action
         def consume_optional(start_index):

             # get the optional identified at this index
             option_tuple = option_string_indices[start_index]
             action, option_string, explicit_arg = option_tuple

             # identify additional optionals in the same arg string
             # (e.g. -xyz is the same as -x -y -z if no args are required)
             match_argument = self._match_argument
             action_tuples = []
             while True:

                 # if we found no optional action, skip it
                 if action is None:
                     extras.append(arg_strings[start_index])
                     return start_index + 1

                 # if there is an explicit argument, try to match the
                 # optional's string arguments to only this
                 if explicit_arg is not None:
                     arg_count = match_argument(action, 'A')

                     # if the action is a single-dash option and takes no
                     # arguments, try to parse more single-dash options out
                     # of the tail of the option string
                     chars = self.prefix_chars
                     if arg_count == 0 and option_string[1] not in chars:
                         action_tuples.append((action, [], option_string))
                         char = option_string[0]
                         option_string = char + explicit_arg[0]
                         new_explicit_arg = explicit_arg[1:] or None
                         optionals_map = self._option_string_actions
                         if option_string in optionals_map:
                             action = optionals_map[option_string]
                             explicit_arg = new_explicit_arg
                         else:
                             msg = _('ignored explicit argument %r')
                             raise ArgumentError(action, msg % explicit_arg)

                     # if the action expect exactly one argument, we've
                     # successfully matched the option; exit the loop
                     elif arg_count == 1:
                         stop = start_index + 1
                         args = [explicit_arg]
                         action_tuples.append((action, args, option_string))
                         break

                     # error if a double-dash option did not use the
                     # explicit argument
                     else:
                         msg = _('ignored explicit argument %r')
                         raise ArgumentError(action, msg % explicit_arg)

                 # if there is no explicit argument, try to match the
                 # optional's string arguments with the following strings
                 # if successful, exit the loop
                 else:
                     start = start_index + 1
                     selected_patterns = arg_strings_pattern[start:]
                     arg_count = match_argument(action, selected_patterns)
                     stop = start + arg_count
                     args = arg_strings[start:stop]
                     action_tuples.append((action, args, option_string))
                     break

             # add the Optional to the list and return the index at which
             # the Optional's string args stopped
             assert action_tuples
             for action, args, option_string in action_tuples:
                 take_action(action, args, option_string)
             return stop

         # the list of Positionals left to be parsed; this is modified
         # by consume_positionals()
         positionals = self._get_positional_actions()

         # function to convert arg_strings into positional actions
         def consume_positionals(start_index):
             # match as many Positionals as possible
             match_partial = self._match_arguments_partial
             selected_pattern = arg_strings_pattern[start_index:]
             arg_counts = match_partial(positionals, selected_pattern)

             # slice off the appropriate arg strings for each Positional
             # and add the Positional and its args to the list
             for action, arg_count in zip(positionals, arg_counts):
                 args = arg_strings[start_index: start_index + arg_count]
                 start_index += arg_count
                 take_action(action, args)

             # slice off the Positionals that we just parsed and return the
             # index at which the Positionals' string args stopped
             positionals[:] = positionals[len(arg_counts):]
             return start_index

         # consume Positionals and Optionals alternately, until we have
         # passed the last option string
         extras = []
         start_index = 0
         if option_string_indices:
             max_option_string_index = max(option_string_indices)
         else:
             max_option_string_index = -1
         while start_index <= max_option_string_index:

             # consume any Positionals preceding the next option
             next_option_string_index = min([
                 index
                 for index in option_string_indices
                 if index >= start_index])
             if start_index != next_option_string_index:
                 positionals_end_index = consume_positionals(start_index)

                 # only try to parse the next optional if we didn't consume
                 # the option string during the positionals parsing
                 if positionals_end_index > start_index:
                     start_index = positionals_end_index
                     continue
                 else:
                     start_index = positionals_end_index

             # if we consumed all the positionals we could and we're not
             # at the index of an option string, there were extra arguments
             if start_index not in option_string_indices:
                 strings = arg_strings[start_index:next_option_string_index]
                 extras.extend(strings)
                 start_index = next_option_string_index

             # consume the next optional and any arguments for it
             start_index = consume_optional(start_index)

         # consume any positionals following the last Optional
 > stop_index = consume_positionals(start_index)

 /usr/lib/python3.10/argparse.py:2083:
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

 start_index = 3

     def consume_positionals(start_index):
         # match as many Positionals as possible
         match_partial = self._match_arguments_partial
         selected_pattern = arg_strings_pattern[start_index:]
         arg_counts = match_partial(positionals, selected_pattern)

         # slice off the appropriate arg strings for each Positional
         # and add the Positional and its args to the list
         for action, arg_count in zip(positionals, arg_counts):
             args = arg_strings[start_index: start_index + arg_count]
             start_index += arg_count
 > take_action(action, args)

 /usr/lib/python3.10/argparse.py:2039:
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

 action = _SubParsersAction(option_strings=[], dest='==SUPPRESS==', nargs='A...', const=None, default=None, type=None, choices={...RawDescriptionHelpFormatter'>, conflict_handler='error', add_help=True)}, required=False, help=None, metavar='COMMAND')
 argument_strings = ['machines', 'read'], option_string = None

     def take_action(action, argument_strings, option_string=None):
         seen_actions.add(action)
 > argument_values = self._get_values(action, argument_strings)

 /usr/lib/python3.10/argparse.py:1932:
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

 self = ArgumentParser(prog='maas', usage=None, description='', formatter_class=<class 'argparse.RawDescriptionHelpFormatter'>, conflict_handler='error', add_help=True)
 action = _SubParsersAction(option_strings=[], dest='==SUPPRESS==', nargs='A...', const=None, default=None, type=None, choices={...RawDescriptionHelpFormatter'>, conflict_handler='error', add_help=True)}, required=False, help=None, metavar='COMMAND')
 arg_strings = ['machines', 'read']

     def _get_values(self, action, arg_strings):
         # for everything but PARSER, REMAINDER args, strip out first '--'
         if action.nargs not in [PARSER, REMAINDER]:
             try:
                 arg_strings.remove('--')
             except ValueError:
                 pass

         # optional argument produces a default when not present
         if not arg_strings and action.nargs == OPTIONAL:
             if action.option_strings:
                 value = action.const
             else:
                 value = action.default
             if isinstance(value, str):
                 value = self._get_value(action, value)
                 self._check_value(action, value)

         # when nargs='*' on a positional, if there were no command-line
         # args, use the default if it is anything other than None
         elif (not arg_strings and action.nargs == ZERO_OR_MORE and
               not action.option_strings):
             if action.default is not None:
                 value = action.default
             else:
                 value = arg_strings
             self._check_value(action, value)

         # single argument or optional argument produces a single value
         elif len(arg_strings) == 1 and action.nargs in [None, OPTIONAL]:
             arg_string, = arg_strings
             value = self._get_value(action, arg_string)
             self._check_value(action, value)

         # REMAINDER arguments convert all values, checking none
         elif action.nargs == REMAINDER:
             value = [self._get_value(action, v) for v in arg_strings]

         # PARSER arguments convert all values, but check only the first
         elif action.nargs == PARSER:
             value = [self._get_value(action, v) for v in arg_strings]
 > self._check_value(action, value[0])

 /usr/lib/python3.10/argparse.py:2473:
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

 self = ArgumentParser(prog='maas', usage=None, description='', formatter_class=<class 'argparse.RawDescriptionHelpFormatter'>, conflict_handler='error', add_help=True)
 action = _SubParsersAction(option_strings=[], dest='==SUPPRESS==', nargs='A...', const=None, default=None, type=None, choices={...RawDescriptionHelpFormatter'>, conflict_handler='error', add_help=True)}, required=False, help=None, metavar='COMMAND')
 value = 'machines'

     def _check_value(self, action, value):
         # converted value must be one of the choices (if specified)
         if action.choices is not None and value not in action.choices:
             args = {'value': value,
                     'choices': ', '.join(map(repr, action.choices))}
             msg = _('invalid choice: %(value)r (choose from %(choices)s)')
 > raise ArgumentError(action, msg % args)
 E argparse.ArgumentError: argument COMMAND: invalid choice: 'machines' (choose from 'login', 'logout', 'list', 'refresh', 'init', 'apikey', 'configauth', 'config-tls', 'config-vault', 'createadmin', 'changepassword', '-uNIDFeflx')

 /usr/lib/python3.10/argparse.py:2520: ArgumentError

 During handling of the above exception, another exception occurred:

 perf = <maastesting.pytest.perftest.PerfTester object at 0x7f21c7361fc0>
 cli_profile = {'credentials': ['vDF3XGWUnRUVVsDeqU', '3asdd5Gj7NDH7mv7GD', 'FCkecN7FEKXEjyYb2WraKs8GkFn5S2X9'], 'description': {'doc...stem_id', 'id'), ...}, 'name': 'BlockDeviceHandler'}, ...]}, 'name': '-uNIDFeflx', 'url': 'http://localhost:5240/MAAS'}
 monkeypatch = <_pytest.monkeypatch.MonkeyPatch object at 0x7f21c6c15360>
 cli_machines_api_response = <HttpResponse status_code=200, "text/html; charset=utf-8">

     @pytest.mark.usefixtures("maasdb")
     def test_perf_list_machines_CLI(
         perf, cli_profile, monkeypatch, cli_machines_api_response
     ):
         @contextmanager
         def mock_ProfileConfig_enter(*args):
             yield {cli_profile["name"]: cli_profile}

         def mock_http_response(*args, **kwargs):
             return (
                 Response(
                     {
                         key: value
                         for (key, value) in cli_machines_api_response.items()
                         if key != "content"
                     }
                 ),
                 cli_machines_api_response.content,
             )

         monkeypatch.setattr(ProfileConfig, "open", mock_ProfileConfig_enter)
         monkeypatch.setattr(api, "http_request", mock_http_response)

         args = ["maas", cli_profile["name"], "machines", "read"]
         with perf.record("test_perf_list_machines_CLI"):
             parser = prepare_parser(args)
             with perf.record("test_perf_list_machines_CLI.parse"):
 > options = parser.parse_args(args[1:])

 src/tests/maasperf/cli/test_machines.py:41:
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
 /usr/lib/python3.10/argparse.py:1838: in parse_args
     args, argv = self.parse_known_args(args, namespace)
 /usr/lib/python3.10/argparse.py:1874: in parse_known_args
     self.error(str(err))
 _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

 self = ArgumentParser(prog='maas', usage=None, description='', formatter_class=<class 'argparse.RawDescriptionHelpFormatter'>, conflict_handler='error', add_help=True)
 message = "argument COMMAND: invalid choice: 'machines' (choose from 'login', 'logout', 'list', 'refresh', 'init', 'apikey', 'configauth', 'config-tls', 'config-vault', 'createadmin', 'changepassword', '-uNIDFeflx')"

     def error(self, message):
         """Make the default error messages more helpful

         Override default ArgumentParser error method to print the help menu
         generated by ArgumentParser instead of just printing out a list of
         valid arguments.
         """
         self.print_help(sys.stderr)
         self._print_error("\n" + message + "\n")
 > sys.exit(2)
 E SystemExit: 2

 src/maascli/parser.py:64: SystemExit
 ---------------------------- Captured stdout setup -----------------------------
 Operations to perform:
   Apply all migrations: auth, contenttypes, maasserver, metadataserver, piston3, sessions, sites
 Running migrations:
   No migrations to apply.
 ----------------------------- Captured stderr call -----------------------------
 usage: maas [-h] COMMAND ...

 options:
   -h, --help show this help message and exit

 drill down:
   COMMAND
     login Log in to a remote API, and remember its description and
                   credentials.
     logout Log out of a remote API, purging any stored credentials.
     list List remote APIs that have been logged-in to.
     refresh Refresh the API descriptions of all profiles.
     init Initialize controller.
     apikey Used to manage a user's API keys. Shows existing keys unless
                   --generate or --delete is passed.
     configauth Configure external authentication.
     config-tls Configure MAAS Region TLS.
     config-vault Configure MAAS Region Vault integration.
     createadmin Create a MAAS administrator account.
     changepassword
                   Change a MAAS user's password.
     -uNIDFeflx Interact with http://localhost:5240/MAAS

 https://maas.io/

 argument COMMAND: invalid choice: 'machines' (choose from 'login', 'logout', 'list', 'refresh', 'init', 'apikey', 'configauth', 'config-tls', 'config-vault', 'createadmin', 'changepassword', '-uNIDFeflx')

Related branches

Changed in maas:
assignee: nobody → Adam Collard (adam-collard)
Changed in maas:
status: Triaged → Fix Committed
Alberto Donato (ack)
Changed in maas:
milestone: 3.4.0 → 3.4.0-beta3
Alberto Donato (ack)
Changed in maas:
status: Fix Committed → Fix Released
To post a comment you must log in.
This report contains Public information  
Everyone can see this information.

Other bug subscribers

Remote bug watches

Bug watches keep track of this bug in other bug trackers.