macro_collector.py 23 KB

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