예제 #1
0
class SpackEnvConfig(yc.YamlConfigLoader):

    ELEMENTS = [
        yc.KeyedElem('spack',
                     elements=[
                         yc.KeyedElem('config',
                                      elements=[
                                          yc.StrElem('install_tree'),
                                          yc.StrElem('build_jobs', default=6),
                                          yc.StrElem('install_path_scheme')
                                      ]),
                         yc.CategoryElem('mirrors', sub_elem=yc.StrElem()),
                         yc.ListElem('repos', sub_elem=yc.StrElem()),
                         yc.CategoryElem(
                             'upstreams',
                             sub_elem=yc.KeyedElem(
                                 elements=[yc.StrElem('install_tree')])),
                     ],
                     help_text='Spack environment configuration file.')
    ]
예제 #2
0
    def add_result_parser_config(cls, name, config_items):
        """Add the given list of config items as a result parser
        configuration named 'name'. Throws errors for invalid configuraitons.
        """

        # Validate the config.
        required_keys = {
            'files': False,
            'action': False,
            'per_file': False,
        }
        for item in config_items:
            for req_key in required_keys.keys():
                if item.name == req_key:
                    required_keys[req_key] = True

        for req_key, found in required_keys.items():
            if not found:
                raise TestConfigError(
                    "Result parser '{}' must have a required config "
                    "element named '{}'".format(name, req_key))

        config = yc.KeyedElem('result_parser_{}'.format(name),
                              elements=config_items)

        list_elem = yc.CategoryElem(name, sub_elem=config)

        if name in [e.name for e in cls._RESULT_PARSERS.config_elems.values()]:
            raise ValueError(
                "Tried to add result parser with name '{}'"
                "to the config, but one already exists.".format(name))

        try:
            cls.check_leaves(config)
        except ValueError as err:
            raise ValueError("Tried to add result parser named '{}', but "
                             "leaf element '{}' was not string based.".format(
                                 name, err.args[0]))

        cls._RESULT_PARSERS.config_elems[name] = list_elem
예제 #3
0
class TestConfigLoader(yc.YamlConfigLoader):
    """This class describes a test section in a Pavilion config file. It is
    expected to be added to by various plugins."""

    ELEMENTS = [
        yc.RegexElem(
            'inherits_from',
            regex=r'\w+',
            help_text="Inherit from the given test section, and override "
            "parameters those specified in this one. Lists are "
            "overridden entirely"),
        yc.StrElem(
            'subtitle',
            help_text="An extended title for this test. This is useful for "
            "assigning unique name to virtual tests through "
            "variable insertion. example, if a test has a single "
            "permutation variable 'subtest', then '{subtest}' "
            "would give a useful descriptor."),
        VarCatElem('variables',
                   sub_elem=yc.ListElem(sub_elem=VariableElem()),
                   help_text="Variables for this test section. These can be "
                   "inserted strings anywhere else in the config through "
                   "the string syntax. They keys 'var', 'per', 'pav', "
                   "'sys' and 'sched' reserved. Each value may be a "
                   "single or list of strings key/string pairs."),
        VarCatElem(
            'permutations',
            sub_elem=yc.ListElem(sub_elem=VariableElem()),
            help_text="Permutation variables for this test section. These are "
            "just like normal variables, but they if a list of "
            "values (whether a single string or key/string pairs) "
            "is given, then a virtual test is created for each "
            "combination across all variables in each section. The "
            "resulting virtual test is thus given a single "
            "permutation of these values."),
        yc.RegexElem('scheduler',
                     regex=r'\w+',
                     help_text="The scheduler class to use to run this test."),
        yc.KeyedElem(
            'build',
            elements=[
                yc.StrElem(
                    'source_location',
                    help_text="Path to the test source. It may be a directory, "
                    "a tar file, or a URI. If it's a directory or "
                    "file, the path is to '$PAV_CONFIG/test_src' by "
                    "default. For url's, the is automatically checked "
                    "for updates every time the test run. Downloaded "
                    "files are placed in a 'downloads' under the "
                    "pavilion working directory. (set in pavilion.yaml)"),
                yc.StrElem(
                    'source_download_name',
                    help_text='When downloading source, we by default use the '
                    'last of the url path as the filename, or a hash '
                    'of the url if is no suitable name. Use this '
                    'parameter to override behavior with a pre-defined '
                    'filename.'),
                yc.ListElem(
                    'modules',
                    sub_elem=yc.StrElem(),
                    help_text="Modules to load into the build environment."),
                yc.CategoryElem(
                    'env',
                    sub_elem=yc.StrElem(),
                    help_text="Environment variables to set in the build "
                    "environment."),
                yc.ListElem(
                    'extra_files',
                    sub_elem=yc.StrElem(),
                    help_text='Files to copy into the build environment. '
                    'Relative paths searched for in ~/.pavilion, '
                    '$PAV_CONFIG. Absolute paths are ok, '
                    'but not recommended.'),
                yc.StrElem(
                    'specificity',
                    help_text="Use this string, along with variables, to "
                    "differentiate builds. A common example would be "
                    "to make per-host specific by using the "
                    "sys.sys_name variable. Note _deferred_ system "
                    "variables aren't a good idea hereas configs are "
                    "compiled on the host that launches the test."),
                yc.ListElem(
                    'cmds',
                    sub_elem=yc.StrElem(),
                    help_text='The sequence of commands to run to perform '
                    'the build.')
            ],
            help_text="The test build configuration. This will be used to "
            "dynamically generate a build script for building "
            "the test."),
        yc.KeyedElem(
            'run',
            elements=[
                yc.ListElem(
                    'modules',
                    sub_elem=yc.StrElem(),
                    help_text="Modules to load into the run environment."),
                yc.CategoryElem(
                    'env',
                    sub_elem=yc.StrElem(),
                    help_text="Environment variables to set in the run "
                    "environment."),
                yc.ListElem(
                    'cmds',
                    sub_elem=yc.StrElem(),
                    help_text='The sequence of commands to run to run the '
                    'test.')
            ],
            help_text="The test run configuration. This will be used "
            "to dynamically generate a run script for the "
            "test."),
    ]

    # We'll append the result parsers separately, to have an easy way to
    # access it.
    _RESULT_PARSERS = yc.KeyedElem(
        'result',
        elements=[],
        help_text="Result parser configurations go here. Each parser config "
        "can occur by itself or as a list of configs, in which "
        "case the parser will run once for each config given. The "
        "output of these parsers will be combined into the final "
        "result json data.")
    ELEMENTS.append(_RESULT_PARSERS)

    @classmethod
    def add_subsection(cls, subsection):
        """Use this method to add additional sub-sections to the config.
        :param yc.ConfigElem subsection: A yaml config element to add. Keyed
            elements are expected, though any ConfigElem based instance
            (whose leave elements are StrElems) should work.
        """

        if not isinstance(subsection, yc.ConfigElement):
            raise ValueError("Tried to add a subsection to the config, but it "
                             "wasn't a yaml_config ConfigElement instance (or "
                             "an instance of a ConfigElement child "
                             "class).")

        name = subsection.name

        names = [el.name for el in cls.ELEMENTS]

        if name in names:
            raise ValueError("Tried to add a subsection to the config called "
                             "{0}, but one already exists.".format(name))

        try:
            cls.check_leaves(subsection)
        except ValueError as err:
            raise ValueError("Tried to add result parser named '{}', but "
                             "leaf element '{}' was not string based.".format(
                                 name, err.args[0]))

        cls.ELEMENTS.append(subsection)

    @classmethod
    def remove_subsection(cls, subsection_name):
        """Remove a subsection from the config. This is really only for use
        in plugin deactivate methods."""

        for section in list(cls.ELEMENTS):
            if subsection_name == section.name:
                cls.ELEMENTS.remove(section)
                return

    @classmethod
    def add_result_parser_config(cls, name, config_items):
        """Add the given list of config items as a result parser
        configuration named 'name'. Throws errors for invalid configuraitons.
        """

        config = yc.KeyedElem('result_parser_{}'.format(name),
                              elements=config_items)

        list_elem = yc.ListElem(name, sub_elem=config)

        if name in [e.name for e in cls._RESULT_PARSERS.config_elems.values()]:
            raise ValueError(
                "Tried to add result parser with name '{}'"
                "to the config, but one already exists.".format(name))

        try:
            cls.check_leaves(config)
        except ValueError as err:
            raise ValueError("Tried to add result parser named '{}', but "
                             "leaf element '{}' was not string based.".format(
                                 name, err.args[0]))

        cls._RESULT_PARSERS.config_elems[name] = list_elem

    @classmethod
    def remove_result_parser_config(cls, name):
        """Remove the given result parser from the result parser configuration
        section.
        :param str name: The name of the parser to remove.
        :return:
        """

        for section in list(cls._RESULT_PARSERS.config_elems.values()):
            if section.name == name:
                del cls._RESULT_PARSERS.config_elems[section.name]
                return

    @classmethod
    def check_leaves(cls, elem):
        """
        :param yc.ConfigElement elem:
        :return:
        """

        if hasattr(elem, 'config_elems'):
            for sub_elem in elem.config_elems.values():
                cls.check_leaves(sub_elem)
        elif hasattr(elem, '_sub_elem') and elem._sub_elem is not None:
            cls.check_leaves(elem._sub_elem)
        elif issubclass(elem.type, str):
            return
        else:
            raise ValueError(elem)
예제 #4
0
파일: config.py 프로젝트: pflarr/pavilion2
class PavilionConfigLoader(yc.YamlConfigLoader):
    """This object uses YamlConfig to define Pavilion's base configuration
    format and options. If you're looking to add an option to the general
    pavilion.yaml format, this is the place to do it."""

    # Each and every configuration element needs to either not be required,
    # or have a sensible default. Essentially, Pavilion needs to work if no
    # config is given.
    ELEMENTS = [
        yc.ListElem(
            "config_dirs",
            sub_elem=ExPathElem(),
            post_validator=config_dirs_validator,
            help_text="Additional Paths to search for Pavilion config files. "
                      "Pavilion configs (other than this core config) are "
                      "searched for in the given order. In the case of "
                      "identically named files, directories listed earlier "
                      "take precedence."),
        yc.BoolElem(
            "user_config",
            default=False,
            help_text="Whether to automatically add the user's config "
                      "directory at ~/.pavilion to the config_dirs. Configs "
                      "in this directory always take precedence."
        ),
        ExPathElem(
            'working_dir', default=USER_HOME_PAV/'working_dir', required=True,
            help_text="Where pavilion puts it's run files, downloads, etc."),
        yc.ListElem(
            "disable_plugins", sub_elem=yc.StrElem(),
            help_text="Allows you to disable plugins by '<type>.<name>'. For "
                      "example, 'module.gcc' would disable the gcc module "
                      "wrapper."),
        yc.StrElem(
            "shared_group", post_validator=_group_validate,
            help_text="Pavilion can automatically set group permissions on all "
                      "created files, so that users can share relevant "
                      "results, etc."),
        yc.StrElem(
            "umask", default="2",
            help_text="The umask to apply to all files created by pavilion. "
                      "This should be in the format needed by the umask shell "
                      "command."),
        yc.IntRangeElem(
            "build_threads", default=4, vmin=1,
            help_text="Maximum simultaneous builds. Note that each build may "
                      "itself spawn off threads/processes, so it's probably "
                      "reasonable to keep this at just a few."),
        yc.StrElem(
            "log_format",
            default="{asctime}, {levelname}, {hostname}, {name}: {message}",
            help_text="The log format to use for the pavilion logger. "
                      "Uses the modern '{' format style. See: "
                      "https://docs.python.org/3/library/logging.html#"
                      "logrecord-attributes"),
        yc.StrElem(
            "log_level", default="info",
            choices=['debug', 'info', 'warning', 'error', 'critical'],
            help_text="The minimum log level for messages sent to the pavilion "
                      "logfile."),
        ExPathElem(
            "result_log",
            # Derive the default from the working directory, if a value isn't
            # given.
            post_validator=(lambda d, v: v if v is not None else
                            d['working_dir']/'results.log'),
            help_text="Results are put in both the general log and a specific "
                      "results log. This defaults to 'results.log' in the "
                      "working directory."),
        yc.BoolElem(
            "flatten_results", default=True,
            help_text="Flatten results with multiple 'per_file' values into "
                      "multiple result log lines, one for each 'per_file' "
                      "value. Each flattened result will have a 'file' key, "
                      "and the contents of its 'per_file' data will be added "
                      "to the base results mapping."),
        ExPathElem(
            'exception_log',
            # Derive the default from the working directory, if a value isn't
            # given.
            post_validator=(lambda d, v: v if v is not None else
                            d['working_dir']/'exceptions.log'),
            help_text="Full exception tracebacks and related debugging "
                      "information is logged here."
        ),
        yc.IntElem(
            "wget_timeout", default=5,
            help_text="How long to wait on web requests before timing out. On "
                      "networks without internet access, zero will allow you "
                      "to spot issues faster."
        ),
        yc.CategoryElem(
            "proxies", sub_elem=yc.StrElem(),
            help_text="Proxies, by protocol, to use when accessing the "
                      "internet. Eg: http: 'http://myproxy.myorg.org:8000'"),
        yc.ListElem(
            "no_proxy", sub_elem=yc.StrElem(),
            help_text="A list of DNS suffixes to ignore for proxy purposes. "
                      "For example: 'blah.com' would match 'www.blah.com', but "
                      "not 'myblah.com'."),
        yc.ListElem(
            "env_setup", sub_elem=yc.StrElem(),
            help_text="A list of commands to be executed at the beginning of "
                      "every kickoff script."),
        yc.CategoryElem(
            "default_results", sub_elem=yc.StrElem(),
            help_text="Each of these will be added as a constant result "
                      "parser with the corresponding key and constant value. "
                      "Generally, the values should contain a pavilion "
                      "variable of some sort to resolve."),

        # The following configuration items are for internal use and provide a
        # convenient way to pass around core pavilion components or data.
        # They are not intended to be set by the user, and will generally be
        # overwritten without even checking for user provided values.
        ExPathElem(
            'pav_cfg_file', hidden=True,
            help_text="The location of the loaded pav config file."
        ),
        ExPathElem(
            'pav_root', default=PAV_ROOT, hidden=True,
            help_text="The root directory of the pavilion install. This "
                      "shouldn't be set by the user."),
        yc.KeyedElem(
            'pav_vars', elements=[], hidden=True, default={},
            help_text="This will contain the pavilion variable dictionary."),
    ]
예제 #5
0
class PavilionConfigLoader(yc.YamlConfigLoader):

    # Each and every configuration element needs to either not be required,
    # or have a sensible default. Essentially, Pavilion needs to work if no
    # config is given.
    ELEMENTS = [
        yc.ListElem(
            "config_dirs",
            defaults=PAV_CONFIG_SEARCH_DIRS,
            sub_elem=yc.StrElem(),
            help_text="Paths to search for Pavilion config files. Pavilion "
                      "configs (other than this core config) are searched for "
                      "in the given order. In the case of identically named "
                      "files, directories listed earlier take precedent."),
        yc.StrElem(
            'working_dir', default=USER_HOME_PAV,
            help_text="Where pavilion puts it's run files, downloads, etc."),
        yc.ListElem(
            "disable_plugins", sub_elem=yc.StrElem(),
            help_text="Allows you to disable plugins by '<type>.<name>'. For "
                      "example, 'module.gcc' would disable the gcc module "
                      "wrapper."),
        yc.StrElem(
            "shared_group", post_validator=group_validate,
            help_text="Pavilion can automatically set group permissions on all "
                      "created files, so that users can share relevant "
                      "results, etc."),
        yc.StrElem(
            "umask", default="0002",
            help_text="The umask to apply to all files created by pavilion. "
                      "This should be in the format needed by the umask shell "
                      "command."),
        yc.StrElem(
            "log_format",
            default="%{asctime}, ${levelname}, ${name}: ${message}",
            help_text="The log format to use for the pavilion logger. See: "
                      "https://docs.python.org/3/library/logging.html#"
                      "logrecord-attributes"),
        yc.StrElem(
            "log_level", default="info", post_validator=log_level_validate,
            help_text="The minimum log level for messages sent to the pavilion "
                      "logfile."),
        yc.IntElem(
            "wget_timeout", default=5,
            help_text="How long to wait on web requests before timing out. On"
                      "networks without internet access, zero will allow you"
                      "to spot issues faster."
        ),
        yc.CategoryElem(
            "proxies", sub_elem=yc.StrElem(),
            help_text="Proxies, by protocol, to use when accessing the "
                      "internet. Eg: http: 'http://myproxy.myorg.org:8000'"),
        yc.ListElem(
            "no_proxy", sub_elem=yc.StrElem(),
            help_text="A list of DNS suffixes to ignore for proxy purposes. "
                      "For example: 'blah.com' would match 'www.blah.com', but "
                      "not 'myblah.com'."),

        # The following configuration items are for internal use and provide a
        # convenient way to pass around core pavilion components or data.
        # They are not intended to be set by the user, and will generally be
        # overwritten without even checking for user provided values.
        yc.StrElem(
            'pav_root', default=pav_root, hidden=True,
            help_text="The root directory of the pavilion install. This "
                      "shouldn't be set by the user."),
        yc.KeyedElem(
            'sys_vars', elements=[], hidden=True, default={},
            help_text="This will contain the system variable dictionary."),
        yc.KeyedElem(
            'pav_vars', elements=[], hidden=True, default={},
            help_text="This will contain the pavilion variable dictionary."),
    ]
예제 #6
0
class TestConfigLoader(yc.YamlConfigLoader):
    """This class describes a test section in a Pavilion config file. It is
expected to be added to by various plugins.

:cvar list(yc.YamlConfig) ELEMENTS: Each YamlConfig instance in this
    list defines a key for the test config.

- Each element must result in a string (which is why you see a lot of StrElem
  below), or a structure that contains only strings at the lowest layer.

  - So lists of dicts of strings are fine, etc.
  - yc.RegexElem also produces a string.
- Everything should have a sensible default.

  - An empty config should be a valid test.
- For bool values, accept ['true', 'false', 'True', 'False'].

  - They should be checked with val.lower() == 'true', etc.
- Every element must have a useful 'help_text'.
"""

    ELEMENTS = [
        yc.StrElem(
            'name',
            hidden=True,
            default='<unnamed>',
            help_text="The base name of the test. Value added automatically."),
        yc.StrElem(
            'suite',
            hidden=True,
            default='<no_suite>',
            help_text="The name of the suite. Value added automatically."),
        yc.StrElem(
            'suite_path',
            hidden=True,
            default='<no_suite>',
            help_text="Path to the suite file. Value added automatically."),
        yc.StrElem(
            'host',
            hidden=True,
            default='<unknown>',
            help_text="Host (typically sys.sys_name) for which this test was "
            "created. Value added automatically."),
        yc.ListElem(
            'modes',
            hidden=True,
            sub_elem=yc.StrElem(),
            help_text="Modes used in the creation of this test. Value is added "
            "automatically."),
        yc.RegexElem(
            'inherits_from',
            regex=TEST_NAME_RE_STR,
            help_text="Inherit from the given test section, and override "
            "parameters those specified in this one. Lists are "
            "overridden entirely"),
        yc.StrElem('subtitle',
                   help_text="An extended title for this test. Required for "
                   "permuted tests."),
        yc.StrElem('group',
                   default=None,
                   help_text="The group under which to build and run tests. "
                   "Defaults to the group specified in pavilion.yaml."),
        yc.RegexElem(
            'umask',
            regex=r'[0-7]{3}',
            default=None,
            help_text="The octal umask to apply to files created during the "
            "build and run processes. Defaults to the umask in "
            "pavilion.yaml."),
        yc.KeyedElem(
            'maintainer',
            help_text="Information about who maintains this test.",
            elements=[
                yc.StrElem(
                    'name',
                    default='unknown',
                    help_text="Name or organization of the maintainer."),
                yc.StrElem('email',
                           help_text="Email address of the test maintainer."),
            ]),
        yc.StrElem('summary',
                   default='',
                   help_text="Summary of the purpose of this test."),
        yc.StrElem('doc',
                   default='',
                   help_text="Detailed documentation string for this test."),
        yc.ListElem(
            'permute_on',
            sub_elem=yc.StrElem(),
            help_text="List of permuted variables. For every permutation of "
            "the values of these variables, a new virtual test will "
            "be generated."),
        VarCatElem('variables',
                   sub_elem=yc.ListElem(sub_elem=VariableElem()),
                   help_text="Variables for this test section. These can be "
                   "inserted strings anywhere else in the config through "
                   "the string syntax. They keys 'var', 'per', 'pav', "
                   "'sys' and 'sched' reserved. Each value may be a "
                   "single or list of strings key/string pairs."),
        yc.RegexElem('scheduler',
                     regex=r'\w+',
                     default="raw",
                     help_text="The scheduler class to use to run this test."),
        CondCategoryElem(
            'only_if',
            sub_elem=yc.ListElem(sub_elem=yc.StrElem()),
            key_case=EnvCatElem.KC_MIXED,
            help_text="Only run this test if each of the clauses in this "
            "section evaluate to true. Each clause consists of "
            "a mapping key (that can contain Pavilion variable "
            "references, like '{{pav.user}}' or '{{sys.sys_arch}}'"
            ") and one or more regex values"
            "(that much match the whole key). A clause is true "
            "if the value of the Pavilion variable matches one or"
            " more of the values. "),
        CondCategoryElem(
            'not_if',
            sub_elem=yc.ListElem(sub_elem=yc.StrElem()),
            key_case=EnvCatElem.KC_MIXED,
            help_text="Will NOT run this test if at least one of the "
            "clauses evaluates to true. Each clause consists of "
            "a mapping key (that can contain Pavilion variable "
            "references, like '{{pav.user}}' or "
            "'{{sys.sys_arch}}') and one or more "
            "regex values (that much match the whole key)."
            "A clause is true if the value of "
            "the Pavilion variable matches one or more of the "
            " values."),
        yc.KeyedElem(
            'build',
            elements=[
                yc.ListElem(
                    'cmds',
                    sub_elem=yc.StrElem(),
                    help_text='The sequence of commands to run to perform '
                    'the build.'),
                yc.ListElem(
                    'copy_files',
                    sub_elem=yc.StrElem(),
                    help_text="When attaching the build to a test run, copy "
                    "these files instead of creating a symlink."
                    "They may include path glob wildcards, "
                    "including the recursive '**'."),
                PathCategoryElem(
                    'create_files',
                    key_case=PathCategoryElem.KC_MIXED,
                    sub_elem=yc.ListElem(sub_elem=yc.StrElem()),
                    help_text="File(s) to create at path relative to the test's"
                    "test source directory"),
                EnvCatElem(
                    'env',
                    sub_elem=yc.StrElem(),
                    key_case=EnvCatElem.KC_MIXED,
                    help_text="Environment variables to set in the build "
                    "environment."),
                yc.ListElem(
                    'extra_files',
                    sub_elem=yc.StrElem(),
                    help_text='File(s) to copy into the build environment. '
                    'Relative paths searched for in ~/.pavilion, '
                    '$PAV_CONFIG. Absolute paths are ok, '
                    'but not recommended.'),
                yc.ListElem(
                    'modules',
                    sub_elem=yc.StrElem(),
                    help_text="Modules to load into the build environment."),
                yc.StrElem('on_nodes',
                           default='False',
                           choices=['true', 'false', 'True', 'False'],
                           help_text="Whether to build on or off of the test "
                           "allocation."),
                yc.ListElem(
                    'preamble',
                    sub_elem=yc.StrElem(),
                    help_text="Setup commands for the beginning of the build "
                    "script. Added to the beginning of the run "
                    "script.  These are generally expected to "
                    "be host rather than test specific."),
                yc.StrElem(
                    'source_path',
                    help_text="Path to the test source. It may be a directory, "
                    "compressed file, compressed or "
                    "uncompressed archive (zip/tar), and is handled "
                    "according to the internal (file-magic) type. "
                    "For relative paths Pavilion looks in the "
                    "test_src directory "
                    "within all known config directories. If this"
                    "is left blank, Pavilion will always assume "
                    "there is no source to build."),
                yc.StrElem(
                    'source_url',
                    help_text='Where to find the source on the internet. By '
                    'default, Pavilion will try to download the '
                    'source from the given URL if the source file '
                    'can\'t otherwise be found. You must give a '
                    'source path so Pavilion knows where to store '
                    'the file (relative paths will be stored '
                    'relative to the local test_src directory.'),
                yc.StrElem(
                    'source_download',
                    choices=['never', 'missing', 'latest'],
                    default='missing',
                    help_text="When to attempt to download the test source.\n"
                    "  never - The url is for reference only.\n"
                    "  missing - (default) Download if the source "
                    "can't be found.\n"
                    "  latest - Always try to fetch the latest "
                    "source, tracking changes by "
                    "file size/timestamp/hash."),
                yc.StrElem(
                    'specificity',
                    default='',
                    help_text="Use this string, along with variables, to "
                    "differentiate builds. A common example would be "
                    "to make per-host specific by using the "
                    "sys.sys_name variable. Note _deferred_ system "
                    "variables aren't a good idea hereas configs are "
                    "compiled on the host that launches the test."),
                yc.StrElem(
                    'timeout',
                    default='30',
                    help_text="Time (in seconds) that a build can continue "
                    "without generating new output before it is "
                    "cancelled.  Can be left empty for no timeout."),
                yc.StrElem(
                    'verbose',
                    choices=['true', 'True', 'False', 'false'],
                    default='False',
                    help_text="Echo commands (including sourced files) in the"
                    " build log, and print the modules loaded and "
                    "environment before the cmds run."),
            ],
            help_text="The test build configuration. This will be "
            "used to dynamically generate a build script for "
            "building the test."),
        yc.KeyedElem(
            'run',
            elements=[
                yc.ListElem('cmds',
                            sub_elem=yc.StrElem(),
                            help_text='The sequence of commands to run to run '
                            'the test.'),
                PathCategoryElem(
                    'create_files',
                    key_case=PathCategoryElem.KC_MIXED,
                    sub_elem=yc.ListElem(sub_elem=yc.StrElem()),
                    help_text="File(s) to create at path relative to the test's"
                    "test source directory"),
                EnvCatElem('env',
                           sub_elem=yc.StrElem(),
                           key_case=EnvCatElem.KC_MIXED,
                           help_text="Environment variables to set in the run "
                           "environment."),
                yc.ListElem(
                    'modules',
                    sub_elem=yc.StrElem(),
                    help_text="Modules to load into the run environment."),
                yc.ListElem(
                    'preamble',
                    sub_elem=yc.StrElem(),
                    help_text="Setup commands for the beginning of the build "
                    "script. Added to the beginning of the run "
                    "script. These are generally expected to "
                    "be host rather than test specific."),
                yc.StrElem('timeout',
                           default='300',
                           help_text="Time that a build can continue without "
                           "generating new output before it is cancelled. "
                           "Can be left empty for no timeout."),
                yc.StrElem(
                    'verbose',
                    choices=['true', 'True', 'False', 'false'],
                    default='False',
                    help_text="Echo commands (including sourced files) in the "
                    "build log, and print the modules loaded and "
                    "environment before the cmds run."),
            ],
            help_text="The test run configuration. This will be used "
            "to dynamically generate a run script for the "
            "test."),
        yc.CategoryElem(
            'result_evaluate',
            sub_elem=yc.StrElem(),
            help_text="The keys and values in this section will also "
            "be added to the result json. The values are "
            "expressions (like in {{<expr>}} in normal Pavilion "
            "strings). Other result values (including those "
            "from result parsers and other evaluations are "
            "available to reference as variables."),
    ]

    # We'll append the result parsers separately, to have an easy way to
    # access it.
    _RESULT_PARSERS = yc.KeyedElem(
        'result_parse',
        elements=[],
        help_text="Result parser configurations go here. Each parser config "
        "can occur by itself or as a list of configs, in which "
        "case the parser will run once for each config given. The "
        "output of these parsers will be added to the final "
        "result json data.")
    ELEMENTS.append(_RESULT_PARSERS)

    @classmethod
    def add_subsection(cls, subsection):
        """Use this method to add additional sub-sections to the config.

        :param yc.ConfigElem subsection: A yaml config element to add. Keyed
            elements are expected, though any ConfigElem based instance
            (whose leave elements are StrElems) should work.
        """

        if not isinstance(subsection, yc.ConfigElement):
            raise ValueError("Tried to add a subsection to the config, but it "
                             "wasn't a yaml_config ConfigElement instance (or "
                             "an instance of a ConfigElement child "
                             "class).")

        name = subsection.name

        names = [el.name for el in cls.ELEMENTS]

        if name in names:
            raise ValueError("Tried to add a subsection to the config called "
                             "{0}, but one already exists.".format(name))

        try:
            cls.check_leaves(subsection)
        except ValueError as err:
            raise ValueError("Tried to add result parser named '{}', but "
                             "leaf element '{}' was not string based.".format(
                                 name, err.args[0]))

        cls.ELEMENTS.append(subsection)

    @classmethod
    def remove_subsection(cls, subsection_name):
        """Remove a subsection from the config. This is really only for use
        in plugin deactivate methods."""

        for section in list(cls.ELEMENTS):
            if subsection_name == section.name:
                cls.ELEMENTS.remove(section)
                return

    @classmethod
    def add_result_parser_config(cls, name, config_items):
        """Add the given list of config items as a result parser
        configuration named 'name'. Throws errors for invalid configuraitons.
        """

        # Validate the config.
        required_keys = {
            'files': False,
            'action': False,
            'per_file': False,
        }
        for item in config_items:
            for req_key in required_keys.keys():
                if item.name == req_key:
                    required_keys[req_key] = True

        for req_key, found in required_keys.items():
            if not found:
                raise TestConfigError(
                    "Result parser '{}' must have a required config "
                    "element named '{}'".format(name, req_key))

        config = yc.KeyedElem('result_parser_{}'.format(name),
                              elements=config_items)

        list_elem = yc.CategoryElem(name, sub_elem=config)

        if name in [e.name for e in cls._RESULT_PARSERS.config_elems.values()]:
            raise ValueError(
                "Tried to add result parser with name '{}'"
                "to the config, but one already exists.".format(name))

        try:
            cls.check_leaves(config)
        except ValueError as err:
            raise ValueError("Tried to add result parser named '{}', but "
                             "leaf element '{}' was not string based.".format(
                                 name, err.args[0]))

        cls._RESULT_PARSERS.config_elems[name] = list_elem

    @classmethod
    def remove_result_parser_config(cls, name):
        """Remove the given result parser from the result parser configuration
        section.

        :param str name: The name of the parser to remove.
        """

        for section in list(cls._RESULT_PARSERS.config_elems.values()):
            if section.name == name:
                del cls._RESULT_PARSERS.config_elems[section.name]
                return

    @classmethod
    def check_leaves(cls, elem):
        """Make sure all of the config elements have a string element or
        equivalent as the final node.

        :param yc.ConfigElement elem:
        """

        # pylint: disable=protected-access

        if hasattr(elem, 'config_elems'):
            for sub_elem in elem.config_elems.values():
                cls.check_leaves(sub_elem)
        elif hasattr(elem, '_sub_elem') and elem._sub_elem is not None:
            cls.check_leaves(elem._sub_elem)
        elif issubclass(elem.type, str):
            return
        else:
            raise ValueError(elem)