diff options
author | Peter Pentchev <roam@debian.org> | 2019-01-12 23:09:28 +0200 |
---|---|---|
committer | Peter Pentchev <roam@debian.org> | 2019-01-12 23:09:28 +0200 |
commit | f7ac332038f9dcfcd55612215c02e44fa5601405 (patch) | |
tree | eda1d3fc693e8d693cbebdb7b5abfe098b7e8e52 | |
parent | 47724c33371ce9125ae4511feb42bfb07d939378 (diff) | |
parent | 7515de39b089395b94d54869466dcefd08742c18 (diff) |
Merge confget-2.2.0pre1 into the Debian branch.
48 files changed, 4189 insertions, 193 deletions
diff --git a/python/README.md b/python/README.md new file mode 100644 index 0000000..890f138 --- /dev/null +++ b/python/README.md @@ -0,0 +1,81 @@ +confget - parse configuration files +=================================== + +The `confget` library parses configuration files (currently INI-style +files and CGI `QUERY_STRING` environment variable) and allows a program +to use the values defined in them. It provides various options for +selecting the variable names and values to return and the configuration +file sections to fetch them from. + +The `confget` library may also be used as a command-line tool with +the same interface as the C implementation. + +The `confget` library is fully typed. + +Specifying configuration values for the backends +------------------------------------------------ + +The `confget.defs` module defines the `Config` class that is used to +control the behavior of the various `confget` backends. Its main +purpose is to specify the filename and, optionally, the section name for +INI-style files, but other backends may use its fields in different ways. + +A `Config` object is created using the following parameters: +- a list of variable names to query (may be empty) +- `filename` (str, optional): the name of the file to open +- `section` (str, default ""): the name of the section within the file +- `section_specified` (bool, default false): if `section` is an empty + string, only fetch variables from the unnamed section at the start of + the file instead of defaulting to the first section in the file + +Parsing INI-style configuration files +------------------------------------- + +The `confget` library's "ini" backend parses an INI-style configuration +file. Its `read_file()` method parses the file and returns a dictionary +of sections and the variables and their values within them: + + import confget + + cfg = confget.Config([], filename='config.ini') + ini = confget.BACKENDS['ini'](cfg) + data = ini.read_file() + print('Section names: {names}'.format(names=sorted(data.keys()))) + print(data['server']['address']) + +Letting variables in a section override the default ones +-------------------------------------------------------- + +In some cases it is useful to have default values before the first +named section in a file and then override some values in various +sections. This may be useful for e.g. host-specific configuration +kept in a section with the same name as the host. + +The `format` module in the `confget` library allows, among other +filtering modes, to get the list of variables with a section +overriding the default ones: + + from confget import backend, format + + cfg = format.FormatConfig(['foo'], filename='config.ini', section='first', + section_override=True) + ini = backend.BACKENDS['ini'](cfg) + data = ini.read_file() + res = format.filter_vars(cfg, data) + assert len(res) == 1, repr(res) + print(res[0].output_full) + + cfg = format.FormatConfig(['foo'], filename='config.ini', section='second', + section_override=True) + ini = backend.BACKENDS['ini'](cfg) + data = ini.read_file() + res = format.filter_vars(cfg, data) + assert len(res) == 1, repr(res) + print(res[0].output_full) + +See the documentation of the `FormatConfig` class and the `filter_vars()` +function in the `confget.format` module for more information and for +a list of the various other filtering modes, all supported when +the library is used as a command-line tool. + +Comments: Peter Pentchev <roam@ringlet.net> diff --git a/python/confget/__init__.py b/python/confget/__init__.py new file mode 100644 index 0000000..cbd8ee9 --- /dev/null +++ b/python/confget/__init__.py @@ -0,0 +1,107 @@ +# Copyright (c) 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +confget - a library for parsing configuration files + +The `confget` library parses configuration files (currently INI-style +files and CGI `QUERY_STRING` environment variable) and allows a program +to use the values defined in them. It provides various options for +selecting the variable names and values to return and the configuration +file sections to fetch them from. + +The `confget` library may also be used as a command-line tool with +the same interface as the C implementation. + +The `confget` library is fully typed. + +Specifying configuration values for the backends +------------------------------------------------ + +The `confget.defs` module defines the `Config` class that is used to +control the behavior of the various `confget` backends. Its main +purpose is to specify the filename and, optionally, the section name for +INI-style files, but other backends may use its fields in different ways. + +A `Config` object is created using the following parameters: +- a list of variable names to query (may be empty) +- `filename` (str, optional): the name of the file to open +- `section` (str, default ""): the name of the section within the file +- `section_specified` (bool, default false): if `section` is an empty + string, only fetch variables from the unnamed section at the start of + the file instead of defaulting to the first section in the file + +Parsing INI-style configuration files +------------------------------------- + +The `confget` library's "ini" backend parses an INI-style configuration +file. Its `read_file()` method parses the file and returns a dictionary +of sections and the variables and their values within them: + + import confget + + cfg = confget.Config([], filename='config.ini') + ini = confget.BACKENDS['ini'](cfg) + data = ini.read_file() + print('Section names: {names}'.format(names=sorted(data.keys()))) + print(data['server']['address']) + +Letting variables in a section override the default ones +-------------------------------------------------------- + +In some cases it is useful to have default values before the first +named section in a file and then override some values in various +sections. This may be useful for e.g. host-specific configuration +kept in a section with the same name as the host. + +The `format` module in the `confget` library allows, among other +filtering modes, to get the list of variables with a section +overriding the default ones: + + from confget import backend, format + + cfg = format.FormatConfig(['foo'], filename='config.ini', section='first', + section_override=True) + ini = backend.BACKENDS['ini'](cfg) + data = ini.read_file() + res = format.filter_vars(cfg, data) + assert len(res) == 1, repr(res) + print(res[0].output_full) + + cfg = format.FormatConfig(['foo'], filename='config.ini', section='second', + section_override=True) + ini = backend.BACKENDS['ini'](cfg) + data = ini.read_file() + res = format.filter_vars(cfg, data) + assert len(res) == 1, repr(res) + print(res[0].output_full) + +See the documentation of the `FormatConfig` class and the `filter_vars()` +function in the `confget.format` module for more information and for +a list of the various other filtering modes, all supported when +the library is used as a command-line tool. +""" + +from .defs import Config, VERSION_STRING # noqa: F401 +from .backend import BACKENDS # noqa: F401 diff --git a/python/confget/__main__.py b/python/confget/__main__.py new file mode 100755 index 0000000..919fcae --- /dev/null +++ b/python/confget/__main__.py @@ -0,0 +1,295 @@ +# Copyright (c) 2018, 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +The command-line interface to the confget module: specify some parameters +through command-line options, then display variable values or names. +""" + +from __future__ import print_function + +import argparse +import sys + +from . import backend +from . import defs +from . import format as fmt + +try: + from typing import List, Optional + + _TYPING_USED = (defs, List, Optional) +except ImportError: + pass + + +class MainConfig(fmt.FormatConfig): + # pylint: disable=too-few-public-methods,too-many-instance-attributes + """ + Extend the format config class with some output settings. + + Add the following settings: + - check_only (boolean): only check whether a variable is defined + - query_sections (boolean): only display the section names + """ + + def __init__(self, # type: MainConfig + check_only, # type: bool + filename, # type: Optional[str] + list_all, # type: bool + match_regex, # type: bool + match_var_names, # type: bool + match_var_values, # type: Optional[str] + name_prefix, # type: Optional[str] + query_sections, # type: bool + section, # type: str + section_override, # type: bool + section_specified, # type: bool + show_var_name, # type: bool + shell_escape, # type: bool + varnames # type: List[str] + ): # type: (...) -> None + # pylint: disable=too-many-arguments,too-many-locals + """ Store the specified configuration values. """ + super(MainConfig, self).__init__( + filename=filename, + list_all=list_all, + match_regex=match_regex, + match_var_names=match_var_names, + match_var_values=match_var_values, + name_prefix=name_prefix, + section=section, + section_specified=section_specified, + section_override=section_override, + show_var_name=show_var_name, + shell_escape=shell_escape, + varnames=varnames, + ) + self.check_only = check_only + self.query_sections = query_sections + + +def version(): + # type: () -> None + """ + Display program version information. + """ + print('confget ' + defs.VERSION_STRING) + + +def features(name): + # type: (Optional[str]) -> None + """ + Display a list of the features supported by the program. + """ + if name is None: + print(' '.join([ + '{name}={version}'.format(name=item[0], version=item[1]) + for item in defs.FEATURES + ])) + else: + ver = dict(defs.FEATURES).get(name, None) + if ver is None: + sys.exit(1) + print(ver) + + +def output_check_only(cfg, data): + # type: (MainConfig, defs.ConfigData) -> None + """ Check whether the variable is present. """ + if cfg.section not in data: + sys.exit(1) + elif cfg.varnames[0] not in data[cfg.section]: + sys.exit(1) + sys.exit(0) + + +def output_vars(cfg, data): + # type: (MainConfig, defs.ConfigData) -> None + """ Output the variable values. """ + for vitem in fmt.filter_vars(cfg, data): + print(vitem.output_full) + + +def output_sections(data): + # type: (defs.ConfigData) -> None + """ Output the section names. """ + for name in sorted(data.keys()): + if name != '': + print(name) + + +def validate_options(args, backend_name): + # type: (argparse.Namespace, str) -> None + """ + Make sure the command-line options are not used in an invalid combination. + """ + query_sections = args.query == 'sections' + + if args.list_all or query_sections: + if args.varnames: + sys.exit('Only a single query at a time, please!') + elif args.match_var_names: + if not args.varnames: + sys.exit('No patterns to match against') + elif args.check_only and len(args.varnames) > 1: + sys.exit('Only a single query at a time, please!') + elif not args.varnames: + sys.exit('No variables specified to query') + + if query_sections and backend_name != 'ini': + sys.exit("The query for sections is only supported for " + "the 'ini' backend for the present") + + +def check_option_conflicts(args): + # type: (argparse.Namespace) -> None + """ Make sure that the command-line options do not conflict. """ + total = int(args.query is not None) + int(args.match_var_names) + \ + int(args.list_all) + \ + int(bool(args.varnames) and + not (args.match_var_names or args.query == 'feature')) + if total > 1: + sys.exit('Only a single query at a time, please!') + + +def main(): + # type: () -> None + """ + The main program: parse arguments, do things. + """ + parser = argparse.ArgumentParser( + prog='confget', + usage=''' + confget [-t ini] -f filename [-s section] varname... + confget -V | -h | --help | --version + confget -q features''') + parser.add_argument('-c', action='store_true', dest='check_only', + help='check whether the variables are defined in ' + 'the file') + parser.add_argument('-f', type=str, dest='filename', + help='specify the configuration file name') + parser.add_argument('-L', action='store_true', dest='match_var_names', + help='specify which variables to display') + parser.add_argument('-l', action='store_true', dest='list_all', + help='list all variables in the specified section') + parser.add_argument('-m', type=str, dest='match_var_values', + help='only display variables with values that match ' + 'the specified pattern') + parser.add_argument('-N', action='store_true', dest='show_var_name', + help='always display the variable name') + parser.add_argument('-n', action='store_true', dest='hide_var_name', + help='never display the variable name') + parser.add_argument('-O', action='store_true', dest='section_override', + help='allow variables in the specified section to ' + 'override those placed before any ' + 'section definitions') + parser.add_argument('-p', type=str, dest='name_prefix', + help='display this string before the variable name') + parser.add_argument('-q', type=str, dest='query', choices=[ + 'feature', + 'features', + 'sections' + ], help='query for a specific type of information, e.g. the list of ' + 'sections defined in ' 'the configuration file') + parser.add_argument('-S', action='store_true', dest='shell_quote', + help='quote the values suitably for the Bourne shell') + parser.add_argument('-s', type=str, dest='section', + help='specify the configuration file section') + parser.add_argument('-t', type=str, default='ini', dest='backend', + help='specify the configuration file type') + parser.add_argument('-V', '--version', action='store_true', + help='display program version information and exit') + parser.add_argument('-x', action='store_true', dest='match_regex', + help='treat the match patterns as regular expressions') + parser.add_argument('varnames', nargs='*', + help='the variable names to query') + + args = parser.parse_args() + if args.version: + version() + return + + check_option_conflicts(args) + + if args.query == 'features': + if args.varnames: + sys.exit('No arguments to -q features') + features(None) + return + if args.query == 'feature': + if len(args.varnames) != 1: + sys.exit('Only a single feature name expected') + features(args.varnames[0]) + return + + query_sections = args.query == 'sections' + + cfg = MainConfig( + check_only=args.check_only, + filename=args.filename, + list_all=args.list_all, + match_regex=args.match_regex, + match_var_names=args.match_var_names, + match_var_values=args.match_var_values, + name_prefix=args.name_prefix, + query_sections=query_sections, + section=args.section if args.section is not None else '', + section_override=args.section_override, + section_specified=args.section is not None, + shell_escape=args.shell_quote, + show_var_name=args.show_var_name or + ((args.match_var_names or args.list_all or len(args.varnames) > 1) and + not args.hide_var_name), + varnames=args.varnames, + ) + + matched_backends = [name for name in sorted(backend.BACKENDS.keys()) + if name.startswith(args.backend)] + if not matched_backends: + sys.exit('Unknown backend "{name}", use "list" for a list' + .format(name=args.backend)) + elif len(matched_backends) > 1: + sys.exit('Ambiguous backend "{name}": {lst}' + .format(name=args.backend, lst=' '.join(matched_backends))) + back = backend.BACKENDS[matched_backends[0]] + + validate_options(args, matched_backends[0]) + + try: + cfgp = back(cfg) + except Exception as exc: # pylint: disable=broad-except + sys.exit(str(exc)) + data = cfgp.read_file() + + if cfg.check_only: + output_check_only(cfg, data) + elif query_sections: + output_sections(data) + else: + output_vars(cfg, data) + + +if __name__ == '__main__': + main() diff --git a/python/confget/backend/__init__.py b/python/confget/backend/__init__.py new file mode 100644 index 0000000..51be726 --- /dev/null +++ b/python/confget/backend/__init__.py @@ -0,0 +1,42 @@ +# Copyright (c) 2018, 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Provide an interface to all the configuration format backends. +""" + +from . import abstract, http_get, ini + +try: + from typing import Dict, Type + + _TYPING_USED = (Dict, Type, abstract) +except ImportError: + pass + + +BACKENDS = { + 'http_get': http_get.HTTPGetBackend, + 'ini': ini.INIBackend, +} # type: Dict[str, Type[abstract.Backend]] diff --git a/python/confget/backend/abstract.py b/python/confget/backend/abstract.py new file mode 100644 index 0000000..1ee2c8d --- /dev/null +++ b/python/confget/backend/abstract.py @@ -0,0 +1,55 @@ +# Copyright (c) 2018, 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +An abstract metaclass for confget backends. +""" + +import abc + +import six + +from .. import defs + +try: + from typing import Dict + + _TYPING_USED = (defs, Dict) +except ImportError: + pass + + +@six.add_metaclass(abc.ABCMeta) +class Backend(object): + # pylint: disable=too-few-public-methods + """ An abstract confget parser backend. """ + def __init__(self, cfg): + # type: (Backend, defs.Config) -> None + self._cfg = cfg + + @abc.abstractmethod + def read_file(self): + # type: (Backend) -> defs.ConfigData + """ Read and parse the configuration file, invoke the callbacks. """ + raise NotImplementedError('Backend.read_file') diff --git a/python/confget/backend/http_get.py b/python/confget/backend/http_get.py new file mode 100644 index 0000000..a6f1ba4 --- /dev/null +++ b/python/confget/backend/http_get.py @@ -0,0 +1,110 @@ +# Copyright (c) 2018, 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +A confget backend for reading INI-style files. +""" + +import os +import re +import urllib + +from .. import defs + +from . import abstract + +try: + from typing import Dict, List + + _TYPING_USED = (defs, Dict, List) +except ImportError: + pass + + +try: + import urllib.parse # pylint: disable=ungrouped-imports + + urllib_unquote = urllib.parse.unquote # pylint: disable=invalid-name +except ImportError: + urllib_unquote = urllib.unquote # pylint: disable=invalid-name,no-member + + +RE_ENTITY = re.compile(r'^ (?P<full> [a-zA-Z0-9_]+ ; )', re.X) + + +class HTTPGetBackend(abstract.Backend): + # pylint: disable=too-few-public-methods + """ Parse INI-style configuration files. """ + + def __init__(self, cfg): + # type: (HTTPGetBackend, defs.Config) -> None + super(HTTPGetBackend, self).__init__(cfg) + + if self._cfg.filename is not None: + raise ValueError('No config filename expected') + + qname = 'QUERY_STRING' if self._cfg.section == '' \ + else self._cfg.section + qval = os.environ.get(qname, None) + if qval is None: + raise ValueError('No "{qname}" variable in the environment' + .format(qname=qname)) + self.query_string = qval + + def read_file(self): + # type: (HTTPGetBackend) -> defs.ConfigData + def split_by_amp(line): + # type: (str) -> List[str] + """ Split a line by "&" or "&" tokens. """ + if not line: + return [] + + start = end = 0 + while True: + pos = line[start:].find('&') + if pos == -1: + return [line] + if line[pos + 1:].startswith('amp;'): + end = pos + 4 + break + entity = RE_ENTITY.match(line[pos + 1:]) + if entity is None: + end = pos + break + start = pos + len(entity.group('full')) + + return [line[:pos]] + split_by_amp(line[end + 1:]) + + data = {} # type: Dict[str, str] + fragments = split_by_amp(self.query_string) + for varval in fragments: + fields = varval.split('=') + if len(fields) == 1: + fields.append('') + elif len(fields) != 2: + raise ValueError('Invalid query string component: "{varval}"' + .format(varval=varval)) + data[urllib_unquote(fields[0])] = urllib_unquote(fields[1]) + + return {self._cfg.section: data} diff --git a/python/confget/backend/ini.py b/python/confget/backend/ini.py new file mode 100644 index 0000000..44b9470 --- /dev/null +++ b/python/confget/backend/ini.py @@ -0,0 +1,176 @@ +# Copyright (c) 2018, 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +A confget backend for reading INI-style files. +""" + +import re +import sys + +from .. import defs + +from . import abstract + +try: + from typing import Callable, Dict, Match, NamedTuple, Pattern + + _TYPING_USED = (defs, Dict) + + MatcherType = NamedTuple('MatcherType', [ + ('regex', Pattern[str]), + ('handle', Callable[[ + Match[str], + Dict[str, str], + defs.Config, + defs.ConfigData, + ], None]), + ]) +except ImportError: + import collections + + MatcherType = collections.namedtuple('MatcherType', [ # type: ignore + 'regex', + 'handle', + ]) + + +class INIBackend(abstract.Backend): + # pylint: disable=too-few-public-methods + """ Parse INI-style configuration files. """ + + def __init__(self, cfg, encoding='UTF-8'): + # type: (INIBackend, defs.Config, str) -> None + super(INIBackend, self).__init__(cfg) + + if self._cfg.filename is None: + raise ValueError('No config filename specified') + elif self._cfg.filename == '-': + infile = sys.stdin + else: + infile = open(self._cfg.filename, mode='r') + + reconfigure = getattr(infile, 'reconfigure', None) + if reconfigure is not None: + reconfigure(encoding=encoding) + + self.infile = infile + + def read_file(self): + # type: (INIBackend) -> defs.ConfigData + state = { + 'section': '', + 'name': '', + 'value': '', + 'cont': '', + 'found': '', + } + res = {'': {}} # type: defs.ConfigData + + def handle_section(match, # type: Match[str] + state, # type: Dict[str, str] + cfg, # type: defs.Config + res # type: defs.ConfigData + ): # type: (...) -> None + """ Handle a section heading: store the name. """ + state['section'] = match.group('name') + if state['section'] not in res: + res[state['section']] = {} + if not (cfg.section_specified or cfg.section or state['found']): + cfg.section = state['section'] + state['found'] = state['section'] + + def handle_comment(_match, # type: Match[str] + _state, # type: Dict[str, str] + _cfg, # type: defs.Config + _res # type: defs.ConfigData + ): # type: (...) -> None + """ Handle a comment line: ignore it. """ + pass # pylint: disable=unnecessary-pass + + def handle_variable(match, # type: Match[str] + state, # type: Dict[str, str] + _cfg, # type: defs.Config + res # type: defs.ConfigData + ): # type: (...) -> None + """ Handle an assignment: store, check for a continuation. """ + state['name'] = match.group('name') + state['value'] = match.group('value') + state['cont'] = match.group('cont') + state['found'] = state['name'] + if not state['cont']: + res[state['section']][state['name']] = state['value'] + + matches = [ + MatcherType( + regex=re.compile(r'^ \s* (?: [#;] .* )? $', re.X), + handle=handle_comment, + ), + MatcherType( + regex=re.compile(r''' + ^ + \s* \[ \s* + (?P<name> [^\]]+? ) + \s* \] \s* + $''', + re.X), + handle=handle_section, + ), + MatcherType( + regex=re.compile(r''' + ^ + \s* + (?P<name> \S+ ) + \s* = \s* + (?P<value> .*? ) + \s* + (?P<cont> [\\] )? + $''', re.X), + handle=handle_variable, + ), + ] + + for line in self.infile.readlines(): + line = line.rstrip('\r\n') + if state['cont']: + if line.endswith('\\'): + line, state['cont'] = line[:-1], line[-1] + else: + state['cont'] = '' + state['value'] += line + if not state['cont']: + res[state['section']][state['name']] = state['value'] + continue + + for data in matches: + match = data.regex.match(line) + if match is None: + continue + data.handle(match, state, self._cfg, res) + break + else: + raise ValueError('Unexpected line in {fname}: {line}' + .format(fname=self._cfg.filename, line=line)) + + return res diff --git a/python/confget/defs.py b/python/confget/defs.py new file mode 100644 index 0000000..21ae7c5 --- /dev/null +++ b/python/confget/defs.py @@ -0,0 +1,59 @@ +# Copyright (c) 2018, 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Common definitions for the confget configuration parsing library. +""" + +try: + from typing import Dict, List, Optional + + _TYPING_USED = (List, Optional) + + ConfigData = Dict[str, Dict[str, str]] +except ImportError: + pass + + +VERSION_STRING = '2.2.0' +FEATURES = [ + ('BASE', VERSION_STRING), +] + + +class Config(object): + # pylint: disable=too-few-public-methods + """ Base class for the internal confget configuration. """ + + def __init__(self, # type: Config + varnames, # type: List[str] + filename=None, # type: Optional[str] + section='', # type: str + section_specified=False, # type: bool + ): # type: (...) -> None + """ Store the specified configuration values. """ + self.filename = filename + self.section = section + self.section_specified = section_specified + self.varnames = varnames diff --git a/python/confget/format.py b/python/confget/format.py new file mode 100644 index 0000000..d2265b5 --- /dev/null +++ b/python/confget/format.py @@ -0,0 +1,221 @@ +# Copyright (c) 2018, 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Filter and format a subset of the configuration variables. +""" + +import fnmatch +import re + +try: + from typing import Callable, Dict, Iterable, NamedTuple, List, Optional + + _TYPING_USED = (Callable, Dict, Iterable, List, Optional) + + FormatOutput = NamedTuple('FormatOutput', [ + ('name', str), + ('value', str), + ('output_name', str), + ('output_value', str), + ('output_full', str), + ]) +except ImportError: + import collections + + FormatOutput = collections.namedtuple('FormatOutput', [ # type: ignore + 'name', + 'value', + 'output_name', + 'output_value', + 'output_full', + ]) + +from . import defs + + +class FormatConfig(defs.Config): + # pylint: disable=too-few-public-methods,too-many-instance-attributes + """ + Extend the config class with some output settings. + + Add the following settings: + - list_all (boolean): list all variables, not just a subset + - match_regex (boolean): for match_var_names and match_var_values, + perform regular expression matches instead of filename pattern ones + - match_var_names (boolean): treat the variable names specified as + patterns and display all variables that match those + - match_var_values (string): display only the variables with values + that match this pattern + - name_prefix (string): when displaying variable names, prefix them + with this string + - show_var_name (boolean): display the variable names, not just + the values + - shell_escape (boolean): format the values in a manner suitable for + the Bourne shell + """ + + def __init__(self, # type: FormatConfig + varnames, # type: List[str] + filename=None, # type: Optional[str] + list_all=False, # type: bool + match_regex=False, # type: bool + match_var_names=False, # type: bool + match_var_values=None, # type: Optional[str] + name_prefix=None, # type: Optional[str] + section='', # type: str + section_override=False, # type: bool + section_specified=False, # type: bool + show_var_name=False, # type: bool + shell_escape=False, # type: bool + ): # type: (...) -> None + # pylint: disable=too-many-arguments + """ Store the specified configuration values. """ + super(FormatConfig, self).__init__( + filename=filename, + section=section, + section_specified=section_specified, + varnames=varnames, + ) + self.list_all = list_all + self.match_regex = match_regex + self.match_var_names = match_var_names + self.match_var_values = match_var_values + self.name_prefix = name_prefix + self.section_override = section_override + self.shell_escape = shell_escape + self.show_var_name = show_var_name + + def __repr__(self): + # type: (FormatConfig) -> str + return '{tname}({varnames}, {attrs})'.format( + tname=type(self).__name__, + varnames=repr(self.varnames), + attrs=', '.join([ + '{name}={value}'.format( + name=name, value=repr(getattr(self, name))) + for name in [ + 'filename', + 'list_all', + 'match_regex', + 'match_var_names', + 'match_var_values', + 'name_prefix', + 'section', + 'section_override', + 'section_specified', + 'show_var_name', + 'shell_escape', + ] + ])) + + +def get_check_function(cfg, patterns): + # type: (FormatConfig, List[str]) -> Callable[[str], bool] + """ + Get a function that determines whether a variable name should be + included in the displayed subset. + """ + if cfg.match_regex: + re_vars = [re.compile(name) for name in patterns] + + def check_re_vars(key): + # type: (str) -> bool + """ Check that the key matches any of the specified regexes. """ + return any(rex.search(key) for rex in re_vars) + + return check_re_vars + + def check_fn_vars(key): + # type: (str) -> bool + """ Check that the key matches any of the specified patterns. """ + return any(fnmatch.fnmatch(key, pattern) + for pattern in patterns) + + return check_fn_vars + + +def get_varnames(cfg, sect_data): + # type: (FormatConfig, Dict[str, str]) -> Iterable[str] + """ Get the variable names that match the configuration requirements. """ + if cfg.list_all: + varnames = sect_data.keys() # type: Iterable[str] + elif cfg.match_var_names: + check_var = get_check_function(cfg, cfg.varnames) + varnames = [name for name in sect_data.keys() if check_var(name)] + else: + varnames = [name for name in cfg.varnames if name in sect_data] + + if not cfg.match_var_values: + return varnames + + check_value = get_check_function(cfg, [cfg.match_var_values]) + return [name for name in varnames if check_value(sect_data[name])] + + +def filter_vars(cfg, data): + # type: (FormatConfig, defs.ConfigData) -> Iterable[FormatOutput] + """ + Filter the variables in the configuration file according to + the various criteria specified in the configuration. + Return an iterable of FormatOutput structures allowing the caller to + process the variable names and values in various ways. + """ + if cfg.section_override: + sect_data = data[''] + else: + sect_data = {} + if cfg.section in data: + sect_data.update(data[cfg.section]) + + varnames = get_varnames(cfg, sect_data) + res = [] # type: List[FormatOutput] + for name in sorted(varnames): + if cfg.name_prefix: + output_name = cfg.name_prefix + name + else: + output_name = name + + value = sect_data[name] + if cfg.shell_escape: + output_value = "'{esc}'".format( + esc="'\"'\"'".join(value.split("'"))) + else: + output_value = value + + if cfg.show_var_name: + output_full = '{name}={value}'.format(name=output_name, + value=output_value) + else: + output_full = output_value + + res.append(FormatOutput( + name=name, + value=value, + output_name=output_name, + output_value=output_value, + output_full=output_full, + )) + + return res diff --git a/python/confget/py.typed b/python/confget/py.typed new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/python/confget/py.typed diff --git a/python/setup.cfg b/python/setup.cfg new file mode 100644 index 0000000..2a9acf1 --- /dev/null +++ b/python/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal = 1 diff --git a/python/setup.py b/python/setup.py new file mode 100644 index 0000000..2d9f620 --- /dev/null +++ b/python/setup.py @@ -0,0 +1,132 @@ +#!/usr/bin/python3 +# +# Copyright (c) 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Setup infrastructure for confget, the configuration file parser. +""" + +import re +import setuptools # type: ignore + + +RE_VERSION = r'''^ + \s* VERSION_STRING \s* = \s* ' + (?P<version> + (?: 0 | [1-9][0-9]* ) # major + \. (?: 0 | [1-9][0-9]* ) # minor + \. (?: 0 | [1-9][0-9]* ) # patchlevel + (?: \. [a-zA-Z0-9]+ )? # optional addendum (dev1, beta3, etc.) + ) + ' \s* + $''' + + +def get_version(): + # type: () -> str + """ Get the version string from the module's __init__ file. """ + found = None + re_semver = re.compile(RE_VERSION, re.X) + with open('confget/defs.py') as init: + for line in init.readlines(): + match = re_semver.match(line) + if not match: + continue + assert found is None + found = match.group('version') + + assert found is not None + return found + + +def get_long_description(): + # type: () -> str + """ Get the package long description from the README file. """ + with open('README.md') as readme: + return readme.read() + + +setuptools.setup( + name='confget', + version=get_version(), + + description='Parse configuration files and extract values from them', + long_description=get_long_description(), + long_description_content_type='text/markdown', + + author='Peter Pentchev', + author_email='roam@ringlet.net', + url='https://devel.ringlet.net/textproc/confget/', + + packages=['confget', 'confget.backend'], + package_data={ + 'confget': [ + # The typed module marker + 'py.typed', + ], + }, + + install_requires=[ + 'six', + ], + + license='BSD-2', + classifiers=[ + 'Development Status :: 5 - Production/Stable', + + 'Environment :: Console', + + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + + 'License :: DFSG approved', + 'License :: Freely Distributable', + 'License :: OSI Approved :: BSD License', + + 'Operating System :: POSIX', + 'Operating System :: Unix', + + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + + 'Topic :: Software Development :: Libraries', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: Utilities', + ], + + entry_points={ + 'console_scripts': [ + 'confget=confget.__main__:main', + ], + }, + + zip_safe=True, +) diff --git a/python/stubs/urllib/__init__.py b/python/stubs/urllib/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/python/stubs/urllib/__init__.py diff --git a/python/stubs/urllib/__init__.pyi b/python/stubs/urllib/__init__.pyi new file mode 100644 index 0000000..d86965b --- /dev/null +++ b/python/stubs/urllib/__init__.pyi @@ -0,0 +1,27 @@ +# Copyright (c) 2018, 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +def unquote(name): + # type: (str) -> str + ... diff --git a/python/stubs/urllib/parse.pyi b/python/stubs/urllib/parse.pyi new file mode 100644 index 0000000..d86965b --- /dev/null +++ b/python/stubs/urllib/parse.pyi @@ -0,0 +1,27 @@ +# Copyright (c) 2018, 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +def unquote(name): + # type: (str) -> str + ... diff --git a/python/stubs/urllib/py.typed b/python/stubs/urllib/py.typed new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/python/stubs/urllib/py.typed diff --git a/python/tox.ini b/python/tox.ini new file mode 100644 index 0000000..57f11d4 --- /dev/null +++ b/python/tox.ini @@ -0,0 +1,143 @@ +# Copyright (c) 2018, 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +[tox] +envlist = pep8,mypy2,mypy3,prove_2,prove_2_nt,prove_3,unit_tests_2,unit_tests_2_nt,unit_tests_3,pylint +skipsdist = True + +[testenv:pep8] +basepython = python3 +deps = + flake8 +commands = + flake8 confget setup.py unit_tests ../t/defs/tools/generate.py + +[testenv:mypy2] +basepython = python3 +deps = + mypy +setenv = + MYPYPATH={toxinidir}/stubs +commands = + mypy --strict --py2 confget setup.py + mypy --strict --py2 --allow-untyped-decorators unit_tests + +[testenv:mypy3] +basepython = python3 +deps = + mypy +setenv = + MYPYPATH={toxinidir}/stubs +commands = + mypy --strict confget setup.py ../t/defs/tools/generate.py + mypy --strict --allow-untyped-decorators unit_tests + +[testenv:pylint] +basepython = python3 +deps = + ddt + pylint + pytest +commands = + pylint --disable=useless-object-inheritance,duplicate-code confget setup.py unit_tests ../t/defs/tools/generate.py + +[testenv:unit_tests_2] +basepython = python2 +deps = + ddt + pytest + typing +setenv = + TESTDIR={toxinidir}/../t +commands = + pytest -s -vv unit_tests + +[testenv:unit_tests_2_nt] +basepython = python2 +deps = + ddt + pytest +setenv = + TESTDIR={toxinidir}/../t +commands = + pytest -s -vv unit_tests + +[testenv:unit_tests_3] +basepython = python3 +deps = + ddt + pytest +setenv = + TESTDIR={toxinidir}/../t +commands = + pytest -s -vv unit_tests + +[testenv:generate] +basepython = python3 +deps = + six +whitelist_externals = + sh +commands = + sh -c 'cd ../t && env PYTHONPATH={toxinidir} python defs/tools/generate.py' + +[testenv:prove_2] +basepython = python2 +deps = + six + typing +setenv = + CONFGET=python -m confget + MANPAGE={toxinidir}/../confget.1 + TESTDIR={toxinidir}/../t +whitelist_externals = + prove +commands = + prove ../t + +[testenv:prove_2_nt] +basepython = python2 +deps = + six +setenv = + CONFGET=python -m confget + MANPAGE={toxinidir}/../confget.1 + TESTDIR={toxinidir}/../t +whitelist_externals = + prove +commands = + prove ../t + +[testenv:prove_3] +basepython = python3 +deps = + six +setenv = + CONFGET=python -m confget + MANPAGE={toxinidir}/../confget.1 + TESTDIR={toxinidir}/../t +whitelist_externals = + prove +commands = + prove ../t diff --git a/python/unit_tests/__init__.py b/python/unit_tests/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/python/unit_tests/__init__.py diff --git a/python/unit_tests/data/__init__.py b/python/unit_tests/data/__init__.py new file mode 100644 index 0000000..fbf1975 --- /dev/null +++ b/python/unit_tests/data/__init__.py @@ -0,0 +1,31 @@ +# Copyright (c) 2018, 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Data structures and definitions for the confget unit tests. +""" + +from .defs import CMDLINE_OPTIONS, XFORM, TestDef # noqa: F401 +from .load import load_all_tests # noqa: F401 +from .util import shell_escape # noqa: F401 diff --git a/python/unit_tests/data/defs.py b/python/unit_tests/data/defs.py new file mode 100644 index 0000000..759487a --- /dev/null +++ b/python/unit_tests/data/defs.py @@ -0,0 +1,295 @@ +# Copyright (c) 2018, 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Class definitions for the confget test suite. +""" + +import abc +import os + +import six + +from confget import backend as cbackend +from confget import format as cformat + +from . import util + + +try: + from typing import Any, Dict, Iterable, List, Optional, Type + + _TYPING_USED = (Any, Dict, Iterable, List, Optional, Type) +except ImportError: + pass + + +CMDLINE_OPTIONS = { + 'check_only': ('-c', False), + 'filename': ('-f', True), + 'hide_var_name': ('-n', False), + 'list_all': ('-l', False), + 'match_var_names': ('-L', False), + 'match_var_values': ('-m', True), + 'section': ('-s', True), + 'section_override': ('-O', False), + 'section_specified': ('', False), + 'show_var_name': ('-N', False), +} + + +@six.add_metaclass(abc.ABCMeta) +class XFormType(object): + """ Transform something to something else with great prejudice. """ + + @abc.abstractproperty + def command(self): + # type: (XFormType) -> str + """ Get the shell command to transform the confget output. """ + raise NotImplementedError( + '{tname}.command'.format(tname=type(self).__name__)) + + @abc.abstractmethod + def do_xform(self, res): + # type: (XFormType, Iterable[cformat.FormatOutput]) -> str + """ Transform the Python representation of the result. """ + raise NotImplementedError( + '{tname}.do_xform()'.format(tname=type(self).__name__)) + + +class XFormNone(XFormType): + """ No transformation, newlines preserved. """ + + @property + def command(self): + # type: (XFormNone) -> str + return '' + + def do_xform(self, res): + # type: (XFormNone, Iterable[cformat.FormatOutput]) -> str + xform = '\n'.join([line.output_full for line in res]) # type: str + return xform + + +class XFormNewlineToSpace(XFormType): + """ Translate newlines to spaces. """ + + @property + def command(self): + # type: (XFormNewlineToSpace) -> str + return '| tr "\\n" " "' + + def do_xform(self, res): + # type: (XFormNewlineToSpace, Iterable[cformat.FormatOutput]) -> str + xform = ''.join([line.output_full + ' ' for line in res]) # type: str + return xform + + +class XFormCountLines(XFormType): + """ Count the lines output by confget. """ + + def __init__(self, sought=None, sought_in=True): + # type: (XFormCountLines, Optional[str], bool) -> None + super(XFormCountLines, self).__init__() + self.sought = sought + self.sought_in = sought_in + + @property + def command(self): + # type: (XFormCountLines) -> str + if self.sought: + prefix = '| fgrep -{inv}e {sought} '.format( + inv='' if self.sought_in else 'v', + sought=util.shell_escape(self.sought)) + else: + prefix = '' + return prefix + "| wc -l | tr -d ' '" + + def do_xform(self, res): + # type: (XFormCountLines, Iterable[cformat.FormatOutput]) -> str + if self.sought: + return str(len( + [line for line in res + if self.sought_in == (self.sought in line.output_full)])) + return str(len([line for line in res])) + + +XFORM = { + '': XFormNone(), + 'count-lines': XFormCountLines(), + 'count-lines-eq': XFormCountLines(sought='='), + 'count-lines-non-eq': XFormCountLines(sought='=', sought_in=False), + 'newline-to-space': XFormNewlineToSpace(), +} + + +@six.add_metaclass(abc.ABCMeta) +class TestOutputDef(object): + """ A definition for a single test's output. """ + + def __init(self): + # type: (TestOutputDef) -> None + """ No initialization at all for the base class. """ + + @abc.abstractmethod + def get_check(self): + # type: (TestOutputDef) -> str + """ Get the check string as a shell command. """ + raise NotImplementedError( + '{name}.get_check()'.format(name=type(self).__name__)) + + @abc.abstractproperty + def var_name(self): + # type: (TestOutputDef) -> str + """ Get the variable name to display. """ + raise NotImplementedError( + '{name}.var_name'.format(name=type(self).__name__)) + + @abc.abstractmethod + def check_result(self, _res): + # type: (TestOutputDef, str) -> None + """ Check whether the processed confget result is correct. """ + raise NotImplementedError( + '{name}.check_result()'.format(name=type(self).__name__)) + + +class TestExactOutputDef(TestOutputDef): + """ Check that the program output this exact string. """ + + def __init__(self, exact): + # type: (TestExactOutputDef, str) -> None + """ Initialize an exact test output object. """ + self.exact = exact + + def get_check(self): + # type: (TestExactOutputDef) -> str + return '[ "$v" = ' + util.shell_escape(self.exact) + ' ]' + + @property + def var_name(self): + # type: (TestExactOutputDef) -> str + return 'v' + + def check_result(self, res): + # type: (TestExactOutputDef, str) -> None + assert res == self.exact + + +class TestExitOKOutputDef(TestOutputDef): + """ Check that the program succeeded or failed as expected. """ + + def __init__(self, success): + # type: (TestExitOKOutputDef, bool) -> None + """ Initialize an "finished successfully" test output object. """ + self.success = success + + def get_check(self): + # type: (TestExitOKOutputDef) -> str + return '[ "$res" {compare} 0 ]'.format( + compare="=" if self.success else "!=") + + @property + def var_name(self): + # type: (TestExitOKOutputDef) -> str + return 'res' + + def check_result(self, res): + # type: (TestExitOKOutputDef, str) -> None + # pylint: disable=useless-super-delegation + super(TestExitOKOutputDef, self).check_result(res) + + +class TestDef: + # pylint: disable=too-few-public-methods + """ A definition for a single test. """ + + def __init__(self, # type: TestDef + args, # type: Dict[str, str] + keys, # type: List[str] + output, # type: TestOutputDef + xform='', # type: str + backend='ini', # type: str + setenv=False, # type: bool + stdin=None, # type: Optional[str] + ): # type: (...) -> None + # pylint: disable=too-many-arguments + """ Initialize a test object. """ + + self.args = args + self.keys = keys + self.xform = xform + self.output = output + self.backend = backend + self.setenv = setenv + self.stdin = stdin + + def get_backend(self): + # type: (TestDef) -> Type[cbackend.abstract.Backend] + """ Get the appropriate confget backend type. """ + return cbackend.BACKENDS[self.backend] + + def get_config(self): + # type: (TestDef) -> cformat.FormatConfig + """ Convert the test's data to a config object. """ + data = {} # type: Dict[str, Any] + for name, value in self.args.items(): + if name == 'hide_var_name': + continue + + opt = CMDLINE_OPTIONS[name] + if opt[1]: + data[name] = value + else: + data[name] = True + + if 'filename' in data: + data['filename'] = os.environ['TESTDIR'] + '/' + data['filename'] + elif self.stdin: + data['filename'] = '-' + + data['show_var_name'] = 'show_var_name' in self.args or \ + (('match_var_names' in self.args or + 'list_all' in self.args or + len(self.keys) > 1) and + 'hide_var_name' not in self.args) + + return cformat.FormatConfig(self.keys, **data) + + def do_xform(self, res): + # type: (TestDef, Iterable[cformat.FormatOutput]) -> str + """ Return the output delimiter depending on the xform property. """ + return XFORM[self.xform].do_xform(res) + + +class TestFileDef: + # pylint: disable=too-few-public-methods + """ A definition for a file defining related tests. """ + + def __init__(self, # type: TestFileDef + tests, # type: List[TestDef] + setenv=None, # type: Optional[Dict[str, str]] + ): # type: (...) -> None + """ Initialize a test file object. """ + self.tests = tests + self.setenv = {} if setenv is None else setenv diff --git a/python/unit_tests/data/load.py b/python/unit_tests/data/load.py new file mode 100644 index 0000000..cf23f54 --- /dev/null +++ b/python/unit_tests/data/load.py @@ -0,0 +1,106 @@ +# Copyright (c) 2018, 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Load a test definition from a JSON file. +""" +# pylint: disable=consider-using-dict-comprehension + +import json +import os + +from . import defs + +try: + from typing import Any, Dict + + TESTING_USED = (defs, Any, Dict) +except ImportError: + pass + + +def _load_test_v1(data, _version): + # type: (Dict[str, Any], Dict[str, int]) -> defs.TestFileDef + """ Load the tests from a v1.x test file. """ + build = { + 'setenv': data.get('setenv', {}), + 'tests': [], + } + + for test in data['tests']: + raw = dict([ + (key, value) for key, value in test.items() + if key in ('args', 'keys', 'xform', 'backend', 'setenv', 'stdin') + ]) + + if 'exact' in test['output']: + raw['output'] = defs.TestExactOutputDef( + exact=test['output']['exact']) + elif 'exit' in test['output']: + raw['output'] = defs.TestExitOKOutputDef( + success=test['output']['exit']) + else: + raise ValueError('test output: ' + repr(test['output'])) + + build['tests'].append(defs.TestDef(**raw)) + + return defs.TestFileDef(**build) + + +_PARSERS = { + 1: _load_test_v1, +} + + +def load_test(fname): + # type: (str) -> defs.TestFileDef + """ Load a single test file into a TestFileDef object. """ + with open(fname, mode='r') as testf: + data = json.load(testf) + + version = { + 'major': data['format']['version']['major'], + 'minor': data['format']['version']['minor'], + } + assert isinstance(version['major'], int) + assert isinstance(version['minor'], int) + + parser = _PARSERS.get(version['major'], None) + if parser is not None: + return parser(data, version) + raise NotImplementedError( + 'Unsupported test file format version {major}.{minor} for {fname}' + .format(major=version['major'], minor=version['minor'], fname=fname)) + + +def load_all_tests(testdir): + # type: (str) -> Dict[str, defs.TestFileDef] + """ Load all the tests in the defs/tests/ subdirectory. """ + tdir = testdir + '/defs/tests/' + filenames = sorted(fname for fname in os.listdir(tdir) + if fname.endswith('.json')) + return dict([ + (os.path.splitext(fname)[0], load_test(tdir + fname)) + for fname in filenames + ]) diff --git a/python/unit_tests/data/util.py b/python/unit_tests/data/util.py new file mode 100644 index 0000000..32d7489 --- /dev/null +++ b/python/unit_tests/data/util.py @@ -0,0 +1,45 @@ +# Copyright (c) 2018, 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Utility functions for the confget test cases. +""" + +import re + + +_RE_APOS = re.compile("(?P<pre> [^']* ) (?P<apos> '+ ) (?P<post> .* )", re.X) + + +def shell_escape(value): + # type: (str) -> str + """ Escape a value for the shell. """ + res = "'" + while True: + match = _RE_APOS.match(value) + if not match: + return res + value + "'" + + res += match.group('pre') + '\'"' + match.group('apos') + '"\'' + value = match.group('post') diff --git a/python/unit_tests/test_run.py b/python/unit_tests/test_run.py new file mode 100644 index 0000000..9a3c13b --- /dev/null +++ b/python/unit_tests/test_run.py @@ -0,0 +1,136 @@ +# Copyright (c) 2018, 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Run the confget tests using the Python methods. + +Load the test data, then run the tests using the objects provided by +the Python library, not by executing the command-line tool. +""" + +from __future__ import print_function + +import itertools +import os +import sys +import unittest + +import ddt # type: ignore +import pytest # type: ignore + +from confget import format as cformat + +pytest.register_assert_rewrite('unit_tests.data.defs') + +from . import data as tdata # noqa: E402 pylint: disable=wrong-import-position + + +try: + from typing import Dict + + _TYPING_USED = (Dict,) +except ImportError: + pass + + +TESTS = tdata.load_all_tests(os.environ['TESTDIR']) + +FULL_TEST_DATA = sorted(itertools.chain(*[ + [ + ( + tfile[0], + idx, + tfile[1].setenv, + test, + ) + for idx, test in enumerate(tfile[1].tests) + ] + for tfile in TESTS.items() +])) + +SKIP_ARGS = set(['check_only']) + + +@ddt.ddt +class TestStuff(unittest.TestCase): + # pylint: disable=no-self-use + """ Run the tests using the Python library. """ + + @ddt.data( + *FULL_TEST_DATA + ) + @ddt.unpack + def test_run(self, # type: TestStuff + fname, # type: str + idx, # type: int + setenv, # type: Dict[str, str] + test, # type: tdata.TestDef + ): # type: (...) -> None + """ Instantiate a confget object, load the data, check it. """ + print('') + + save_env = dict(os.environ) + try: + print('fname {fname} {idx:2} setenv {count} keys {kkk}' + .format(fname=fname, idx=idx, count=len(setenv.keys()), + kkk=test.keys)) + if set(test.args.keys()) & SKIP_ARGS: + print('- skipping: {skip}'.format( + skip=' '.join(sorted(set(test.args.keys()) & SKIP_ARGS)))) + return + + backend = test.get_backend() + print('- backend {back}'.format(back=backend)) + config = test.get_config() + print('- config {cfg}'.format(cfg=config)) + if test.setenv: + os.environ.update(setenv) + + if test.stdin: + fname = os.environ['TESTDIR'] + '/' + test.stdin + print('- reopening {fname} as stdin'.format(fname=fname)) + with open(fname, mode='r') as stdin: + save_stdin = sys.stdin + try: + sys.stdin = stdin + obj = backend(config) + print('- obj {obj}'.format(obj=obj)) + data = obj.read_file() + finally: + sys.stdin = save_stdin + else: + obj = backend(config) + print('- obj {obj}'.format(obj=obj)) + data = obj.read_file() + + print('- sections: {sect}'.format(sect=sorted(data.keys()))) + res = cformat.filter_vars(config, data) + print('- result: {res}'.format(res=res)) + output = test.do_xform(res) + print('- transformed: {output}'.format(output=repr(output))) + test.output.check_result(output) + finally: + if test.setenv: + os.environ.clear() + os.environ.update(save_env) diff --git a/t/01-get-values.t b/t/01-get-values.t index d259bf1..f338516 100644 --- a/t/01-get-values.t +++ b/t/01-get-values.t @@ -1,6 +1,6 @@ #!/bin/sh # -# Copyright (c) 2008, 2011 Peter Pentchev +# Copyright (c) 2008, 2011, 2019 Peter Pentchev # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -27,54 +27,64 @@ [ -z "$CONFGET" ] && CONFGET='./confget' [ -z "$TESTDIR" ] && TESTDIR='t' -T1="$TESTDIR/t1.ini" - echo '1..18' -if [ ! -f "$T1" ]; then - echo "Bail out! No test file $T1" - exit 255 -fi -v=`$CONFGET -f "$T1" -s a key1` +if [ ! -f "$TESTDIR/t1.ini" ]; then + echo "Bail out! No test file $TESTDIR/t1.ini" + exit 255 +fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' 'key1' ` +res="$?" if [ "$v" = 'value1' ]; then echo 'ok 1'; else echo "not ok 1 v is '$v'"; fi -v=`$CONFGET -f "$T1" -N -s a key2` +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-N' 'key2' ` +res="$?" if [ "$v" = 'key2=value2' ]; then echo 'ok 2'; else echo "not ok 2 v is '$v'"; fi -v=`$CONFGET -f "$T1" -s a key3` -if [ "$v" = " val'ue3" ]; then echo 'ok 3'; else echo "not ok 3 v is '$v'"; fi -v=`$CONFGET -f "$T1" -s 'b sect' key4` -if [ "$v" = "v'alu'e4" ]; then echo 'ok 4'; else echo "not ok 4 v is '$v'"; fi -v=`$CONFGET -f "$T1" -s c key5` +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' 'key3' ` +res="$?" +if [ "$v" = ' val'"'"'ue3' ]; then echo 'ok 3'; else echo "not ok 3 v is '$v'"; fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'b sect' 'key4' ` +res="$?" +if [ "$v" = 'v'"'"'alu'"'"'e4' ]; then echo 'ok 4'; else echo "not ok 4 v is '$v'"; fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'c' 'key5' ` +res="$?" if [ "$v" = ' # value5' ]; then echo 'ok 5'; else echo "not ok 5 v is '$v'"; fi -v=`$CONFGET -f "$T1" -s a key1 key2 | tr "\n" ' '` +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' 'key1' 'key2' | tr "\n" " "` +res="$?" if [ "$v" = 'key1=value1 key2=value2 ' ]; then echo 'ok 6'; else echo "not ok 6 v is '$v'"; fi -v=`$CONFGET -f "$T1" -s a -n key6 key2 | tr "\n" ' '` +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-n' 'key6' 'key2' | tr "\n" " "` +res="$?" if [ "$v" = 'value2 value6 ' ]; then echo 'ok 7'; else echo "not ok 7 v is '$v'"; fi -v=`$CONFGET -f "$T1" -s 'b sect' key7` +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'b sect' 'key7' ` +res="$?" if [ "$v" = 'value7' ]; then echo 'ok 8'; else echo "not ok 8 v is '$v'"; fi -v=`$CONFGET -f - -s 'b sect' key7 < "$T1"` +v=`$CONFGET '-s' 'b sect' -f - 'key7' < "$TESTDIR/t1.ini" ` +res="$?" if [ "$v" = 'value7' ]; then echo 'ok 9'; else echo "not ok 9 v is '$v'"; fi -v=`$CONFGET -f - -s 'b sect' key77 < "$T1"` +v=`$CONFGET '-s' 'b sect' -f - 'key77' < "$TESTDIR/t1.ini" ` +res="$?" if [ "$v" = '' ]; then echo 'ok 10'; else echo "not ok 10 v is '$v'"; fi - -QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' -Q1='key4&key5=%09%09%20val%27ue5&key6' -Q2='' -export QUERY_STRING Q1 Q2 - -v=`$CONFGET -t http_get key1` +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get 'key1' ` +res="$?" if [ "$v" = 'value1' ]; then echo 'ok 11'; else echo "not ok 11 v is '$v'"; fi -v=`$CONFGET -t http_get -N key2` +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-N' 'key2' ` +res="$?" if [ "$v" = 'key2==value2&' ]; then echo 'ok 12'; else echo "not ok 12 v is '$v'"; fi -v=`$CONFGET -t http_get key3` -if [ "$v" = " val'ue3" ]; then echo 'ok 13'; else echo "not ok 13 v is '$v'"; fi -v=`$CONFGET -t http_get -s Q1 key4` -if [ "$v" = "" ]; then echo 'ok 14'; else echo "not ok 14 v is '$v'"; fi -v=`$CONFGET -t http_get -s Q1 key5` -if [ "$v" = " val'ue5" ]; then echo 'ok 15'; else echo "not ok 15 v is '$v'"; fi -v=`$CONFGET -t http_get -s Q1 key6` -if [ "$v" = "" ]; then echo 'ok 16'; else echo "not ok 16 v is '$v'"; fi -v=`$CONFGET -t http_get -s Q1 key66` -if [ "$v" = "" ]; then echo 'ok 17'; else echo "not ok 17 v is '$v'"; fi -v=`$CONFGET -t http_get -s Q2 key66` -if [ "$v" = "" ]; then echo 'ok 18'; else echo "not ok 18 v is '$v'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get 'key3' ` +res="$?" +if [ "$v" = ' val'"'"'ue3' ]; then echo 'ok 13'; else echo "not ok 13 v is '$v'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-s' 'Q1' 'key4' ` +res="$?" +if [ "$v" = '' ]; then echo 'ok 14'; else echo "not ok 14 v is '$v'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-s' 'Q1' 'key5' ` +res="$?" +if [ "$v" = ' val'"'"'ue5' ]; then echo 'ok 15'; else echo "not ok 15 v is '$v'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-s' 'Q1' 'key6' ` +res="$?" +if [ "$v" = '' ]; then echo 'ok 16'; else echo "not ok 16 v is '$v'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-s' 'Q1' 'key66' ` +res="$?" +if [ "$v" = '' ]; then echo 'ok 17'; else echo "not ok 17 v is '$v'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-s' 'Q2' 'key66' ` +res="$?" +if [ "$v" = '' ]; then echo 'ok 18'; else echo "not ok 18 v is '$v'"; fi diff --git a/t/02-check-values.t b/t/02-check-values.t index fd29a4c..f91fab3 100644 --- a/t/02-check-values.t +++ b/t/02-check-values.t @@ -1,6 +1,6 @@ #!/bin/sh # -# Copyright (c) 2008, 2011 Peter Pentchev +# Copyright (c) 2008, 2011, 2019 Peter Pentchev # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -27,33 +27,55 @@ [ -z "$CONFGET" ] && CONFGET='./confget' [ -z "$TESTDIR" ] && TESTDIR='t' -T1="$TESTDIR/t1.ini" - echo '1..15' -if [ ! -f "$T1" ]; then - echo "Bail out! No test file $T1" - exit 255 -fi - -if $CONFGET -f "$T1" -s a -c key1; then echo 'ok 1 '; else echo "not ok 1 $?"; fi -if $CONFGET -f "$T1" -s a -c key2; then echo 'ok 2 '; else echo "not ok 2 $?"; fi -if $CONFGET -f "$T1" -s a -c key3; then echo 'ok 3 '; else echo "not ok 3 $?"; fi -if ! $CONFGET -f "$T1" -s a -c key4; then echo 'ok 4 '; else echo "not ok 4 $?"; fi -if ! $CONFGET -f "$T1" -s 'b sect' -c key5; then echo 'ok 5 '; else echo "not ok 5 $?"; fi -if $CONFGET -f "$T1" -s 'b sect' -c key4; then echo 'ok 6 '; else echo "not ok 6 $?"; fi -if $CONFGET -f "$T1" -s c -c key5; then echo 'ok 7 '; else echo "not ok 7 $?"; fi -QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' -Q1='key4&key5=%09%09%20val%27ue5&key6' -Q2='' -export QUERY_STRING Q1 Q2 - -if $CONFGET -t http_get -c key1; then echo 'ok 8 '; else echo "not ok 8 $?"; fi -if $CONFGET -t http_get -c key2; then echo 'ok 9 '; else echo "not ok 9 $?"; fi -if $CONFGET -t http_get -c key3; then echo 'ok 10 '; else echo "not ok 10 $?"; fi -if $CONFGET -t http_get -s Q1 -c key4; then echo 'ok 11 '; else echo "not ok 11 $?"; fi -if $CONFGET -t http_get -s Q1 -c key6; then echo 'ok 12 '; else echo "not ok 12 $?"; fi -if ! $CONFGET -t http_get -c key6; then echo 'ok 13 '; else echo "not ok 13 $?"; fi -if ! $CONFGET -t http_get -s Q1 -c key1; then echo 'ok 14 '; else echo "not ok 14 $?"; fi -if ! $CONFGET -t http_get -s Q2 -c key1; then echo 'ok 15 '; else echo "not ok 15 $?"; fi +if [ ! -f "$TESTDIR/t1.ini" ]; then + echo "Bail out! No test file $TESTDIR/t1.ini" + exit 255 +fi +v=`$CONFGET '-c' '-f' "$TESTDIR/t1.ini" '-s' 'a' 'key1' ` +res="$?" +if [ "$res" = 0 ]; then echo 'ok 1'; else echo "not ok 1 res is '$res'"; fi +v=`$CONFGET '-c' '-f' "$TESTDIR/t1.ini" '-s' 'a' 'key2' ` +res="$?" +if [ "$res" = 0 ]; then echo 'ok 2'; else echo "not ok 2 res is '$res'"; fi +v=`$CONFGET '-c' '-f' "$TESTDIR/t1.ini" '-s' 'a' 'key3' ` +res="$?" +if [ "$res" = 0 ]; then echo 'ok 3'; else echo "not ok 3 res is '$res'"; fi +v=`$CONFGET '-c' '-f' "$TESTDIR/t1.ini" '-s' 'a' 'key4' ` +res="$?" +if [ "$res" != 0 ]; then echo 'ok 4'; else echo "not ok 4 res is '$res'"; fi +v=`$CONFGET '-c' '-f' "$TESTDIR/t1.ini" '-s' 'b sect' 'key5' ` +res="$?" +if [ "$res" != 0 ]; then echo 'ok 5'; else echo "not ok 5 res is '$res'"; fi +v=`$CONFGET '-c' '-f' "$TESTDIR/t1.ini" '-s' 'b sect' 'key4' ` +res="$?" +if [ "$res" = 0 ]; then echo 'ok 6'; else echo "not ok 6 res is '$res'"; fi +v=`$CONFGET '-c' '-f' "$TESTDIR/t1.ini" '-s' 'c' 'key5' ` +res="$?" +if [ "$res" = 0 ]; then echo 'ok 7'; else echo "not ok 7 res is '$res'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-c' 'key1' ` +res="$?" +if [ "$res" = 0 ]; then echo 'ok 8'; else echo "not ok 8 res is '$res'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-c' 'key2' ` +res="$?" +if [ "$res" = 0 ]; then echo 'ok 9'; else echo "not ok 9 res is '$res'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-c' 'key3' ` +res="$?" +if [ "$res" = 0 ]; then echo 'ok 10'; else echo "not ok 10 res is '$res'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-c' '-s' 'Q1' 'key4' ` +res="$?" +if [ "$res" = 0 ]; then echo 'ok 11'; else echo "not ok 11 res is '$res'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-c' '-s' 'Q1' 'key6' ` +res="$?" +if [ "$res" = 0 ]; then echo 'ok 12'; else echo "not ok 12 res is '$res'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-c' 'key6' ` +res="$?" +if [ "$res" != 0 ]; then echo 'ok 13'; else echo "not ok 13 res is '$res'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-c' '-s' 'Q1' 'key1' ` +res="$?" +if [ "$res" != 0 ]; then echo 'ok 14'; else echo "not ok 14 res is '$res'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-c' '-s' 'Q2' 'key1' ` +res="$?" +if [ "$res" != 0 ]; then echo 'ok 15'; else echo "not ok 15 res is '$res'"; fi diff --git a/t/03-list-all.t b/t/03-list-all.t index e3b525b..327cb2d 100644 --- a/t/03-list-all.t +++ b/t/03-list-all.t @@ -1,6 +1,6 @@ #!/bin/sh # -# Copyright (c) 2008, 2011 Peter Pentchev +# Copyright (c) 2008, 2011, 2019 Peter Pentchev # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -27,32 +27,31 @@ [ -z "$CONFGET" ] && CONFGET='./confget' [ -z "$TESTDIR" ] && TESTDIR='t' -T1="$TESTDIR/t1.ini" - echo '1..7' -if [ ! -f "$T1" ]; then - echo "Bail out! No test file $T1" - exit 255 -fi - -l=`$CONFGET -f "$T1" -s a -l | wc -l | tr -d ' '` -if [ "$l" = '4' ]; then echo 'ok 1'; else echo "not ok 1 '$l'"; fi -l=`$CONFGET -f "$T1" -N -s 'b sect' -l | fgrep -e '=' | wc -l | tr -d ' '` -if [ "$l" = '2' ]; then echo 'ok 2'; else echo "not ok 2 '$l'"; fi -l=`$CONFGET -f "$T1" -n -s c -l | fgrep -ve '=' | wc -l | tr -d ' '` -if [ "$l" = '1' ]; then echo 'ok 3'; else echo "not ok 3 '$l'"; fi -l=`$CONFGET -f "$T1" -s d -l | wc -l | tr -d ' '` -if [ "$l" = '0' ]; then echo 'ok 4'; else echo "not ok 4 '$l'"; fi -QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' -Q1='key4&key5=%09%09%20val%27ue5&key6' -Q2='' -export QUERY_STRING Q1 Q2 - -l=`$CONFGET -t http_get -l | wc -l | tr -d ' '` -if [ "$l" = '3' ]; then echo 'ok 5'; else echo "not ok 5 '$l'"; fi -l=`$CONFGET -t http_get -s Q1 -l | wc -l | tr -d ' '` -if [ "$l" = '3' ]; then echo 'ok 6'; else echo "not ok 6 '$l'"; fi -l=`$CONFGET -t http_get -s Q2 -l | wc -l | tr -d ' '` -if [ "$l" = '0' ]; then echo 'ok 7'; else echo "not ok 7 '$l'"; fi +if [ ! -f "$TESTDIR/t1.ini" ]; then + echo "Bail out! No test file $TESTDIR/t1.ini" + exit 255 +fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-l' | wc -l | tr -d ' '` +res="$?" +if [ "$v" = '4' ]; then echo 'ok 1'; else echo "not ok 1 v is '$v'"; fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'b sect' '-l' '-N' | fgrep -e '=' | wc -l | tr -d ' '` +res="$?" +if [ "$v" = '2' ]; then echo 'ok 2'; else echo "not ok 2 v is '$v'"; fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'c' '-l' '-n' | fgrep -ve '=' | wc -l | tr -d ' '` +res="$?" +if [ "$v" = '1' ]; then echo 'ok 3'; else echo "not ok 3 v is '$v'"; fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'd' '-l' | wc -l | tr -d ' '` +res="$?" +if [ "$v" = '0' ]; then echo 'ok 4'; else echo "not ok 4 v is '$v'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-l' | wc -l | tr -d ' '` +res="$?" +if [ "$v" = '3' ]; then echo 'ok 5'; else echo "not ok 5 v is '$v'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-l' '-s' 'Q1' | wc -l | tr -d ' '` +res="$?" +if [ "$v" = '3' ]; then echo 'ok 6'; else echo "not ok 6 v is '$v'"; fi +v=`env Q1='key4&key5=%09%09%20val%27ue5&key6' Q2='' QUERY_STRING='key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3' $CONFGET -t http_get '-l' '-s' 'Q2' | wc -l | tr -d ' '` +res="$?" +if [ "$v" = '0' ]; then echo 'ok 7'; else echo "not ok 7 v is '$v'"; fi diff --git a/t/04-test-manpage.t b/t/04-bespoke-test-manpage.t index ddc0607..ddc0607 100644 --- a/t/04-test-manpage.t +++ b/t/04-bespoke-test-manpage.t diff --git a/t/05-match-names.t b/t/05-match-names.t index be7d78a..cd529e0 100644 --- a/t/05-match-names.t +++ b/t/05-match-names.t @@ -1,6 +1,6 @@ #!/bin/sh # -# Copyright (c) 2008, 2011 Peter Pentchev +# Copyright (c) 2008, 2011, 2019 Peter Pentchev # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -27,20 +27,22 @@ [ -z "$CONFGET" ] && CONFGET='./confget' [ -z "$TESTDIR" ] && TESTDIR='t' -T1="$TESTDIR/t1.ini" - echo '1..4' -if [ ! -f "$T1" ]; then - echo "Bail out! No test file $T1" - exit 255 -fi -l=`$CONFGET -f "$T1" -s a -L '*' | wc -l | tr -d ' '` -if [ "$l" = '4' ]; then echo 'ok 1'; else echo "not ok 1 '$l'"; fi -l=`$CONFGET -f "$T1" -N -s 'b sect' -L '*' | fgrep -e '=' | wc -l | tr -d ' '` -if [ "$l" = '2' ]; then echo 'ok 2'; else echo "not ok 2 '$l'"; fi -l=`$CONFGET -f "$T1" -n -s a -L '*ey2' | fgrep -ve '=' | wc -l | tr -d ' '` -if [ "$l" = '1' ]; then echo 'ok 3'; else echo "not ok 3 '$l'"; fi -l=`$CONFGET -f "$T1" -s d -L '*ey2' | wc -l | tr -d ' '` -if [ "$l" = '0' ]; then echo 'ok 4'; else echo "not ok 4 '$l'"; fi +if [ ! -f "$TESTDIR/t1.ini" ]; then + echo "Bail out! No test file $TESTDIR/t1.ini" + exit 255 +fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-L' '*' | wc -l | tr -d ' '` +res="$?" +if [ "$v" = '4' ]; then echo 'ok 1'; else echo "not ok 1 v is '$v'"; fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'b sect' '-L' '-N' '*' | fgrep -e '=' | wc -l | tr -d ' '` +res="$?" +if [ "$v" = '2' ]; then echo 'ok 2'; else echo "not ok 2 v is '$v'"; fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-L' '-n' '*ey2' | fgrep -ve '=' | wc -l | tr -d ' '` +res="$?" +if [ "$v" = '1' ]; then echo 'ok 3'; else echo "not ok 3 v is '$v'"; fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'd' '-L' '*ey2' | wc -l | tr -d ' '` +res="$?" +if [ "$v" = '0' ]; then echo 'ok 4'; else echo "not ok 4 v is '$v'"; fi diff --git a/t/06-get-default.t b/t/06-get-default.t index 6755248..052f08e 100644 --- a/t/06-get-default.t +++ b/t/06-get-default.t @@ -1,6 +1,6 @@ #!/bin/sh # -# Copyright (c) 2008 Peter Pentchev +# Copyright (c) 2008, 2011, 2019 Peter Pentchev # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -27,27 +27,30 @@ [ -z "$CONFGET" ] && CONFGET='./confget' [ -z "$TESTDIR" ] && TESTDIR='t' -T1="$TESTDIR/t1.ini" -T2="$TESTDIR/t2.ini" - echo '1..5' -if [ ! -f "$T1" ]; then - echo "Bail out! No test file $T1" - exit 255 -fi -if [ ! -f "$T2" ]; then - echo "Bail out! No test file $T2" - exit 255 + +if [ ! -f "$TESTDIR/t1.ini" ]; then + echo "Bail out! No test file $TESTDIR/t1.ini" + exit 255 fi -v=`$CONFGET -f "$T1" key1` +if [ ! -f "$TESTDIR/t2.ini" ]; then + echo "Bail out! No test file $TESTDIR/t2.ini" + exit 255 +fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" 'key1' ` +res="$?" if [ "$v" = 'value1' ]; then echo 'ok 1'; else echo "not ok 1 v is '$v'"; fi -v=`$CONFGET -f "$T1" key4` +v=`$CONFGET '-f' "$TESTDIR/t1.ini" 'key4' ` +res="$?" if [ "$v" = '' ]; then echo 'ok 2'; else echo "not ok 2 v is '$v'"; fi -v=`$CONFGET -f "$T1" key6` +v=`$CONFGET '-f' "$TESTDIR/t1.ini" 'key6' ` +res="$?" if [ "$v" = 'value6' ]; then echo 'ok 3'; else echo "not ok 3 v is '$v'"; fi -v=`$CONFGET -f "$T2" key1` +v=`$CONFGET '-f' "$TESTDIR/t2.ini" 'key1' ` +res="$?" if [ "$v" = '1' ]; then echo 'ok 4'; else echo "not ok 4 v is '$v'"; fi -v=`$CONFGET -f "$T2" key1 key2 | tr "\n" ' '` +v=`$CONFGET '-f' "$TESTDIR/t2.ini" 'key1' 'key2' | tr "\n" " "` +res="$?" if [ "$v" = 'key1=1 key2=2 ' ]; then echo 'ok 5'; else echo "not ok 5 v is '$v'"; fi diff --git a/t/07-match-values.t b/t/07-match-values.t index bfcda6f..b66a9a8 100644 --- a/t/07-match-values.t +++ b/t/07-match-values.t @@ -1,6 +1,6 @@ #!/bin/sh # -# Copyright (c) 2008, 2011 Peter Pentchev +# Copyright (c) 2008, 2011, 2019 Peter Pentchev # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -27,28 +27,37 @@ [ -z "$CONFGET" ] && CONFGET='./confget' [ -z "$TESTDIR" ] && TESTDIR='t' -T1="$TESTDIR/t1.ini" - echo '1..9' -if [ ! -f "$T1" ]; then - echo "Bail out! No test file $T1" - exit 255 -fi -v=`$CONFGET -f "$T1" -m 'value*' -s a key1` +if [ ! -f "$TESTDIR/t1.ini" ]; then + echo "Bail out! No test file $TESTDIR/t1.ini" + exit 255 +fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-m' 'value*' 'key1' ` +res="$?" if [ "$v" = 'value1' ]; then echo 'ok 1'; else echo "not ok 1 v is '$v'"; fi -v=`$CONFGET -f "$T1" -m '*ue2' -s a key2` +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-m' '*ue2' 'key2' ` +res="$?" if [ "$v" = 'value2' ]; then echo 'ok 2'; else echo "not ok 2 v is '$v'"; fi -v=`$CONFGET -f "$T1" -m "*val'ue*" -s a key3` -if [ "$v" = " val'ue3" ]; then echo 'ok 3'; else echo "not ok 3 v is '$v'"; fi -v=`$CONFGET -f "$T1" -m '*alu*' -s 'b sect' key4` -if [ "$v" = "v'alu'e4" ]; then echo 'ok 4'; else echo "not ok 4 v is '$v'"; fi -v=`$CONFGET -f "$T1" -s c key5` +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-m' '*val'"'"'ue*' 'key3' ` +res="$?" +if [ "$v" = ' val'"'"'ue3' ]; then echo 'ok 3'; else echo "not ok 3 v is '$v'"; fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'b sect' '-m' '*alu*' 'key4' ` +res="$?" +if [ "$v" = 'v'"'"'alu'"'"'e4' ]; then echo 'ok 4'; else echo "not ok 4 v is '$v'"; fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'c' 'key5' ` +res="$?" if [ "$v" = ' # value5' ]; then echo 'ok 5'; else echo "not ok 5 v is '$v'"; fi -v=`$CONFGET -f "$T1" -m '*alu*' -s a key1 key2 | tr "\n" ' '` +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-m' '*alu*' 'key1' 'key2' | tr "\n" " "` +res="$?" if [ "$v" = 'key1=value1 key2=value2 ' ]; then echo 'ok 6'; else echo "not ok 6 v is '$v'"; fi -v=`$CONFGET -f "$T1" -s a -m '*7' key6` +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'a' '-m' '*7' 'key6' ` +res="$?" if [ "$v" = '' ]; then echo 'ok 7'; else echo "not ok 7 v is '$v'"; fi -if $CONFGET -f "$T1" -s 'b sect' -c -m '*7' key7; then echo 'ok 8'; else echo "not ok 8 code '$?'"; fi -if ! $CONFGET -f "$T1" -s 'b sect' -c -m '*7' key6; then echo 'ok 9'; else echo "not ok 9 code '$?'"; fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'b sect' '-c' '-m' '*7' 'key7' ` +res="$?" +if [ "$res" = 0 ]; then echo 'ok 8'; else echo "not ok 8 res is '$res'"; fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' 'b sect' '-c' '-m' '*7' 'key6' ` +res="$?" +if [ "$res" != 0 ]; then echo 'ok 9'; else echo "not ok 9 res is '$res'"; fi diff --git a/t/08-shell-quote.t b/t/08-bespoke-shell-quote.t index 49264ad..49264ad 100644 --- a/t/08-shell-quote.t +++ b/t/08-bespoke-shell-quote.t diff --git a/t/09-regexp.t b/t/09-bespoke-regexp.t index 684de13..684de13 100644 --- a/t/09-regexp.t +++ b/t/09-bespoke-regexp.t diff --git a/t/10-qsections.t b/t/10-bespoke-qsections.t index 7b7804a..7b7804a 100644 --- a/t/10-qsections.t +++ b/t/10-bespoke-qsections.t diff --git a/t/11-no-default.t b/t/11-no-default.t index 82b96e4..b531334 100644 --- a/t/11-no-default.t +++ b/t/11-no-default.t @@ -1,6 +1,6 @@ #!/bin/sh # -# Copyright (c) 2008, 2016 Peter Pentchev +# Copyright (c) 2008, 2011, 2019 Peter Pentchev # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -27,40 +27,45 @@ [ -z "$CONFGET" ] && CONFGET='./confget' [ -z "$TESTDIR" ] && TESTDIR='t' -T1="$TESTDIR/t1.ini" -T2="$TESTDIR/t2.ini" - echo '1..10' -if [ ! -f "$T1" ]; then - echo "Bail out! No test file $T1" - exit 255 -fi -if [ ! -f "$T2" ]; then - echo "Bail out! No test file $T2" - exit 255 + +if [ ! -f "$TESTDIR/t1.ini" ]; then + echo "Bail out! No test file $TESTDIR/t1.ini" + exit 255 fi -v=`$CONFGET -f "$T1" key1` +if [ ! -f "$TESTDIR/t2.ini" ]; then + echo "Bail out! No test file $TESTDIR/t2.ini" + exit 255 +fi +v=`$CONFGET '-f' "$TESTDIR/t1.ini" 'key1' ` +res="$?" if [ "$v" = 'value1' ]; then echo 'ok 1'; else echo "not ok 1 v is '$v'"; fi -v=`$CONFGET -f "$T1" key4` +v=`$CONFGET '-f' "$TESTDIR/t1.ini" 'key4' ` +res="$?" if [ "$v" = '' ]; then echo 'ok 2'; else echo "not ok 2 v is '$v'"; fi -v=`$CONFGET -f "$T1" key6` +v=`$CONFGET '-f' "$TESTDIR/t1.ini" 'key6' ` +res="$?" if [ "$v" = 'value6' ]; then echo 'ok 3'; else echo "not ok 3 v is '$v'"; fi - -v=`$CONFGET -f "$T1" -s '' key1` +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' '' 'key1' ` +res="$?" if [ "$v" = '' ]; then echo 'ok 4'; else echo "not ok 4 v is '$v'"; fi -v=`$CONFGET -f "$T1" -s '' key4` +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' '' 'key4' ` +res="$?" if [ "$v" = '' ]; then echo 'ok 5'; else echo "not ok 5 v is '$v'"; fi -v=`$CONFGET -f "$T1" -s '' key6` +v=`$CONFGET '-f' "$TESTDIR/t1.ini" '-s' '' 'key6' ` +res="$?" if [ "$v" = '' ]; then echo 'ok 6'; else echo "not ok 6 v is '$v'"; fi - -v=`$CONFGET -f "$T2" key1` +v=`$CONFGET '-f' "$TESTDIR/t2.ini" 'key1' ` +res="$?" if [ "$v" = '1' ]; then echo 'ok 7'; else echo "not ok 7 v is '$v'"; fi -v=`$CONFGET -f "$T2" key1 key2 | tr "\n" ' '` +v=`$CONFGET '-f' "$TESTDIR/t2.ini" 'key1' 'key2' | tr "\n" " "` +res="$?" if [ "$v" = 'key1=1 key2=2 ' ]; then echo 'ok 8'; else echo "not ok 8 v is '$v'"; fi - -v=`$CONFGET -f "$T2" -s '' key1` +v=`$CONFGET '-f' "$TESTDIR/t2.ini" '-s' '' 'key1' ` +res="$?" if [ "$v" = '1' ]; then echo 'ok 9'; else echo "not ok 9 v is '$v'"; fi -v=`$CONFGET -f "$T2" -s '' key1 key2 | tr "\n" ' '` +v=`$CONFGET '-f' "$TESTDIR/t2.ini" '-s' '' 'key1' 'key2' | tr "\n" " "` +res="$?" if [ "$v" = 'key1=1 key2=2 ' ]; then echo 'ok 10'; else echo "not ok 10 v is '$v'"; fi diff --git a/t/12-last-value.t b/t/12-last-value.t index a4623bb..baa8122 100644 --- a/t/12-last-value.t +++ b/t/12-last-value.t @@ -1,6 +1,6 @@ #!/bin/sh # -# Copyright (c) 2008, 2016 Peter Pentchev +# Copyright (c) 2008, 2011, 2019 Peter Pentchev # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -27,45 +27,52 @@ [ -z "$CONFGET" ] && CONFGET='./confget' [ -z "$TESTDIR" ] && TESTDIR='t' -T3="$TESTDIR/t3.ini" - echo '1..14' -if [ ! -f "$T3" ]; then - echo "Bail out! No test file $T3" - exit 255 -fi -v=`$CONFGET -f "$T3" both` +if [ ! -f "$TESTDIR/t3.ini" ]; then + echo "Bail out! No test file $TESTDIR/t3.ini" + exit 255 +fi +v=`$CONFGET '-f' "$TESTDIR/t3.ini" 'both' ` +res="$?" if [ "$v" = 'default' ]; then echo 'ok 1'; else echo "not ok 1 v is '$v'"; fi -v=`$CONFGET -f "$T3" defonly` +v=`$CONFGET '-f' "$TESTDIR/t3.ini" 'defonly' ` +res="$?" if [ "$v" = 'default' ]; then echo 'ok 2'; else echo "not ok 2 v is '$v'"; fi -v=`$CONFGET -f "$T3" aonly` +v=`$CONFGET '-f' "$TESTDIR/t3.ini" 'aonly' ` +res="$?" if [ "$v" = '' ]; then echo 'ok 3'; else echo "not ok 3 v is '$v'"; fi - -v=`$CONFGET -f "$T3" -s '' both` +v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' '' 'both' ` +res="$?" if [ "$v" = 'default' ]; then echo 'ok 4'; else echo "not ok 4 v is '$v'"; fi -v=`$CONFGET -f "$T3" -s '' defonly` +v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' '' 'defonly' ` +res="$?" if [ "$v" = 'default' ]; then echo 'ok 5'; else echo "not ok 5 v is '$v'"; fi -v=`$CONFGET -f "$T3" -s '' aonly` +v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' '' 'aonly' ` +res="$?" if [ "$v" = '' ]; then echo 'ok 6'; else echo "not ok 6 v is '$v'"; fi - -v=`$CONFGET -f "$T3" -s 'a' both` +v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' 'a' 'both' ` +res="$?" if [ "$v" = 'a' ]; then echo 'ok 7'; else echo "not ok 7 v is '$v'"; fi -v=`$CONFGET -f "$T3" -s 'a' defonly` +v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' 'a' 'defonly' ` +res="$?" if [ "$v" = '' ]; then echo 'ok 8'; else echo "not ok 8 v is '$v'"; fi -v=`$CONFGET -f "$T3" -s 'a' aonly` +v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' 'a' 'aonly' ` +res="$?" if [ "$v" = 'a' ]; then echo 'ok 9'; else echo "not ok 9 v is '$v'"; fi - -v=`$CONFGET -f "$T3" -O -s 'a' both` +v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' 'a' '-O' 'both' ` +res="$?" if [ "$v" = 'a' ]; then echo 'ok 10'; else echo "not ok 10 v is '$v'"; fi -v=`$CONFGET -f "$T3" -O -s 'a' defonly` +v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' 'a' '-O' 'defonly' ` +res="$?" if [ "$v" = 'default' ]; then echo 'ok 11'; else echo "not ok 11 v is '$v'"; fi -v=`$CONFGET -f "$T3" -O -s 'a' aonly` +v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-s' 'a' '-O' 'aonly' ` +res="$?" if [ "$v" = 'a' ]; then echo 'ok 12'; else echo "not ok 12 v is '$v'"; fi - -# A non-matching value should override a matching one and nothing should be displayed. -v=`$CONFGET -f "$T3" -O -s 'a' -m 'def*' both` +v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-m' 'def*' '-s' 'a' '-O' 'both' ` +res="$?" if [ "$v" = '' ]; then echo 'ok 13'; else echo "not ok 13 v is '$v'"; fi -v=`$CONFGET -f "$T3" -O -s 'a' -m 'a*' both` +v=`$CONFGET '-f' "$TESTDIR/t3.ini" '-m' 'a*' '-s' 'a' '-O' 'both' ` +res="$?" if [ "$v" = 'a' ]; then echo 'ok 14'; else echo "not ok 14 v is '$v'"; fi diff --git a/t/13-features.t b/t/13-bespoke-features.t index 4ed1088..4ed1088 100644 --- a/t/13-features.t +++ b/t/13-bespoke-features.t diff --git a/t/14-too-many.t b/t/14-bespoke-too-many.t index 5949fe3..5c1be63 100644 --- a/t/14-too-many.t +++ b/t/14-bespoke-too-many.t @@ -56,6 +56,13 @@ for args in \ '-q sections key1' \ '-q features -q feature BASE' \ '-q features key1'; do + if [ "${args#-q sections -q}" != "$args" ] || [ "${args#-q features -q}" != "$args" ]; then + if [ "${CONFGET#*python}" != "$CONFGET" ]; then + echo "ok $idx $args - skipped, Python argparse" + idx="$((idx + 1))" + continue + fi + fi v=`$CONFGET -f "$T2" $args 2>&1` if expr "x$v" : 'x.*Only a single ' > /dev/null; then echo "ok $idx $args"; else echo "not ok $idx args $args v is $v"; fi idx="$((idx + 1))" diff --git a/t/defs/schema/test-1.0.json b/t/defs/schema/test-1.0.json new file mode 100644 index 0000000..065fe1f --- /dev/null +++ b/t/defs/schema/test-1.0.json @@ -0,0 +1,176 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "https://devel.ringlet.net/textproc/confget/schema/test-1.0.json", + "title": "confget test definition", + "description": "A definition of a test file for the confget test suite", + "definitions": { + "format": { + "type": "object", + "properties": { + "version": { + "type": "object", + "properties": { + "major": { + "type": "integer", + "const": 1 + }, + "minor": { + "type": "integer", + "const": 0 + } + }, + "required": ["major", "minor"], + "additionalProperties": false + } + }, + "required": ["version"], + "additionalProperties": false + }, + + "setenv": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + + "test_def": { + "type": "object", + "properties": { + "args": { + "$ref": "#/definitions/test_def_args" + }, + "keys": { + "type": "array", + "items": { + "type": "string" + } + }, + "xform": { + "type": "string", + "enum": [ + "", + "count-lines", + "count-lines-eq", + "count-lines-non-eq", + "newline-to-space" + ] + }, + "backend": { + "type": "string", + "enum": ["http_get", "ini"] + }, + "setenv": { + "type": "boolean" + }, + "stdin": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "string", + "pattern": "^[0-9a-z_-]+\\.ini$" + } + ] + }, + "output": { + "oneOf": [ + { + "$ref": "#/definitions/output_exact" + }, + { + "$ref": "#/definitions/output_exit_ok" + } + ] + } + }, + "required": ["args", "keys", "output"], + "additionalProperties": false + }, + + "test_def_args": { + "type": "object", + "properties": { + "check_only": { + "type": "string", + "maxLength": 0 + }, + "filename": { + "type": "string" + }, + "hide_var_name": { + "type": "string", + "maxLength": 0 + }, + "list_all": { + "type": "string", + "maxLength": 0 + }, + "match_var_names": { + "type": "string", + "maxLength": 0 + }, + "match_var_values": { + "type": "string" + }, + "section": { + "type": "string" + }, + "section_override": { + "type": "string", + "maxLength": 0 + }, + "section_specified": { + "type": "string", + "maxLength": 0 + }, + "show_var_name": { + "type": "string", + "maxLength": 0 + } + }, + "additionalProperties": false + }, + + "output_exact": { + "type": "object", + "properties": { + "exact": { + "type": "string" + } + }, + "required": ["exact"], + "additionalProperties": false + }, + + "output_exit_ok": { + "type": "object", + "properties": { + "exit": { + "type": "boolean" + } + }, + "required": ["exit"], + "additionalProperties": false + } + }, + + "type": "object", + "properties": { + "format": { + "$ref": "#/definitions/format" + }, + "setenv": { + "$ref": "#/definitions/setenv" + }, + "tests": { + "type": "array", + "items": { + "$ref": "#/definitions/test_def" + } + } + }, + "required": ["format", "tests"], + "additionalProperties": false +} diff --git a/t/defs/tests/01-get-values.json b/t/defs/tests/01-get-values.json new file mode 100644 index 0000000..eb89340 --- /dev/null +++ b/t/defs/tests/01-get-values.json @@ -0,0 +1,293 @@ +{ + "format": { + "version": { + "major": 1, + "minor": 0 + } + }, + "setenv": { + "QUERY_STRING": "key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3", + "Q1": "key4&key5=%09%09%20val%27ue5&key6", + "Q2": "" + }, + "tests": [ + { + "args": { + "filename": "t1.ini", + "section": "a" + }, + "keys": [ + "key1" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "value1" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "a", + "show_var_name": "" + }, + "keys": [ + "key2" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "key2=value2" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "a" + }, + "keys": [ + "key3" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "\t\t val'ue3" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "b sect" + }, + "keys": [ + "key4" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "v'alu'e4" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "c" + }, + "keys": [ + "key5" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "\t\t# value5" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "a" + }, + "keys": [ + "key1", + "key2" + ], + "xform": "newline-to-space", + "backend": "ini", + "output": { + "exact": "key1=value1 key2=value2 " + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "a", + "hide_var_name": "" + }, + "keys": [ + "key6", + "key2" + ], + "xform": "newline-to-space", + "backend": "ini", + "output": { + "exact": "value2 value6 " + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "b sect" + }, + "keys": [ + "key7" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "value7" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "section": "b sect" + }, + "keys": [ + "key7" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "value7" + }, + "setenv": false, + "stdin": "t1.ini" + }, + { + "args": { + "section": "b sect" + }, + "keys": [ + "key77" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "" + }, + "setenv": false, + "stdin": "t1.ini" + }, + { + "args": {}, + "keys": [ + "key1" + ], + "xform": "", + "backend": "http_get", + "output": { + "exact": "value1" + }, + "setenv": true, + "stdin": null + }, + { + "args": { + "show_var_name": "" + }, + "keys": [ + "key2" + ], + "xform": "", + "backend": "http_get", + "output": { + "exact": "key2==value2&" + }, + "setenv": true, + "stdin": null + }, + { + "args": {}, + "keys": [ + "key3" + ], + "xform": "", + "backend": "http_get", + "output": { + "exact": "\t\t val'ue3" + }, + "setenv": true, + "stdin": null + }, + { + "args": { + "section": "Q1" + }, + "keys": [ + "key4" + ], + "xform": "", + "backend": "http_get", + "output": { + "exact": "" + }, + "setenv": true, + "stdin": null + }, + { + "args": { + "section": "Q1" + }, + "keys": [ + "key5" + ], + "xform": "", + "backend": "http_get", + "output": { + "exact": "\t\t val'ue5" + }, + "setenv": true, + "stdin": null + }, + { + "args": { + "section": "Q1" + }, + "keys": [ + "key6" + ], + "xform": "", + "backend": "http_get", + "output": { + "exact": "" + }, + "setenv": true, + "stdin": null + }, + { + "args": { + "section": "Q1" + }, + "keys": [ + "key66" + ], + "xform": "", + "backend": "http_get", + "output": { + "exact": "" + }, + "setenv": true, + "stdin": null + }, + { + "args": { + "section": "Q2" + }, + "keys": [ + "key66" + ], + "xform": "", + "backend": "http_get", + "output": { + "exact": "" + }, + "setenv": true, + "stdin": null + } + ] +} diff --git a/t/defs/tests/02-check-values.json b/t/defs/tests/02-check-values.json new file mode 100644 index 0000000..1772d80 --- /dev/null +++ b/t/defs/tests/02-check-values.json @@ -0,0 +1,258 @@ +{ + "format": { + "version": { + "major": 1, + "minor": 0 + } + }, + "setenv": { + "QUERY_STRING": "key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3", + "Q1": "key4&key5=%09%09%20val%27ue5&key6", + "Q2": "" + }, + "tests": [ + { + "args": { + "check_only": "", + "filename": "t1.ini", + "section": "a" + }, + "keys": [ + "key1" + ], + "xform": "", + "backend": "ini", + "output": { + "exit": true + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "check_only": "", + "filename": "t1.ini", + "section": "a" + }, + "keys": [ + "key2" + ], + "xform": "", + "backend": "ini", + "output": { + "exit": true + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "check_only": "", + "filename": "t1.ini", + "section": "a" + }, + "keys": [ + "key3" + ], + "xform": "", + "backend": "ini", + "output": { + "exit": true + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "check_only": "", + "filename": "t1.ini", + "section": "a" + }, + "keys": [ + "key4" + ], + "xform": "", + "backend": "ini", + "output": { + "exit": false + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "check_only": "", + "filename": "t1.ini", + "section": "b sect" + }, + "keys": [ + "key5" + ], + "xform": "", + "backend": "ini", + "output": { + "exit": false + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "check_only": "", + "filename": "t1.ini", + "section": "b sect" + }, + "keys": [ + "key4" + ], + "xform": "", + "backend": "ini", + "output": { + "exit": true + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "check_only": "", + "filename": "t1.ini", + "section": "c" + }, + "keys": [ + "key5" + ], + "xform": "", + "backend": "ini", + "output": { + "exit": true + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "check_only": "" + }, + "keys": [ + "key1" + ], + "xform": "", + "backend": "http_get", + "output": { + "exit": true + }, + "setenv": true, + "stdin": null + }, + { + "args": { + "check_only": "" + }, + "keys": [ + "key2" + ], + "xform": "", + "backend": "http_get", + "output": { + "exit": true + }, + "setenv": true, + "stdin": null + }, + { + "args": { + "check_only": "" + }, + "keys": [ + "key3" + ], + "xform": "", + "backend": "http_get", + "output": { + "exit": true + }, + "setenv": true, + "stdin": null + }, + { + "args": { + "check_only": "", + "section": "Q1" + }, + "keys": [ + "key4" + ], + "xform": "", + "backend": "http_get", + "output": { + "exit": true + }, + "setenv": true, + "stdin": null + }, + { + "args": { + "check_only": "", + "section": "Q1" + }, + "keys": [ + "key6" + ], + "xform": "", + "backend": "http_get", + "output": { + "exit": true + }, + "setenv": true, + "stdin": null + }, + { + "args": { + "check_only": "" + }, + "keys": [ + "key6" + ], + "xform": "", + "backend": "http_get", + "output": { + "exit": false + }, + "setenv": true, + "stdin": null + }, + { + "args": { + "check_only": "", + "section": "Q1" + }, + "keys": [ + "key1" + ], + "xform": "", + "backend": "http_get", + "output": { + "exit": false + }, + "setenv": true, + "stdin": null + }, + { + "args": { + "check_only": "", + "section": "Q2" + }, + "keys": [ + "key1" + ], + "xform": "", + "backend": "http_get", + "output": { + "exit": false + }, + "setenv": true, + "stdin": null + } + ] +} diff --git a/t/defs/tests/03-list-all.json b/t/defs/tests/03-list-all.json new file mode 100644 index 0000000..b85b867 --- /dev/null +++ b/t/defs/tests/03-list-all.json @@ -0,0 +1,118 @@ +{ + "format": { + "version": { + "major": 1, + "minor": 0 + } + }, + "setenv": { + "QUERY_STRING": "key1=value1&key2=%3Dvalue2%26&key3=%09%09%20val%27ue3", + "Q1": "key4&key5=%09%09%20val%27ue5&key6", + "Q2": "" + }, + "tests": [ + { + "args": { + "filename": "t1.ini", + "section": "a", + "list_all": "" + }, + "keys": [], + "xform": "count-lines", + "backend": "ini", + "output": { + "exact": "4" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "b sect", + "list_all": "", + "show_var_name": "" + }, + "keys": [], + "xform": "count-lines-eq", + "backend": "ini", + "output": { + "exact": "2" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "c", + "list_all": "", + "hide_var_name": "" + }, + "keys": [], + "xform": "count-lines-non-eq", + "backend": "ini", + "output": { + "exact": "1" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "d", + "list_all": "" + }, + "keys": [], + "xform": "count-lines", + "backend": "ini", + "output": { + "exact": "0" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "list_all": "" + }, + "keys": [], + "xform": "count-lines", + "backend": "http_get", + "output": { + "exact": "3" + }, + "setenv": true, + "stdin": null + }, + { + "args": { + "list_all": "", + "section": "Q1" + }, + "keys": [], + "xform": "count-lines", + "backend": "http_get", + "output": { + "exact": "3" + }, + "setenv": true, + "stdin": null + }, + { + "args": { + "list_all": "", + "section": "Q2" + }, + "keys": [], + "xform": "count-lines", + "backend": "http_get", + "output": { + "exact": "0" + }, + "setenv": true, + "stdin": null + } + ] +} diff --git a/t/defs/tests/05-match-names.json b/t/defs/tests/05-match-names.json new file mode 100644 index 0000000..7dd76c2 --- /dev/null +++ b/t/defs/tests/05-match-names.json @@ -0,0 +1,81 @@ +{ + "format": { + "version": { + "major": 1, + "minor": 0 + } + }, + "setenv": {}, + "tests": [ + { + "args": { + "filename": "t1.ini", + "section": "a", + "match_var_names": "" + }, + "keys": [ + "*" + ], + "xform": "count-lines", + "backend": "ini", + "output": { + "exact": "4" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "b sect", + "match_var_names": "", + "show_var_name": "" + }, + "keys": [ + "*" + ], + "xform": "count-lines-eq", + "backend": "ini", + "output": { + "exact": "2" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "a", + "match_var_names": "", + "hide_var_name": "" + }, + "keys": [ + "*ey2" + ], + "xform": "count-lines-non-eq", + "backend": "ini", + "output": { + "exact": "1" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "d", + "match_var_names": "" + }, + "keys": [ + "*ey2" + ], + "xform": "count-lines", + "backend": "ini", + "output": { + "exact": "0" + }, + "setenv": false, + "stdin": null + } + ] +} diff --git a/t/defs/tests/06-get-default.json b/t/defs/tests/06-get-default.json new file mode 100644 index 0000000..d20f812 --- /dev/null +++ b/t/defs/tests/06-get-default.json @@ -0,0 +1,87 @@ +{ + "format": { + "version": { + "major": 1, + "minor": 0 + } + }, + "setenv": {}, + "tests": [ + { + "args": { + "filename": "t1.ini" + }, + "keys": [ + "key1" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "value1" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini" + }, + "keys": [ + "key4" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini" + }, + "keys": [ + "key6" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "value6" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t2.ini" + }, + "keys": [ + "key1" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "1" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t2.ini" + }, + "keys": [ + "key1", + "key2" + ], + "xform": "newline-to-space", + "backend": "ini", + "output": { + "exact": "key1=1 key2=2 " + }, + "setenv": false, + "stdin": null + } + ] +} diff --git a/t/defs/tests/07-match-values.json b/t/defs/tests/07-match-values.json new file mode 100644 index 0000000..96c6e2a --- /dev/null +++ b/t/defs/tests/07-match-values.json @@ -0,0 +1,166 @@ +{ + "format": { + "version": { + "major": 1, + "minor": 0 + } + }, + "setenv": {}, + "tests": [ + { + "args": { + "filename": "t1.ini", + "section": "a", + "match_var_values": "value*" + }, + "keys": [ + "key1" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "value1" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "a", + "match_var_values": "*ue2" + }, + "keys": [ + "key2" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "value2" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "a", + "match_var_values": "*val'ue*" + }, + "keys": [ + "key3" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "\t\t val'ue3" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "b sect", + "match_var_values": "*alu*" + }, + "keys": [ + "key4" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "v'alu'e4" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "c" + }, + "keys": [ + "key5" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "\t\t# value5" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "a", + "match_var_values": "*alu*" + }, + "keys": [ + "key1", + "key2" + ], + "xform": "newline-to-space", + "backend": "ini", + "output": { + "exact": "key1=value1 key2=value2 " + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "a", + "match_var_values": "*7" + }, + "keys": [ + "key6" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "b sect", + "check_only": "", + "match_var_values": "*7" + }, + "keys": [ + "key7" + ], + "xform": "", + "backend": "ini", + "output": { + "exit": true + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "b sect", + "check_only": "", + "match_var_values": "*7" + }, + "keys": [ + "key6" + ], + "xform": "", + "backend": "ini", + "output": { + "exit": false + }, + "setenv": false, + "stdin": null + } + ] +} diff --git a/t/defs/tests/11-no-default.json b/t/defs/tests/11-no-default.json new file mode 100644 index 0000000..492043f --- /dev/null +++ b/t/defs/tests/11-no-default.json @@ -0,0 +1,173 @@ +{ + "format": { + "version": { + "major": 1, + "minor": 0 + } + }, + "setenv": {}, + "tests": [ + { + "args": { + "filename": "t1.ini" + }, + "keys": [ + "key1" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "value1" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini" + }, + "keys": [ + "key4" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini" + }, + "keys": [ + "key6" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "value6" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "", + "section_specified": "" + }, + "keys": [ + "key1" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "", + "section_specified": "" + }, + "keys": [ + "key4" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t1.ini", + "section": "", + "section_specified": "" + }, + "keys": [ + "key6" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t2.ini" + }, + "keys": [ + "key1" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "1" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t2.ini" + }, + "keys": [ + "key1", + "key2" + ], + "xform": "newline-to-space", + "backend": "ini", + "output": { + "exact": "key1=1 key2=2 " + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t2.ini", + "section": "", + "section_specified": "" + }, + "keys": [ + "key1" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "1" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t2.ini", + "section": "", + "section_specified": "" + }, + "keys": [ + "key1", + "key2" + ], + "xform": "newline-to-space", + "backend": "ini", + "output": { + "exact": "key1=1 key2=2 " + }, + "setenv": false, + "stdin": null + } + ] +} diff --git a/t/defs/tests/12-last-value.json b/t/defs/tests/12-last-value.json new file mode 100644 index 0000000..3005780 --- /dev/null +++ b/t/defs/tests/12-last-value.json @@ -0,0 +1,239 @@ +{ + "format": { + "version": { + "major": 1, + "minor": 0 + } + }, + "setenv": {}, + "tests": [ + { + "args": { + "filename": "t3.ini" + }, + "keys": [ + "both" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "default" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t3.ini" + }, + "keys": [ + "defonly" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "default" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t3.ini" + }, + "keys": [ + "aonly" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t3.ini", + "section": "" + }, + "keys": [ + "both" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "default" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t3.ini", + "section": "" + }, + "keys": [ + "defonly" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "default" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t3.ini", + "section": "" + }, + "keys": [ + "aonly" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t3.ini", + "section": "a" + }, + "keys": [ + "both" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "a" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t3.ini", + "section": "a" + }, + "keys": [ + "defonly" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t3.ini", + "section": "a" + }, + "keys": [ + "aonly" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "a" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t3.ini", + "section": "a", + "section_override": "" + }, + "keys": [ + "both" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "a" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t3.ini", + "section": "a", + "section_override": "" + }, + "keys": [ + "defonly" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "default" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t3.ini", + "section": "a", + "section_override": "" + }, + "keys": [ + "aonly" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "a" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t3.ini", + "match_var_values": "def*", + "section": "a", + "section_override": "" + }, + "keys": [ + "both" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "" + }, + "setenv": false, + "stdin": null + }, + { + "args": { + "filename": "t3.ini", + "match_var_values": "a*", + "section": "a", + "section_override": "" + }, + "keys": [ + "both" + ], + "xform": "", + "backend": "ini", + "output": { + "exact": "a" + }, + "setenv": false, + "stdin": null + } + ] +} diff --git a/t/defs/tools/encode.py b/t/defs/tools/encode.py new file mode 100644 index 0000000..32e961a --- /dev/null +++ b/t/defs/tools/encode.py @@ -0,0 +1,105 @@ +#!/usr/bin/python3 +# +# Copyright (c) 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Encode a Python confget test data structure into a JSON file. +""" + +import json + +from typing import Any, Dict + +from unit_tests.data import data as t_data +from unit_tests.data import defs as t_defs + + +class TestEncoder(json.JSONEncoder): + """ Encode the confget test data into serializable objects. """ + + def encode_test_file_def(self, obj: t_defs.TestFileDef) -> Dict[str, Any]: + """ Encode a full TestFileDef object. """ + return { + 'format': { + 'version': { + 'major': 1, + 'minor': 0, + }, + }, + 'setenv': obj.setenv, + 'tests': [self.default(test) for test in obj.tests], + } + + def encode_test_def(self, obj: t_defs.TestDef) -> Dict[str, Any]: + """ Encode a single test definition. """ + return { + 'args': obj.args, + 'keys': obj.keys, + 'xform': obj.xform, + 'backend': obj.backend, + 'output': self.default(obj.output), + 'setenv': obj.setenv, + 'stdin': obj.stdin, + } + + def encode_exact_output_def(self, obj: t_defs.TestExactOutputDef + ) -> Dict[str, str]: + """ Encode an exact output requirement. """ + return { + 'exact': obj.exact, + } + + def encode_exit_ok_output_def(self, obj: t_defs.TestExitOKOutputDef + ) -> Dict[str, bool]: + """ Encode an exit code requirement. """ + return { + 'exit': obj.success, + } + + SERIALIZERS = { + t_defs.TestFileDef: encode_test_file_def, + t_defs.TestDef: encode_test_def, + t_defs.TestExactOutputDef: encode_exact_output_def, + t_defs.TestExitOKOutputDef: encode_exit_ok_output_def, + } + + def default(self, obj: Any) -> Any: + """ Meow. """ + method = self.SERIALIZERS.get(type(obj), None) + if method is not None: + return method(self, obj) + return super(TestEncoder, self).default(obj) + + +def main() -> None: + """ Main function: encode, output. """ + for name, tdef in sorted(t_data.TESTS.items()): + print(f'--- {name} ---') + with open(f'output/{name}.json', mode='w') as outf: + print(json.dumps(tdef, indent=2, cls=TestEncoder), file=outf) + + +if __name__ == '__main__': + main() diff --git a/t/defs/tools/generate.py b/t/defs/tools/generate.py new file mode 100755 index 0000000..45dd99a --- /dev/null +++ b/t/defs/tools/generate.py @@ -0,0 +1,146 @@ +#!/usr/bin/python3 +# +# Copyright (c) 2018, 2019 Peter Pentchev <roam@ringlet.net> +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +""" +Generate shell scripts with TAP output for the confget tests. +""" + +from typing import List + +from unit_tests import data as t_data + + +PRELUDE = r'''#!/bin/sh +# +# Copyright (c) 2008, 2011, 2019 Peter Pentchev +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. 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. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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. + +[ -z "$CONFGET" ] && CONFGET='./confget' +[ -z "$TESTDIR" ] && TESTDIR='t' + +echo '1..{count}' +''' + +CHECK_TESTFILE = r''' +if [ ! -f "$TESTDIR/{fname}" ]; then + echo "Bail out! No test file $TESTDIR/{fname}" + exit 255 +fi''' + + +def add_cmdline_options(cmdline: List[str], test: t_data.defs.TestDef + ) -> None: + """ Add the options from test.args into cmdline. """ + for name, value in test.args.items(): + option = t_data.CMDLINE_OPTIONS[name] + if option[0] == '': + continue + cmdline.append(f"'{option[0]}'") + if option[1]: + if name == 'filename': + cmdline.append(f'"$TESTDIR/{value}"') + else: + cmdline.append(t_data.shell_escape(value)) + + +def main() -> None: + """ Main function: generate the test files. """ + tests = t_data.load_all_tests('.') + for fname, data in sorted(tests.items()): + print(f'Generating {fname}.t with {len(data.tests)} tests') + with open(f'{fname}.t', mode='w') as testf: + print(PRELUDE.format(count=len(data.tests)), file=testf) + + filenames = sorted({ + test.args['filename'] for test in data.tests + if 'filename' in test.args + }) + for filename in filenames: + print(CHECK_TESTFILE.format(fname=filename), file=testf) + + for idx, test in enumerate(data.tests): + tap_idx = idx + 1 + + cmdline: List[str] = [] + if test.setenv: + cmdline.append('env') + for name, value in sorted(data.setenv.items()): + cmdline.append(f'{name}={t_data.shell_escape(value)}') + + cmdline.append('$CONFGET') + if test.backend != 'ini': + cmdline.extend(['-t', test.backend]) + + add_cmdline_options(cmdline, test) + + if test.stdin is not None: + cmdline.extend([ + t_data.CMDLINE_OPTIONS['filename'][0], + '-' + ]) + + cmdline.extend([f"'{key}'" for key in test.keys]) + + if test.stdin is not None: + cmdline.extend(['<', f'"$TESTDIR/{test.stdin}"']) + + xform = t_data.XFORM[test.xform].command + print(f'- {tap_idx}: {" ".join(cmdline)} {xform}') + + print(f'v=`{" ".join(cmdline)} {xform}`', file=testf) + print('res="$?"', file=testf) + + check = test.output.get_check() + print(f' - {check}') + print(f"if {check}; then echo 'ok {tap_idx}'; else " + f'echo "not ok {tap_idx} {test.output.var_name} is ' + f"'${test.output.var_name}'\"; fi", + file=testf) + + +if __name__ == '__main__': + main() |