blob: 5530f7abba26a5b11f53b7dc7ca3d1cf7e7a8463 [file] [log] [blame]
Gilles Peskinee7c44552021-01-25 21:40:45 +01001"""Collect macro definitions from header files.
2"""
3
4# Copyright The Mbed TLS Contributors
5# SPDX-License-Identifier: Apache-2.0
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License.
9# You may obtain a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16# See the License for the specific language governing permissions and
17# limitations under the License.
18
Gilles Peskine22fcf1b2021-03-10 01:02:39 +010019import itertools
Gilles Peskinee7c44552021-01-25 21:40:45 +010020import re
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +020021from typing import Dict, Iterable, Iterator, List, Optional, Pattern, Set, Tuple, Union
22
23
24class ReadFileLineException(Exception):
25 def __init__(self, filename: str, line_number: Union[int, str]) -> None:
26 message = 'in {} at {}'.format(filename, line_number)
27 super(ReadFileLineException, self).__init__(message)
28 self.filename = filename
29 self.line_number = line_number
30
31
32class read_file_lines:
33 # Dear Pylint, conventionally, a context manager class name is lowercase.
34 # pylint: disable=invalid-name,too-few-public-methods
35 """Context manager to read a text file line by line.
36
37 ```
38 with read_file_lines(filename) as lines:
39 for line in lines:
40 process(line)
41 ```
42 is equivalent to
43 ```
44 with open(filename, 'r') as input_file:
45 for line in input_file:
46 process(line)
47 ```
48 except that if process(line) raises an exception, then the read_file_lines
49 snippet annotates the exception with the file name and line number.
50 """
51 def __init__(self, filename: str, binary: bool = False) -> None:
52 self.filename = filename
53 self.line_number = 'entry' #type: Union[int, str]
54 self.generator = None #type: Optional[Iterable[Tuple[int, str]]]
55 self.binary = binary
56 def __enter__(self) -> 'read_file_lines':
57 self.generator = enumerate(open(self.filename,
58 'rb' if self.binary else 'r'))
59 return self
60 def __iter__(self) -> Iterator[str]:
61 assert self.generator is not None
62 for line_number, content in self.generator:
63 self.line_number = line_number
64 yield content
65 self.line_number = 'exit'
66 def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
67 if exc_type is not None:
68 raise ReadFileLineException(self.filename, self.line_number) \
69 from exc_value
Gilles Peskine22fcf1b2021-03-10 01:02:39 +010070
71
72class PSAMacroEnumerator:
73 """Information about constructors of various PSA Crypto types.
74
75 This includes macro names as well as information about their arguments
76 when applicable.
77
78 This class only provides ways to enumerate expressions that evaluate to
79 values of the covered types. Derived classes are expected to populate
80 the set of known constructors of each kind, as well as populate
81 `self.arguments_for` for arguments that are not of a kind that is
82 enumerated here.
83 """
Gilles Peskine4c7da692021-04-21 21:39:27 +020084 #pylint: disable=too-many-instance-attributes
Gilles Peskine22fcf1b2021-03-10 01:02:39 +010085
86 def __init__(self) -> None:
87 """Set up an empty set of known constructor macros.
88 """
89 self.statuses = set() #type: Set[str]
Gilles Peskine4c7da692021-04-21 21:39:27 +020090 self.lifetimes = set() #type: Set[str]
91 self.locations = set() #type: Set[str]
92 self.persistence_levels = set() #type: Set[str]
Gilles Peskine22fcf1b2021-03-10 01:02:39 +010093 self.algorithms = set() #type: Set[str]
94 self.ecc_curves = set() #type: Set[str]
95 self.dh_groups = set() #type: Set[str]
96 self.key_types = set() #type: Set[str]
97 self.key_usage_flags = set() #type: Set[str]
98 self.hash_algorithms = set() #type: Set[str]
99 self.mac_algorithms = set() #type: Set[str]
100 self.ka_algorithms = set() #type: Set[str]
101 self.kdf_algorithms = set() #type: Set[str]
102 self.aead_algorithms = set() #type: Set[str]
103 # macro name -> list of argument names
104 self.argspecs = {} #type: Dict[str, List[str]]
105 # argument name -> list of values
106 self.arguments_for = {
107 'mac_length': [],
108 'min_mac_length': [],
109 'tag_length': [],
110 'min_tag_length': [],
111 } #type: Dict[str, List[str]]
Gilles Peskine46d3a372021-05-20 21:37:06 +0200112 # Whether to include intermediate macros in enumerations. Intermediate
113 # macros serve as category headers and are not valid values of their
114 # type. See `is_internal_name`.
115 # Always false in this class, may be set to true in derived classes.
Gilles Peskineb93f8542021-04-19 13:50:25 +0200116 self.include_intermediate = False
117
118 def is_internal_name(self, name: str) -> bool:
119 """Whether this is an internal macro. Internal macros will be skipped."""
120 if not self.include_intermediate:
121 if name.endswith('_BASE') or name.endswith('_NONE'):
122 return True
123 if '_CATEGORY_' in name:
124 return True
125 return name.endswith('_FLAG') or name.endswith('_MASK')
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100126
127 def gather_arguments(self) -> None:
128 """Populate the list of values for macro arguments.
129
130 Call this after parsing all the inputs.
131 """
132 self.arguments_for['hash_alg'] = sorted(self.hash_algorithms)
133 self.arguments_for['mac_alg'] = sorted(self.mac_algorithms)
134 self.arguments_for['ka_alg'] = sorted(self.ka_algorithms)
135 self.arguments_for['kdf_alg'] = sorted(self.kdf_algorithms)
136 self.arguments_for['aead_alg'] = sorted(self.aead_algorithms)
137 self.arguments_for['curve'] = sorted(self.ecc_curves)
138 self.arguments_for['group'] = sorted(self.dh_groups)
Gilles Peskine4c7da692021-04-21 21:39:27 +0200139 self.arguments_for['persistence'] = sorted(self.persistence_levels)
140 self.arguments_for['location'] = sorted(self.locations)
141 self.arguments_for['lifetime'] = sorted(self.lifetimes)
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100142
143 @staticmethod
144 def _format_arguments(name: str, arguments: Iterable[str]) -> str:
Gilles Peskine0a93c1b2021-04-21 15:36:58 +0200145 """Format a macro call with arguments.
146
147 The resulting format is consistent with
148 `InputsForTest.normalize_argument`.
149 """
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100150 return name + '(' + ', '.join(arguments) + ')'
151
152 _argument_split_re = re.compile(r' *, *')
153 @classmethod
154 def _argument_split(cls, arguments: str) -> List[str]:
155 return re.split(cls._argument_split_re, arguments)
156
157 def distribute_arguments(self, name: str) -> Iterator[str]:
158 """Generate macro calls with each tested argument set.
159
160 If name is a macro without arguments, just yield "name".
161 If name is a macro with arguments, yield a series of
162 "name(arg1,...,argN)" where each argument takes each possible
163 value at least once.
164 """
165 try:
166 if name not in self.argspecs:
167 yield name
168 return
169 argspec = self.argspecs[name]
170 if argspec == []:
171 yield name + '()'
172 return
173 argument_lists = [self.arguments_for[arg] for arg in argspec]
174 arguments = [values[0] for values in argument_lists]
175 yield self._format_arguments(name, arguments)
176 # Dear Pylint, enumerate won't work here since we're modifying
177 # the array.
178 # pylint: disable=consider-using-enumerate
179 for i in range(len(arguments)):
180 for value in argument_lists[i][1:]:
181 arguments[i] = value
182 yield self._format_arguments(name, arguments)
183 arguments[i] = argument_lists[0][0]
184 except BaseException as e:
185 raise Exception('distribute_arguments({})'.format(name)) from e
186
Gilles Peskine08966e62021-04-21 15:37:34 +0200187 def distribute_arguments_without_duplicates(
188 self, seen: Set[str], name: str
189 ) -> Iterator[str]:
190 """Same as `distribute_arguments`, but don't repeat seen results."""
191 for result in self.distribute_arguments(name):
192 if result not in seen:
193 seen.add(result)
194 yield result
195
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100196 def generate_expressions(self, names: Iterable[str]) -> Iterator[str]:
197 """Generate expressions covering values constructed from the given names.
198
199 `names` can be any iterable collection of macro names.
200
201 For example:
202 * ``generate_expressions(['PSA_ALG_CMAC', 'PSA_ALG_HMAC'])``
203 generates ``'PSA_ALG_CMAC'`` as well as ``'PSA_ALG_HMAC(h)'`` for
204 every known hash algorithm ``h``.
205 * ``macros.generate_expressions(macros.key_types)`` generates all
206 key types.
207 """
Gilles Peskine08966e62021-04-21 15:37:34 +0200208 seen = set() #type: Set[str]
209 return itertools.chain(*(
210 self.distribute_arguments_without_duplicates(seen, name)
211 for name in names
212 ))
Gilles Peskine22fcf1b2021-03-10 01:02:39 +0100213
Gilles Peskinee7c44552021-01-25 21:40:45 +0100214
Gilles Peskine33c601c2021-03-10 01:25:50 +0100215class PSAMacroCollector(PSAMacroEnumerator):
Gilles Peskinee7c44552021-01-25 21:40:45 +0100216 """Collect PSA crypto macro definitions from C header files.
217 """
218
Gilles Peskine10ab2672021-03-10 00:59:53 +0100219 def __init__(self, include_intermediate: bool = False) -> None:
Gilles Peskine13d60eb2021-01-25 22:42:14 +0100220 """Set up an object to collect PSA macro definitions.
221
222 Call the read_file method of the constructed object on each header file.
223
224 * include_intermediate: if true, include intermediate macros such as
225 PSA_XXX_BASE that do not designate semantic values.
226 """
Gilles Peskine33c601c2021-03-10 01:25:50 +0100227 super().__init__()
Gilles Peskine13d60eb2021-01-25 22:42:14 +0100228 self.include_intermediate = include_intermediate
Gilles Peskine10ab2672021-03-10 00:59:53 +0100229 self.key_types_from_curve = {} #type: Dict[str, str]
230 self.key_types_from_group = {} #type: Dict[str, str]
Gilles Peskine10ab2672021-03-10 00:59:53 +0100231 self.algorithms_from_hash = {} #type: Dict[str, str]
Gilles Peskinee7c44552021-01-25 21:40:45 +0100232
Gilles Peskine33c601c2021-03-10 01:25:50 +0100233 def record_algorithm_subtype(self, name: str, expansion: str) -> None:
234 """Record the subtype of an algorithm constructor.
235
236 Given a ``PSA_ALG_xxx`` macro name and its expansion, if the algorithm
237 is of a subtype that is tracked in its own set, add it to the relevant
238 set.
239 """
240 # This code is very ad hoc and fragile. It should be replaced by
241 # something more robust.
242 if re.match(r'MAC(?:_|\Z)', name):
243 self.mac_algorithms.add(name)
244 elif re.match(r'KDF(?:_|\Z)', name):
245 self.kdf_algorithms.add(name)
246 elif re.search(r'0x020000[0-9A-Fa-f]{2}', expansion):
247 self.hash_algorithms.add(name)
248 elif re.search(r'0x03[0-9A-Fa-f]{6}', expansion):
249 self.mac_algorithms.add(name)
250 elif re.search(r'0x05[0-9A-Fa-f]{6}', expansion):
251 self.aead_algorithms.add(name)
252 elif re.search(r'0x09[0-9A-Fa-f]{2}0000', expansion):
253 self.ka_algorithms.add(name)
254 elif re.search(r'0x08[0-9A-Fa-f]{6}', expansion):
255 self.kdf_algorithms.add(name)
256
Gilles Peskinee7c44552021-01-25 21:40:45 +0100257 # "#define" followed by a macro name with either no parameters
258 # or a single parameter and a non-empty expansion.
259 # Grab the macro name in group 1, the parameter name if any in group 2
260 # and the expansion in group 3.
261 _define_directive_re = re.compile(r'\s*#\s*define\s+(\w+)' +
262 r'(?:\s+|\((\w+)\)\s*)' +
263 r'(.+)')
264 _deprecated_definition_re = re.compile(r'\s*MBEDTLS_DEPRECATED')
265
266 def read_line(self, line):
267 """Parse a C header line and record the PSA identifier it defines if any.
268 This function analyzes lines that start with "#define PSA_"
269 (up to non-significant whitespace) and skips all non-matching lines.
270 """
271 # pylint: disable=too-many-branches
272 m = re.match(self._define_directive_re, line)
273 if not m:
274 return
275 name, parameter, expansion = m.groups()
276 expansion = re.sub(r'/\*.*?\*/|//.*', r' ', expansion)
Gilles Peskine33c601c2021-03-10 01:25:50 +0100277 if parameter:
278 self.argspecs[name] = [parameter]
Gilles Peskinee7c44552021-01-25 21:40:45 +0100279 if re.match(self._deprecated_definition_re, expansion):
280 # Skip deprecated values, which are assumed to be
281 # backward compatibility aliases that share
282 # numerical values with non-deprecated values.
283 return
Gilles Peskinef8deb752021-01-25 22:41:45 +0100284 if self.is_internal_name(name):
Gilles Peskinee7c44552021-01-25 21:40:45 +0100285 # Macro only to build actual values
286 return
287 elif (name.startswith('PSA_ERROR_') or name == 'PSA_SUCCESS') \
288 and not parameter:
289 self.statuses.add(name)
290 elif name.startswith('PSA_KEY_TYPE_') and not parameter:
291 self.key_types.add(name)
292 elif name.startswith('PSA_KEY_TYPE_') and parameter == 'curve':
293 self.key_types_from_curve[name] = name[:13] + 'IS_' + name[13:]
294 elif name.startswith('PSA_KEY_TYPE_') and parameter == 'group':
295 self.key_types_from_group[name] = name[:13] + 'IS_' + name[13:]
296 elif name.startswith('PSA_ECC_FAMILY_') and not parameter:
297 self.ecc_curves.add(name)
298 elif name.startswith('PSA_DH_FAMILY_') and not parameter:
299 self.dh_groups.add(name)
300 elif name.startswith('PSA_ALG_') and not parameter:
301 if name in ['PSA_ALG_ECDSA_BASE',
302 'PSA_ALG_RSA_PKCS1V15_SIGN_BASE']:
303 # Ad hoc skipping of duplicate names for some numerical values
304 return
305 self.algorithms.add(name)
Gilles Peskine33c601c2021-03-10 01:25:50 +0100306 self.record_algorithm_subtype(name, expansion)
Gilles Peskinee7c44552021-01-25 21:40:45 +0100307 elif name.startswith('PSA_ALG_') and parameter == 'hash_alg':
308 if name in ['PSA_ALG_DSA', 'PSA_ALG_ECDSA']:
309 # A naming irregularity
310 tester = name[:8] + 'IS_RANDOMIZED_' + name[8:]
311 else:
312 tester = name[:8] + 'IS_' + name[8:]
313 self.algorithms_from_hash[name] = tester
314 elif name.startswith('PSA_KEY_USAGE_') and not parameter:
Gilles Peskine33c601c2021-03-10 01:25:50 +0100315 self.key_usage_flags.add(name)
Gilles Peskinee7c44552021-01-25 21:40:45 +0100316 else:
317 # Other macro without parameter
318 return
319
320 _nonascii_re = re.compile(rb'[^\x00-\x7f]+')
321 _continued_line_re = re.compile(rb'\\\r?\n\Z')
322 def read_file(self, header_file):
323 for line in header_file:
324 m = re.search(self._continued_line_re, line)
325 while m:
326 cont = next(header_file)
327 line = line[:m.start(0)] + cont
328 m = re.search(self._continued_line_re, line)
329 line = re.sub(self._nonascii_re, rb'', line).decode('ascii')
330 self.read_line(line)
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200331
332
Gilles Peskineb93f8542021-04-19 13:50:25 +0200333class InputsForTest(PSAMacroEnumerator):
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200334 # pylint: disable=too-many-instance-attributes
335 """Accumulate information about macros to test.
336enumerate
337 This includes macro names as well as information about their arguments
338 when applicable.
339 """
340
341 def __init__(self) -> None:
342 super().__init__()
343 self.all_declared = set() #type: Set[str]
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200344 # Identifier prefixes
345 self.table_by_prefix = {
346 'ERROR': self.statuses,
347 'ALG': self.algorithms,
348 'ECC_CURVE': self.ecc_curves,
349 'DH_GROUP': self.dh_groups,
Gilles Peskine4c7da692021-04-21 21:39:27 +0200350 'KEY_LIFETIME': self.lifetimes,
351 'KEY_LOCATION': self.locations,
352 'KEY_PERSISTENCE': self.persistence_levels,
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200353 'KEY_TYPE': self.key_types,
354 'KEY_USAGE': self.key_usage_flags,
355 } #type: Dict[str, Set[str]]
356 # Test functions
357 self.table_by_test_function = {
358 # Any function ending in _algorithm also gets added to
359 # self.algorithms.
360 'key_type': [self.key_types],
361 'block_cipher_key_type': [self.key_types],
362 'stream_cipher_key_type': [self.key_types],
363 'ecc_key_family': [self.ecc_curves],
364 'ecc_key_types': [self.ecc_curves],
365 'dh_key_family': [self.dh_groups],
366 'dh_key_types': [self.dh_groups],
367 'hash_algorithm': [self.hash_algorithms],
368 'mac_algorithm': [self.mac_algorithms],
369 'cipher_algorithm': [],
370 'hmac_algorithm': [self.mac_algorithms],
371 'aead_algorithm': [self.aead_algorithms],
372 'key_derivation_algorithm': [self.kdf_algorithms],
373 'key_agreement_algorithm': [self.ka_algorithms],
374 'asymmetric_signature_algorithm': [],
375 'asymmetric_signature_wildcard': [self.algorithms],
376 'asymmetric_encryption_algorithm': [],
377 'other_algorithm': [],
Gilles Peskine4c7da692021-04-21 21:39:27 +0200378 'lifetime': [self.lifetimes],
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200379 } #type: Dict[str, List[Set[str]]]
380 self.arguments_for['mac_length'] += ['1', '63']
381 self.arguments_for['min_mac_length'] += ['1', '63']
382 self.arguments_for['tag_length'] += ['1', '63']
383 self.arguments_for['min_tag_length'] += ['1', '63']
384
Gilles Peskined6d2d6a2021-03-30 21:46:35 +0200385 def add_numerical_values(self) -> None:
386 """Add numerical values that are not supported to the known identifiers."""
387 # Sets of names per type
388 self.algorithms.add('0xffffffff')
389 self.ecc_curves.add('0xff')
390 self.dh_groups.add('0xff')
391 self.key_types.add('0xffff')
392 self.key_usage_flags.add('0x80000000')
393
394 # Hard-coded values for unknown algorithms
395 #
396 # These have to have values that are correct for their respective
397 # PSA_ALG_IS_xxx macros, but are also not currently assigned and are
398 # not likely to be assigned in the near future.
399 self.hash_algorithms.add('0x020000fe') # 0x020000ff is PSA_ALG_ANY_HASH
400 self.mac_algorithms.add('0x03007fff')
401 self.ka_algorithms.add('0x09fc0000')
402 self.kdf_algorithms.add('0x080000ff')
403 # For AEAD algorithms, the only variability is over the tag length,
404 # and this only applies to known algorithms, so don't test an
405 # unknown algorithm.
406
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200407 def get_names(self, type_word: str) -> Set[str]:
408 """Return the set of known names of values of the given type."""
409 return {
410 'status': self.statuses,
411 'algorithm': self.algorithms,
412 'ecc_curve': self.ecc_curves,
413 'dh_group': self.dh_groups,
414 'key_type': self.key_types,
415 'key_usage': self.key_usage_flags,
416 }[type_word]
417
418 # Regex for interesting header lines.
419 # Groups: 1=macro name, 2=type, 3=argument list (optional).
420 _header_line_re = \
421 re.compile(r'#define +' +
422 r'(PSA_((?:(?:DH|ECC|KEY)_)?[A-Z]+)_\w+)' +
423 r'(?:\(([^\n()]*)\))?')
424 # Regex of macro names to exclude.
425 _excluded_name_re = re.compile(r'_(?:GET|IS|OF)_|_(?:BASE|FLAG|MASK)\Z')
426 # Additional excluded macros.
427 _excluded_names = set([
428 # Macros that provide an alternative way to build the same
429 # algorithm as another macro.
430 'PSA_ALG_AEAD_WITH_DEFAULT_LENGTH_TAG',
431 'PSA_ALG_FULL_LENGTH_MAC',
432 # Auxiliary macro whose name doesn't fit the usual patterns for
433 # auxiliary macros.
434 'PSA_ALG_AEAD_WITH_DEFAULT_LENGTH_TAG_CASE',
435 ])
436 def parse_header_line(self, line: str) -> None:
437 """Parse a C header line, looking for "#define PSA_xxx"."""
438 m = re.match(self._header_line_re, line)
439 if not m:
440 return
441 name = m.group(1)
442 self.all_declared.add(name)
443 if re.search(self._excluded_name_re, name) or \
Gilles Peskineb93f8542021-04-19 13:50:25 +0200444 name in self._excluded_names or \
445 self.is_internal_name(name):
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200446 return
447 dest = self.table_by_prefix.get(m.group(2))
448 if dest is None:
449 return
450 dest.add(name)
451 if m.group(3):
452 self.argspecs[name] = self._argument_split(m.group(3))
453
454 _nonascii_re = re.compile(rb'[^\x00-\x7f]+') #type: Pattern
455 def parse_header(self, filename: str) -> None:
456 """Parse a C header file, looking for "#define PSA_xxx"."""
457 with read_file_lines(filename, binary=True) as lines:
458 for line in lines:
459 line = re.sub(self._nonascii_re, rb'', line).decode('ascii')
460 self.parse_header_line(line)
461
462 _macro_identifier_re = re.compile(r'[A-Z]\w+')
463 def generate_undeclared_names(self, expr: str) -> Iterable[str]:
464 for name in re.findall(self._macro_identifier_re, expr):
465 if name not in self.all_declared:
466 yield name
467
468 def accept_test_case_line(self, function: str, argument: str) -> bool:
469 #pylint: disable=unused-argument
470 undeclared = list(self.generate_undeclared_names(argument))
471 if undeclared:
472 raise Exception('Undeclared names in test case', undeclared)
473 return True
474
Gilles Peskine0a93c1b2021-04-21 15:36:58 +0200475 @staticmethod
476 def normalize_argument(argument: str) -> str:
477 """Normalize whitespace in the given C expression.
478
479 The result uses the same whitespace as
480 ` PSAMacroEnumerator.distribute_arguments`.
481 """
482 return re.sub(r',', r', ', re.sub(r' +', r'', argument))
483
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200484 def add_test_case_line(self, function: str, argument: str) -> None:
485 """Parse a test case data line, looking for algorithm metadata tests."""
486 sets = []
487 if function.endswith('_algorithm'):
488 sets.append(self.algorithms)
489 if function == 'key_agreement_algorithm' and \
490 argument.startswith('PSA_ALG_KEY_AGREEMENT('):
491 # We only want *raw* key agreement algorithms as such, so
492 # exclude ones that are already chained with a KDF.
493 # Keep the expression as one to test as an algorithm.
494 function = 'other_algorithm'
495 sets += self.table_by_test_function[function]
496 if self.accept_test_case_line(function, argument):
497 for s in sets:
Gilles Peskine0a93c1b2021-04-21 15:36:58 +0200498 s.add(self.normalize_argument(argument))
Gilles Peskine3cf3a8e2021-03-30 19:09:05 +0200499
500 # Regex matching a *.data line containing a test function call and
501 # its arguments. The actual definition is partly positional, but this
502 # regex is good enough in practice.
503 _test_case_line_re = re.compile(r'(?!depends_on:)(\w+):([^\n :][^:\n]*)')
504 def parse_test_cases(self, filename: str) -> None:
505 """Parse a test case file (*.data), looking for algorithm metadata tests."""
506 with read_file_lines(filename) as lines:
507 for line in lines:
508 m = re.match(self._test_case_line_re, line)
509 if m:
510 self.add_test_case_line(m.group(1), m.group(2))