Skip to content

Commit e07b664

Browse files
authored
Merge pull request #278 from gilles-peskine-arm/generate_mldsa_tests-create
Support committed generated test data and generate PQCP test data
2 parents 0879d0c + d1bb3a6 commit e07b664

7 files changed

Lines changed: 476 additions & 14 deletions

File tree

scripts/mbedtls_framework/config_macros.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
import glob
77
import os
88
import re
9-
from typing import FrozenSet, Iterable, Iterator
9+
from typing import FrozenSet, Iterable, Iterator, List
1010

1111
from . import build_tree
12+
from . import generate_files_helper
1213

1314

1415
class ConfigMacros:
@@ -34,7 +35,7 @@ def _load_file(filename: str) -> FrozenSet[str]:
3435
for line in input_)
3536

3637

37-
class Current(ConfigMacros):
38+
class Current(ConfigMacros, generate_files_helper.Generator):
3839
"""Information about config-like macros parsed from the source code."""
3940

4041
_SHADOW_FILE = 'scripts/data_files/config-options-current.txt'
@@ -136,6 +137,25 @@ def update_shadow_file(self, always_update: bool) -> None:
136137
for name in sorted(self.live_config_options()):
137138
out.write(name + '\n')
138139

140+
# Implement the generate_files_helper.Generator interface
141+
def generator_name(self) -> str:
142+
"""Name as a generate_files_helper.Generator."""
143+
return 'options'
144+
145+
def target_files(self) -> List[str]:
146+
"""List the (single) generated file name."""
147+
return [os.path.join(self._submodule, self._SHADOW_FILE)]
148+
149+
def outdated_files(self) -> List[str]:
150+
"""List the (single) generated file name if it is out of date."""
151+
if self.is_shadow_file_up_to_date():
152+
return []
153+
else:
154+
return self.target_files()
155+
156+
def update(self, always: bool) -> None:
157+
"""Update the shadow file from the live config file."""
158+
self.update_shadow_file(always)
139159

140160

141161
class History(ConfigMacros):
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""Utilities for intermediate files that are generated, but platform-independent
2+
and configuration-independent.
3+
"""
4+
5+
# Copyright The Mbed TLS Contributors
6+
# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
7+
8+
import argparse
9+
import os
10+
import subprocess
11+
import sys
12+
from typing import Dict, Iterable, List, Sequence, Set
13+
14+
15+
class Generator:
16+
"""An abstract base class for generators of intermediate files."""
17+
18+
def generator_name(self) -> str:
19+
"""A name for this generator.
20+
21+
Generator names must be unique and should not be identical to
22+
the name of any target.
23+
"""
24+
raise NotImplementedError
25+
26+
def target_files(self) -> List[str]:
27+
"""The list of files targeted by this generator.
28+
29+
File names are relative to the project root.
30+
"""
31+
raise NotImplementedError
32+
33+
def outdated_files(self) -> Iterable[str]:
34+
"""Return the list of targets that are out of date.
35+
36+
This is empty after running update().
37+
Missing targets are considered out of date.
38+
"""
39+
raise NotImplementedError
40+
41+
def update(self, always: bool) -> None:
42+
"""Update the target(s) of this generator.
43+
44+
If always is false, avoid changing the output file if it already has
45+
the desired content. If always is true, make sure to update the
46+
time stamp on the output file even if it already has the desired content.
47+
"""
48+
raise NotImplementedError
49+
50+
51+
class TestDataGenerator(Generator):
52+
"""A test data generator script.
53+
54+
Even though the test data generator scripts are written in Python, we
55+
run them as a separate process, because their output depends on the
56+
program name (they write sys.argv[0] in a comment in the .data file).
57+
"""
58+
59+
def __init__(self, script: str) -> None:
60+
"""Run the specified test generator to generate files.
61+
62+
Assume that the script is written in Python and has the command line
63+
interface of test_data_generation.py.
64+
"""
65+
self.script = script
66+
67+
def generator_name(self) -> str:
68+
return os.path.basename(self.script)
69+
70+
def target_files(self) -> List[str]:
71+
output = subprocess.check_output([sys.executable, self.script, '--list'],
72+
encoding='utf-8')
73+
return output.splitlines()
74+
75+
def outdated_files(self) -> List[str]:
76+
output = subprocess.check_output([sys.executable, self.script, '--list-outdated'],
77+
encoding='utf-8')
78+
return output.splitlines()
79+
80+
def update(self, _always) -> None:
81+
subprocess.check_call([sys.executable, self.script])
82+
83+
84+
def assemble(available: Iterable[Generator]) -> Dict[str, Generator]:
85+
"""Assemble the generators into a dictionary with both names and targets as keys."""
86+
by_ident = {} #type: Dict[str, Generator]
87+
for generator in available:
88+
ident = generator.generator_name()
89+
if ident in by_ident:
90+
raise Exception(f'Generator conflict: name "{ident}" of {generator} '
91+
f'already recorded for {by_ident[ident]}')
92+
by_ident[ident] = generator
93+
for ident in generator.target_files():
94+
if ident in by_ident:
95+
raise Exception(f'Generator conflict: target "{ident}" of {generator} '
96+
f'already recorded for {by_ident[ident]}')
97+
by_ident[ident] = generator
98+
return by_ident
99+
100+
def list_names(available: Iterable[Generator]) -> List[str]:
101+
"""Return the list of generator names."""
102+
return sorted(generator.generator_name() for generator in available)
103+
104+
def list_targets(available: Iterable[Generator]) -> List[str]:
105+
"""Return the list of generator targets."""
106+
return sorted(target
107+
for generator in available
108+
for target in generator.target_files())
109+
110+
def select(available: Dict[str, Generator],
111+
wanted: Iterable[str]) -> List[Generator]:
112+
"""Select generators by name or target."""
113+
wanted_names = set() #type: Set[str]
114+
for ident in wanted:
115+
if ident not in available:
116+
raise Exception(f'No generator found for {ident}')
117+
wanted_names.add(ident)
118+
return [available[name] for name in sorted(wanted_names)]
119+
120+
def main(generators: Sequence[Generator],
121+
description: str) -> None:
122+
#pylint: disable=too-many-branches
123+
"""Command line entry point.
124+
"""
125+
parser = argparse.ArgumentParser(description=description)
126+
parser.add_argument('--always-update', '-U',
127+
action='store_true',
128+
help=('Update target files unconditionally '
129+
'(overrides --update)'))
130+
parser.add_argument('--list',
131+
action='store_true',
132+
help='List generator names and targets and exit')
133+
parser.add_argument('--list-names',
134+
action='store_true',
135+
help='List generator names and exit')
136+
parser.add_argument('--list-targets',
137+
action='store_true',
138+
help='List generator targets and exit')
139+
parser.add_argument('--update', '-u',
140+
action='store_true',
141+
help='Update target files if needed')
142+
parser.add_argument('--verbose', '-v',
143+
action='store_true',
144+
help='Be more verbose')
145+
parser.add_argument('idents', nargs='*', metavar='NAME|TARGET',
146+
help='List of generator names or targets (all targets if empty)')
147+
args = parser.parse_args()
148+
149+
if args.list:
150+
args.list_names = True
151+
args.list_targets = True
152+
if args.list_names:
153+
for name in list_names(generators):
154+
print(name)
155+
if args.list_targets:
156+
for target in list_targets(generators):
157+
print(target)
158+
if args.list_names or args.list_targets:
159+
return
160+
161+
if args.idents:
162+
available = assemble(generators)
163+
wanted = select(available, args.idents) #type: Sequence[Generator]
164+
else:
165+
wanted = generators
166+
if args.update or args.always_update:
167+
for generator in wanted:
168+
if args.verbose:
169+
sys.stderr.write(f'Running generator {generator.generator_name()}...\n')
170+
generator.update(args.always_update)
171+
else:
172+
outdated = [] #type: List[str]
173+
for generator in wanted:
174+
if args.verbose:
175+
sys.stderr.write(f'Checking targets of generator {generator.generator_name()}...\n')
176+
outdated += generator.outdated_files()
177+
if outdated:
178+
sys.stderr.write(f'Some targets are missing or out of date.\n')
179+
for target in outdated:
180+
print(target)
181+
sys.stderr.write(f'Run {sys.argv[0]} -u and commit the result.')
182+
sys.exit(1)

scripts/mbedtls_framework/test_case.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,22 +127,28 @@ def write(self, out: typing_util.Writable) -> None:
127127
out.write(prefix + self.function + ':' +
128128
':'.join(self.arguments) + '\n')
129129

130+
def write_data_stream(out,
131+
test_cases: Iterable[TestCase],
132+
caller: Optional[str] = None) -> None:
133+
"""Write the test cases to the specified output stream."""
134+
if caller is None:
135+
caller = os.path.basename(sys.argv[0])
136+
out.write('# Automatically generated by {}. Do not edit!\n'
137+
.format(caller))
138+
for tc in test_cases:
139+
tc.write(out)
140+
out.write('\n# End of automatically generated file.\n')
141+
130142
def write_data_file(filename: str,
131143
test_cases: Iterable[TestCase],
132144
caller: Optional[str] = None) -> None:
133145
"""Write the test cases to the specified file.
134146
135147
If the file already exists, it is overwritten.
136148
"""
137-
if caller is None:
138-
caller = os.path.basename(sys.argv[0])
139149
tempfile = filename + '.new'
140150
with open(tempfile, 'w') as out:
141-
out.write('# Automatically generated by {}. Do not edit!\n'
142-
.format(caller))
143-
for tc in test_cases:
144-
tc.write(out)
145-
out.write('\n# End of automatically generated file.\n')
151+
write_data_stream(out, test_cases, caller)
146152
os.replace(tempfile, filename)
147153

148154
def psa_or_3_6_feature_macro(psa_name: str,

scripts/mbedtls_framework/test_data_generation.py

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#
1212

1313
import argparse
14+
import io
1415
import os
1516
import posixpath
1617
import re
@@ -139,6 +140,11 @@ def generate_tests(cls) -> Iterator[test_case.TestCase]:
139140

140141
class TestGenerator:
141142
"""Generate test cases and write to data files."""
143+
144+
# Note that targets whose names contain 'test_format' have their content
145+
# validated by `abi_check.py`.
146+
targets = {} # type: Dict[str, Callable[..., Iterable[test_case.TestCase]]]
147+
142148
def __init__(self, options) -> None:
143149
self.test_suite_directory = options.directory
144150
# Update `targets` with an entry for each child class of BaseTarget.
@@ -163,10 +169,6 @@ def write_test_data_file(self, basename: str,
163169
filename = self.filename_for(basename)
164170
test_case.write_data_file(filename, test_cases)
165171

166-
# Note that targets whose names contain 'test_format' have their content
167-
# validated by `abi_check.py`.
168-
targets = {} # type: Dict[str, Callable[..., Iterable[test_case.TestCase]]]
169-
170172
def generate_target(self, name: str, *target_args) -> None:
171173
"""Generate cases and write to data file for a target.
172174
@@ -176,13 +178,32 @@ def generate_target(self, name: str, *target_args) -> None:
176178
test_cases = self.targets[name](*target_args)
177179
self.write_test_data_file(name, test_cases)
178180

181+
def is_up_to_date(self, target) -> bool:
182+
"""Check if the given target already has the expected content."""
183+
filename = self.filename_for(target)
184+
if not os.path.exists(filename):
185+
return False
186+
test_cases = self.targets[target]()
187+
out = io.StringIO()
188+
test_case.write_data_stream(out, test_cases)
189+
out.seek(0)
190+
new_content = out.read()
191+
out.close()
192+
with open(filename) as current_file:
193+
old_content = current_file.read()
194+
return new_content == old_content
195+
196+
179197
def main(args, description: str, generator_class: Type[TestGenerator] = TestGenerator):
180198
"""Command line entry point."""
181199
parser = argparse.ArgumentParser(description=description)
182200
parser.add_argument('--list', action='store_true',
183201
help='List available targets and exit')
184202
parser.add_argument('--list-for-cmake', action='store_true',
185203
help='Print \';\'-separated list of available targets and exit')
204+
parser.add_argument('--list-outdated', action='store_true',
205+
help=('List outdated targets and exit '
206+
'(succeeds even if there are outdated or missing targets)'))
186207
# If specified explicitly, this option may be a path relative to the
187208
# current directory when the script is invoked. The default value
188209
# is relative to the mbedtls root, which we don't know yet. So we
@@ -221,4 +242,8 @@ def main(args, description: str, generator_class: Type[TestGenerator] = TestGene
221242
else:
222243
options.targets = sorted(generator.targets)
223244
for target in options.targets:
224-
generator.generate_target(target)
245+
if options.list_outdated:
246+
if not generator.is_up_to_date(target):
247+
print(generator.filename_for(target))
248+
else:
249+
generator.generate_target(target)

0 commit comments

Comments
 (0)