| # Copyright (c) 2020 ARM Limited |
| # All rights reserved |
| # |
| # The license below extends only to copyright in the software and shall |
| # not be construed as granting a license to any other intellectual |
| # property including but not limited to intellectual property relating |
| # to a hardware implementation of the functionality of the software |
| # licensed hereunder. You may use the software subject to the license |
| # terms below provided that you ensure that this notice is replicated |
| # unmodified and in its entirety in all distributions of the software, |
| # modified or unmodified, in source code or in binary form. |
| # |
| # Copyright (c) 2017 Mark D. Hill and David A. Wood |
| # All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer; |
| # redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution; |
| # neither the name of the copyright holders nor the names of its |
| # contributors may be used to endorse or promote products derived from |
| # this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| # |
| # Authors: Sean Wilson |
| |
| ''' |
| Global configuration module which exposes two types of configuration |
| variables: |
| |
| 1. config |
| 2. constants (Also attached to the config variable as an attribute) |
| |
| The main motivation for this module is to have a centralized location for |
| defaults and configuration by command line and files for the test framework. |
| |
| A secondary goal is to reduce programming errors by providing common constant |
| strings and values as python attributes to simplify detection of typos. |
| A simple typo in a string can take a lot of debugging to uncover the issue, |
| attribute errors are easier to notice and most autocompletion systems detect |
| them. |
| |
| The config variable is initialzed by calling :func:`initialize_config`. |
| Before this point only ``constants`` will be availaible. This is to ensure |
| that library function writers never accidentally get stale config attributes. |
| |
| Program arguments/flag arguments are available from the config as attributes. |
| If an attribute was not set by the command line or the optional config file, |
| then it will fallback to the `_defaults` value, if still the value is not |
| found an AttributeError will be raised. |
| |
| :func define_defaults: |
| Provided by the config if the attribute is not found in the config or |
| commandline. For instance, if we are using the list command fixtures might |
| not be able to count on the build_dir being provided since we aren't going |
| to build anything. |
| |
| :var constants: |
| Values not directly exposed by the config, but are attached to the object |
| for centralized access. I.E. you can reach them with |
| :code:`config.constants.attribute`. These should be used for setting |
| common string names used across the test framework. |
| :code:`_defaults.build_dir = None` Once this module has been imported |
| constants should not be modified and their base attributes are frozen. |
| ''' |
| import abc |
| import argparse |
| import copy |
| import os |
| import re |
| |
| from six import add_metaclass |
| from pickle import HIGHEST_PROTOCOL as highest_pickle_protocol |
| |
| from testlib.helper import absdirpath, AttrDict, FrozenAttrDict |
| |
| class UninitialzedAttributeException(Exception): |
| ''' |
| Signals that an attribute in the config file was not initialized. |
| ''' |
| pass |
| |
| class UninitializedConfigException(Exception): |
| ''' |
| Signals that the config was not initialized before trying to access an |
| attribute. |
| ''' |
| pass |
| |
| class TagRegex(object): |
| def __init__(self, include, regex): |
| self.include = include |
| self.regex = re.compile(regex) |
| |
| def __str__(self): |
| type_ = 'Include' if self.include else 'Remove' |
| return '%10s: %s' % (type_, self.regex.pattern) |
| |
| class _Config(object): |
| _initialized = False |
| |
| __shared_dict = {} |
| |
| constants = AttrDict() |
| _defaults = AttrDict() |
| _config = {} |
| |
| _cli_args = {} |
| _post_processors = {} |
| |
| def __init__(self): |
| # This object will act as if it were a singleton. |
| self.__dict__ = self.__shared_dict |
| |
| def _init(self, parser): |
| self._parse_commandline_args(parser) |
| self._run_post_processors() |
| self._initialized = True |
| |
| def _add_post_processor(self, attr, post_processor): |
| ''' |
| :param attr: Attribute to pass to and recieve from the |
| :func:`post_processor`. |
| |
| :param post_processor: A callback functions called in a chain to |
| perform additional setup for a config argument. Should return a |
| tuple containing the new value for the config attr. |
| ''' |
| if attr not in self._post_processors: |
| self._post_processors[attr] = [] |
| self._post_processors[attr].append(post_processor) |
| |
| def _set(self, name, value): |
| self._config[name] = value |
| |
| def _parse_commandline_args(self, parser): |
| args = parser.parse_args() |
| |
| self._config_file_args = {} |
| |
| for attr in dir(args): |
| # Ignore non-argument attributes. |
| if not attr.startswith('_'): |
| self._config_file_args[attr] = getattr(args, attr) |
| self._config.update(self._config_file_args) |
| |
| def _run_post_processors(self): |
| for attr, callbacks in self._post_processors.items(): |
| newval = self._lookup_val(attr) |
| for callback in callbacks: |
| newval = callback(newval) |
| if newval is not None: |
| newval = newval[0] |
| self._set(attr, newval) |
| |
| |
| def _lookup_val(self, attr): |
| ''' |
| Get the attribute from the config or fallback to defaults. |
| |
| :returns: If the value is not stored return None. Otherwise a tuple |
| containing the value. |
| ''' |
| if attr in self._config: |
| return (self._config[attr],) |
| elif hasattr(self._defaults, attr): |
| return (getattr(self._defaults, attr),) |
| |
| def __getattr__(self, attr): |
| if attr in dir(super(_Config, self)): |
| return getattr(super(_Config, self), attr) |
| elif not self._initialized: |
| raise UninitializedConfigException( |
| 'Cannot directly access elements from the config before it is' |
| ' initialized') |
| else: |
| val = self._lookup_val(attr) |
| if val is not None: |
| return val[0] |
| else: |
| raise UninitialzedAttributeException( |
| '%s was not initialzed in the config.' % attr) |
| |
| def get_tags(self): |
| d = {typ: set(self.__getattr__(typ)) |
| for typ in self.constants.supported_tags} |
| if any(map(lambda vals: bool(vals), d.values())): |
| return d |
| else: |
| return {} |
| |
| def define_defaults(defaults): |
| ''' |
| Defaults are provided by the config if the attribute is not found in the |
| config or commandline. For instance, if we are using the list command |
| fixtures might not be able to count on the build_dir being provided since |
| we aren't going to build anything. |
| ''' |
| defaults.base_dir = os.path.abspath(os.path.join(absdirpath(__file__), |
| os.pardir, |
| os.pardir)) |
| defaults.result_path = os.path.join(os.getcwd(), 'testing-results') |
| defaults.resource_url = 'http://dist.gem5.org/dist/v20-1' |
| defaults.resource_path = os.path.abspath(os.path.join(defaults.base_dir, |
| 'tests', |
| 'gem5', |
| 'resources')) |
| |
| def define_constants(constants): |
| ''' |
| 'constants' are values not directly exposed by the config, but are attached |
| to the object for centralized access. These should be used for setting |
| common string names used across the test framework. A simple typo in |
| a string can take a lot of debugging to uncover the issue, attribute errors |
| are easier to notice and most autocompletion systems detect them. |
| ''' |
| constants.system_out_name = 'system-out' |
| constants.system_err_name = 'system-err' |
| |
| constants.isa_tag_type = 'isa' |
| constants.x86_tag = 'X86' |
| constants.sparc_tag = 'SPARC' |
| constants.riscv_tag = 'RISCV' |
| constants.arm_tag = 'ARM' |
| constants.mips_tag = 'MIPS' |
| constants.power_tag = 'POWER' |
| constants.null_tag = 'NULL' |
| |
| constants.variant_tag_type = 'variant' |
| constants.opt_tag = 'opt' |
| constants.debug_tag = 'debug' |
| constants.fast_tag = 'fast' |
| |
| constants.length_tag_type = 'length' |
| constants.quick_tag = 'quick' |
| constants.long_tag = 'long' |
| |
| constants.host_isa_tag_type = 'host' |
| constants.host_x86_64_tag = 'x86_64' |
| constants.host_arm_tag = 'aarch64' |
| |
| constants.supported_tags = { |
| constants.isa_tag_type : ( |
| constants.x86_tag, |
| constants.sparc_tag, |
| constants.riscv_tag, |
| constants.arm_tag, |
| constants.mips_tag, |
| constants.power_tag, |
| constants.null_tag, |
| ), |
| constants.variant_tag_type: ( |
| constants.opt_tag, |
| constants.debug_tag, |
| constants.fast_tag, |
| ), |
| constants.length_tag_type: ( |
| constants.quick_tag, |
| constants.long_tag, |
| ), |
| constants.host_isa_tag_type: ( |
| constants.host_x86_64_tag, |
| constants.host_arm_tag, |
| ), |
| } |
| |
| # Binding target ISA with host ISA. This is useful for the |
| # case where host ISA and target ISA need to coincide |
| constants.target_host = { |
| constants.arm_tag : (constants.host_arm_tag,), |
| constants.x86_tag : (constants.host_x86_64_tag,), |
| constants.sparc_tag : (constants.host_x86_64_tag,), |
| constants.riscv_tag : (constants.host_x86_64_tag,), |
| constants.mips_tag : (constants.host_x86_64_tag,), |
| constants.power_tag : (constants.host_x86_64_tag,), |
| constants.null_tag : (None,) |
| } |
| |
| constants.supported_isas = constants.supported_tags['isa'] |
| constants.supported_variants = constants.supported_tags['variant'] |
| constants.supported_lengths = constants.supported_tags['length'] |
| constants.supported_hosts = constants.supported_tags['host'] |
| |
| constants.tempdir_fixture_name = 'tempdir' |
| constants.gem5_simulation_stderr = 'simerr' |
| constants.gem5_simulation_stdout = 'simout' |
| constants.gem5_simulation_stats = 'stats.txt' |
| constants.gem5_simulation_config_ini = 'config.ini' |
| constants.gem5_simulation_config_json = 'config.json' |
| constants.gem5_returncode_fixture_name = 'gem5-returncode' |
| constants.gem5_binary_fixture_name = 'gem5' |
| constants.xml_filename = 'results.xml' |
| constants.pickle_filename = 'results.pickle' |
| constants.pickle_protocol = highest_pickle_protocol |
| |
| # The root directory which all test names will be based off of. |
| constants.testing_base = absdirpath(os.path.join(absdirpath(__file__), |
| os.pardir)) |
| |
| def define_post_processors(config): |
| ''' |
| post_processors are used to do final configuration of variables. This is |
| useful if there is a dynamically set default, or some function that needs |
| to be applied after parsing in order to set a configration value. |
| |
| Post processors must accept a single argument that will either be a tuple |
| containing the already set config value or ``None`` if the config value |
| has not been set to anything. They must return the modified value in the |
| same format. |
| ''' |
| |
| def set_default_build_dir(build_dir): |
| ''' |
| Post-processor to set the default build_dir based on the base_dir. |
| |
| .. seealso :func:`~_Config._add_post_processor` |
| ''' |
| if not build_dir or build_dir[0] is None: |
| base_dir = config._lookup_val('base_dir')[0] |
| build_dir = (os.path.join(base_dir, 'build'),) |
| return build_dir |
| |
| def fix_verbosity_hack(verbose): |
| return (verbose[0].val,) |
| |
| def threads_as_int(threads): |
| if threads is not None: |
| return (int(threads[0]),) |
| |
| def test_threads_as_int(test_threads): |
| if test_threads is not None: |
| return (int(test_threads[0]),) |
| |
| def default_isa(isa): |
| if not isa[0]: |
| return [constants.supported_tags[constants.isa_tag_type]] |
| else: |
| return isa |
| |
| def default_variant(variant): |
| if not variant[0]: |
| # Default variant is only opt. No need to run tests with multiple |
| # different compilation targets |
| return [[constants.opt_tag]] |
| else: |
| return variant |
| |
| def default_length(length): |
| if not length[0]: |
| return [[constants.quick_tag]] |
| else: |
| return length |
| |
| def default_host(host): |
| if not host[0]: |
| try: |
| import platform |
| host_machine = platform.machine() |
| if host_machine not in constants.supported_hosts: |
| raise ValueError("Invalid host machine") |
| return [[host_machine]] |
| except: |
| return [[constants.host_x86_64_tag]] |
| else: |
| return host |
| |
| def compile_tag_regex(positional_tags): |
| if not positional_tags: |
| return positional_tags |
| else: |
| new_positional_tags_list = [] |
| positional_tags = positional_tags[0] |
| |
| for flag, regex in positional_tags: |
| if flag == 'exclude_tags': |
| tag_regex = TagRegex(False, regex) |
| elif flag == 'include_tags': |
| tag_regex = TagRegex(True, regex) |
| else: |
| raise ValueError('Unsupported flag.') |
| new_positional_tags_list.append(tag_regex) |
| |
| return (new_positional_tags_list,) |
| |
| config._add_post_processor('build_dir', set_default_build_dir) |
| config._add_post_processor('verbose', fix_verbosity_hack) |
| config._add_post_processor('isa', default_isa) |
| config._add_post_processor('variant', default_variant) |
| config._add_post_processor('length', default_length) |
| config._add_post_processor('host', default_host) |
| config._add_post_processor('threads', threads_as_int) |
| config._add_post_processor('test_threads', test_threads_as_int) |
| config._add_post_processor(StorePositionalTagsAction.position_kword, |
| compile_tag_regex) |
| class Argument(object): |
| ''' |
| Class represents a cli argument/flag for a argparse parser. |
| |
| :attr name: The long name of this object that will be stored in the arg |
| output by the final parser. |
| ''' |
| def __init__(self, *flags, **kwargs): |
| self.flags = flags |
| self.kwargs = kwargs |
| |
| if len(flags) == 0: |
| raise ValueError("Need at least one argument.") |
| elif 'dest' in kwargs: |
| self.name = kwargs['dest'] |
| elif len(flags) > 1 or flags[0].startswith('-'): |
| for flag in flags: |
| if not flag.startswith('-'): |
| raise ValueError("invalid option string %s: must start" |
| "with a character '-'" % flag) |
| |
| if flag.startswith('--'): |
| if not hasattr(self, 'name'): |
| self.name = flag.lstrip('-') |
| |
| if not hasattr(self, 'name'): |
| self.name = flags[0].lstrip('-') |
| self.name = self.name.replace('-', '_') |
| |
| def add_to(self, parser): |
| '''Add this argument to the given parser.''' |
| parser.add_argument(*self.flags, **self.kwargs) |
| |
| def copy(self): |
| '''Copy this argument so you might modify any of its kwargs.''' |
| return copy.deepcopy(self) |
| |
| |
| class _StickyInt: |
| ''' |
| A class that is used to cheat the verbosity count incrementer by |
| pretending to be an int. This makes the int stay on the heap and eat other |
| real numbers when they are added to it. |
| |
| We use this so we can allow the verbose flag to be provided before or after |
| the subcommand. This likely has no utility outside of this use case. |
| ''' |
| def __init__(self, val=0): |
| self.val = val |
| self.type = int |
| def __add__(self, other): |
| self.val += other |
| return self |
| |
| common_args = NotImplemented |
| |
| class StorePositionAction(argparse.Action): |
| '''Base class for classes wishing to create namespaces where |
| arguments are stored in the order provided via the command line. |
| ''' |
| position_kword = 'positional' |
| |
| def __call__(self, parser, namespace, values, option_string=None): |
| if not self.position_kword in namespace: |
| setattr(namespace, self.position_kword, []) |
| previous = getattr(namespace, self.position_kword) |
| previous.append((self.dest, values)) |
| setattr(namespace, self.position_kword, previous) |
| |
| class StorePositionalTagsAction(StorePositionAction): |
| position_kword = 'tag_filters' |
| |
| def define_common_args(config): |
| ''' |
| Common args are arguments which are likely to be simular between different |
| subcommands, so they are available to all by placing their definitions |
| here. |
| ''' |
| global common_args |
| |
| # A list of common arguments/flags used across cli parsers. |
| common_args = [ |
| Argument( |
| 'directory', |
| nargs='?', |
| default=os.getcwd(), |
| help='Directory to start searching for tests in'), |
| Argument( |
| '--exclude-tags', |
| action=StorePositionalTagsAction, |
| help='A tag comparison used to select tests.'), |
| Argument( |
| '--include-tags', |
| action=StorePositionalTagsAction, |
| help='A tag comparison used to select tests.'), |
| Argument( |
| '--isa', |
| action='append', |
| default=[], |
| help="Only tests that are valid with one of these ISAs. " |
| "Comma separated."), |
| Argument( |
| '--variant', |
| action='append', |
| default=[], |
| help="Only tests that are valid with one of these binary variants" |
| "(e.g., opt, debug). Comma separated."), |
| Argument( |
| '--length', |
| action='append', |
| default=[], |
| help="Only tests that are one of these lengths. Comma separated."), |
| Argument( |
| '--host', |
| action='append', |
| default=[], |
| help="Only tests that are meant to runnable on the selected host"), |
| Argument( |
| '--uid', |
| action='store', |
| default=None, |
| help='UID of a specific test item to run.'), |
| Argument( |
| '--build-dir', |
| action='store', |
| help='Build directory for SCons'), |
| Argument( |
| '--base-dir', |
| action='store', |
| default=config._defaults.base_dir, |
| help='Directory to change to in order to exec scons.'), |
| Argument( |
| '-j', '--threads', |
| action='store', |
| default=1, |
| help='Number of threads to run SCons with.'), |
| Argument( |
| '-t', '--test-threads', |
| action='store', |
| default=1, |
| help='Number of threads to spawn to run concurrent tests with.'), |
| Argument( |
| '-v', |
| action='count', |
| dest='verbose', |
| default=_StickyInt(), |
| help='Increase verbosity'), |
| Argument( |
| '--config-path', |
| action='store', |
| default=os.getcwd(), |
| help='Path to read a testing.ini config in' |
| ), |
| Argument( |
| '--skip-build', |
| action='store_true', |
| default=False, |
| help='Skip the building component of SCons targets.' |
| ), |
| Argument( |
| '--result-path', |
| action='store', |
| help='The path to store results in.' |
| ), |
| Argument( |
| '--bin-path', |
| action='store', |
| default=config._defaults.resource_path, |
| help='Path where resources are stored (downloaded if not present)' |
| ), |
| Argument( |
| '--resource-url', |
| action='store', |
| default=config._defaults.resource_url, |
| help='The URL where the resources reside.' |
| ), |
| |
| ] |
| |
| # NOTE: There is a limitation which arises due to this format. If you have |
| # multiple arguments with the same name only the final one in the list |
| # will be saved. |
| # |
| # e.g. if you have a -v argument which increments verbosity level and |
| # a separate --verbose flag which 'store's verbosity level. the final |
| # one in the list will be saved. |
| common_args = AttrDict({arg.name:arg for arg in common_args}) |
| |
| @add_metaclass(abc.ABCMeta) |
| class ArgParser(object): |
| |
| def __init__(self, parser): |
| # Copy public methods of the parser. |
| for attr in dir(parser): |
| if not attr.startswith('_'): |
| setattr(self, attr, getattr(parser, attr)) |
| self.parser = parser |
| self.add_argument = self.parser.add_argument |
| |
| # Argument will be added to all parsers and subparsers. |
| common_args.verbose.add_to(parser) |
| |
| |
| class CommandParser(ArgParser): |
| ''' |
| Main parser which parses command strings and uses those to direct to |
| a subparser. |
| ''' |
| def __init__(self): |
| parser = argparse.ArgumentParser() |
| super(CommandParser, self).__init__(parser) |
| self.subparser = self.add_subparsers(dest='command') |
| |
| |
| class RunParser(ArgParser): |
| ''' |
| Parser for the \'run\' command. |
| ''' |
| def __init__(self, subparser): |
| parser = subparser.add_parser( |
| 'run', |
| help='''Run Tests.''' |
| ) |
| |
| super(RunParser, self).__init__(parser) |
| |
| common_args.uid.add_to(parser) |
| common_args.skip_build.add_to(parser) |
| common_args.directory.add_to(parser) |
| common_args.build_dir.add_to(parser) |
| common_args.base_dir.add_to(parser) |
| common_args.bin_path.add_to(parser) |
| common_args.threads.add_to(parser) |
| common_args.test_threads.add_to(parser) |
| common_args.isa.add_to(parser) |
| common_args.variant.add_to(parser) |
| common_args.length.add_to(parser) |
| common_args.host.add_to(parser) |
| common_args.include_tags.add_to(parser) |
| common_args.exclude_tags.add_to(parser) |
| |
| |
| class ListParser(ArgParser): |
| ''' |
| Parser for the \'list\' command. |
| ''' |
| def __init__(self, subparser): |
| parser = subparser.add_parser( |
| 'list', |
| help='''List and query test metadata.''' |
| ) |
| super(ListParser, self).__init__(parser) |
| |
| Argument( |
| '--suites', |
| action='store_true', |
| default=False, |
| help='List all test suites.' |
| ).add_to(parser) |
| Argument( |
| '--tests', |
| action='store_true', |
| default=False, |
| help='List all test cases.' |
| ).add_to(parser) |
| Argument( |
| '--fixtures', |
| action='store_true', |
| default=False, |
| help='List all fixtures.' |
| ).add_to(parser) |
| Argument( |
| '--all-tags', |
| action='store_true', |
| default=False, |
| help='List all tags.' |
| ).add_to(parser) |
| Argument( |
| '-q', |
| dest='quiet', |
| action='store_true', |
| default=False, |
| help='Quiet output (machine readable).' |
| ).add_to(parser) |
| |
| common_args.directory.add_to(parser) |
| common_args.bin_path.add_to(parser) |
| common_args.isa.add_to(parser) |
| common_args.variant.add_to(parser) |
| common_args.length.add_to(parser) |
| common_args.host.add_to(parser) |
| common_args.include_tags.add_to(parser) |
| common_args.exclude_tags.add_to(parser) |
| |
| |
| class RerunParser(ArgParser): |
| def __init__(self, subparser): |
| parser = subparser.add_parser( |
| 'rerun', |
| help='''Rerun failed tests.''' |
| ) |
| super(RerunParser, self).__init__(parser) |
| |
| common_args.skip_build.add_to(parser) |
| common_args.directory.add_to(parser) |
| common_args.build_dir.add_to(parser) |
| common_args.base_dir.add_to(parser) |
| common_args.bin_path.add_to(parser) |
| common_args.threads.add_to(parser) |
| common_args.test_threads.add_to(parser) |
| common_args.isa.add_to(parser) |
| common_args.variant.add_to(parser) |
| common_args.length.add_to(parser) |
| common_args.host.add_to(parser) |
| |
| config = _Config() |
| define_constants(config.constants) |
| |
| # Constants are directly exposed and available once this module is created. |
| # All constants MUST be defined before this point. |
| config.constants = FrozenAttrDict(config.constants.__dict__) |
| constants = config.constants |
| |
| ''' |
| This config object is the singleton config object available throughout the |
| framework. |
| ''' |
| def initialize_config(): |
| ''' |
| Parse the commandline arguments and setup the config varibles. |
| ''' |
| global config |
| |
| # Setup constants and defaults |
| define_defaults(config._defaults) |
| define_post_processors(config) |
| define_common_args(config) |
| |
| # Setup parser and subcommands |
| baseparser = CommandParser() |
| runparser = RunParser(baseparser.subparser) |
| listparser = ListParser(baseparser.subparser) |
| rerunparser = RerunParser(baseparser.subparser) |
| |
| # Initialize the config by parsing args and running callbacks. |
| config._init(baseparser) |