2 """Generate variable test cases"""
5 # Copyright 2024 Odin Kroeger.
7 # This file is part of Para.
9 # Para is free softwareyou can redistribute it and/or modify it under
10 # the terms of the GNU General Public License as published by the Free
11 # Software Foundation, either version 3 of the License, or
12 # (at your option) any later version.
14 # Para is distributed in the hope that it will be useful, but WITHOUT
15 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
16 # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
17 # License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with Para. If not, see <https://www.gnu.org/licenses/>.
23 from collections.abc import Generator
24 from ctypes import (RTLD_LOCAL, CDLL, c_char_p, c_int)
25 from ctypes.util import find_library
26 from filecmp import cmp
27 from getopt import GetoptError, getopt
28 from itertools import chain, combinations, filterfalse, permutations
29 from locale import getpreferredencoding
30 from os import remove, rename, strerror
31 from os.path import basename, commonprefix, dirname, exists, isfile
32 from sys import argv, exit, stdout, version_info
33 from tempfile import NamedTemporaryFile
34 from typing import Callable, Iterable, Final, NoReturn, TypeVar
39 if version_info < (3, 9):
40 exit('mkvars requires Python 3.9 or later')
47 __author__ = 'Odin Kroeger'
48 __copyright__ = '2024 Odin Kroeger'
49 __license__ = 'GPLv3+'
63 CQUOTEMAP: Final[dict[str, str]] = {
70 """Mapping of characters that need quoting to escape sequences"""
72 # Para does not handle newlines like Bourne shells do.
73 # Characters repeat so that they can occur more than once.
74 METACHARS: Final[tuple[str, ...]] = ('=', '=')
75 """Word-splitting meta characters"""
77 VARRE: Final[re.Pattern] = re.compile('^[a-z_][0-9a-z_]*=', flags=re.I)
78 """Expression that matches valid name=value pairs"""
89 fname = find_library('c')
91 raise Error('Could not find LibC')
93 super().__init__(fname, mode=RTLD_LOCAL)
94 self.putenv.argtypes = (c_char_p,)
95 self.putenv.restype = c_int
102 class Error(Exception):
103 """Class for errors."""
110 def cquote(string: str) -> str:
111 """Quotes a string so it can be used in a C source file"""
112 return '"' + ''.join(CQUOTEMAP.get(c, c) for c in string) + '"'
115 def error(*args, status=1, **kwargs) -> NoReturn:
116 """Log an err and :func:`exit <sys.exit>` with `status`.
119 argsPositional arguments for :func:`logging.error`.
121 kwargsKeyword arguments for :func:`logging.error`.
123 logging.error(*args, **kwargs)
127 def unique(iterable: Iterable[T]) -> Generator[T]:
128 """Return every element from `iterable`, but only once."""
130 for elem in filterfalse(seen.__contains__, iterable):
135 def powerset(iterable: Iterable[T]) -> Generator[tuple[T, ...]]:
136 """Computes the powerset of `iterable`"""
137 items = tuple(iterable)
138 combs = (combinations(items, r) for r in range(len(items) + 1))
139 yield from chain.from_iterable(combs)
142 def showhelp(func: Callable):
143 """Print the docstring of `func` and :func:`exit <sys.exit>`."""
145 lines = func.__doc__.splitlines()
146 prefix = commonprefix([line for line in lines[1:-1] if line])
148 for i, line in enumerate(lines):
149 line = line.removeprefix(prefix)
150 if not (i == nlines - 1 and not line):
160 """mkvars - Create X macros for testing putvar
162 Usage: mkvars [word ...]
165 -o file Save output to file.
167 progname = basename(argv[0])
168 logging.basicConfig(format=f'{progname}: %(message)s')
172 opts, args = getopt(argv[1:], 'o:h')
173 except GetoptError as err:
176 for opt, optarg in opts:
182 encoding = getpreferredencoding()
185 tokens = ((tuple(args) if args else
186 ('foo', 'foo', '_', '.', '/', '0')) + METACHARS)
190 if exists(output) and not isfile(output):
191 file = open(output, 'w', encoding=encoding)
193 tmp = NamedTemporaryFile('w+', delete=False, encoding=encoding,
195 prefix='.', suffix='.tmp')
196 file = tmp # type: ignore
198 file = stdout # type: ignore
201 print('/* Computed putvar test cases */', file=file)
202 for sub in unique(powerset(tokens)):
203 for perm in unique(permutations(sub)):
206 if VARRE.match(var) and libc.putenv(var.encode(encoding)) == 0:
207 print(f'X({cvar})', file=file)
209 print(f'Y({cvar})', file=file)
215 if tmp and not (exists(output) and cmp(file.name, output)):
216 rename(tmp.name, output)
217 except FileNotFoundError as err:
218 error(f'{err.filename}: {strerror(err.errno)}')
219 except OSError as err:
220 error(strerror(err.errno))
227 if __name__ == '__main__':