Skip to content

Commit 2afaa78

Browse files
Merge pull request #20 from gilles-peskine-arm/search_outcomes_config-create
Script to search the outcome file for configurations with given requirements
2 parents 1a83e0c + 8249b91 commit 2afaa78

File tree

1 file changed

+225
-0
lines changed

1 file changed

+225
-0
lines changed

scripts/search_outcomes_config.py

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
#!/usr/bin/env python
2+
"""Search an outcome file for configurations with given settings.
3+
4+
Read an outcome file and report the configurations in which test_suite_config
5+
runs with the required settings (compilation option enabled or disabled).
6+
"""
7+
8+
import argparse
9+
import os
10+
import re
11+
import subprocess
12+
from typing import Dict, FrozenSet, Iterator, List, Set
13+
import tempfile
14+
import unittest
15+
16+
from mbedtls_framework import build_tree
17+
18+
19+
def make_regexp_for_settings(settings: List[str]) -> str:
20+
"""Construct a regexp matching the interesting outcome lines.
21+
22+
Interesting outcome lines are from test_suite_config where the given
23+
setting is passing.
24+
25+
We assume that the elements of settings don't contain regexp special
26+
characters.
27+
"""
28+
return (r';test_suite_config[^;]*;Config: (' +
29+
'|'.join(settings) +
30+
r');PASS;')
31+
32+
def run_grep(regexp: str, outcome_file: str) -> List[str]:
33+
"""Run grep on the outcome file and return the matching lines."""
34+
env = os.environ.copy()
35+
env['LC_ALL'] = 'C' # Speeds up some versions of GNU grep
36+
try:
37+
return subprocess.check_output(['grep', '-E', regexp, outcome_file],
38+
encoding='ascii',
39+
env=env).splitlines()
40+
except subprocess.CalledProcessError as exn:
41+
if exn.returncode == 1:
42+
return [] # No results. We don't consider this an error.
43+
raise
44+
45+
OUTCOME_LINE_RE = re.compile(r'[^;]*;'
46+
r'([^;]*);'
47+
r'test_suite_config\.(?:[^;]*);'
48+
r'Config: ([^;]*);'
49+
r'PASS;')
50+
51+
def extract_configuration_data(outcome_lines: List[str]) -> Dict[str, FrozenSet[str]]:
52+
"""Extract the configuration data from outcome lines.
53+
54+
The result maps a configuration name to the list of passing settings
55+
in that configuration.
56+
"""
57+
config_data = {} #type: Dict[str, Set[str]]
58+
for line in outcome_lines:
59+
m = OUTCOME_LINE_RE.match(line)
60+
# Assuming a well-formed outcome file, make_regexp_for_settings()
61+
# arranges to only return lines that should match OUTCOME_LINE_RE.
62+
# So this assertion can't fail unless there is an unexpected
63+
# divergence between OUTCOME_LINE_RE, make_regexp_for_settings()
64+
# and the format of the given outcome file
65+
assert m is not None
66+
config_name, setting = m.groups()
67+
if config_name not in config_data:
68+
config_data[config_name] = set()
69+
config_data[config_name].add(setting)
70+
return dict((name, frozenset(settings))
71+
for name, settings in config_data.items())
72+
73+
74+
def matching_configurations(config_data: Dict[str, FrozenSet[str]],
75+
required: List[str]) -> Iterator[str]:
76+
"""Search configurations with the given passing settings.
77+
78+
config_data maps a configuration name to the list of passing settings
79+
in that configuration.
80+
81+
Each setting should be an Mbed TLS compile setting (MBEDTLS_xxx or
82+
PSA_xxx), optionally prefixed with "!".
83+
"""
84+
required_set = frozenset(required)
85+
for config, observed in config_data.items():
86+
if required_set.issubset(observed):
87+
yield config
88+
89+
def search_config_outcomes(outcome_file: str, settings: List[str]) -> List[str]:
90+
"""Search the given outcome file for reports of the given settings.
91+
92+
Each setting should be an Mbed TLS compile setting (MBEDTLS_xxx or
93+
PSA_xxx), optionally prefixed with "!".
94+
"""
95+
# The outcome file is large enough (hundreds of MB) that parsing it
96+
# in Python is slow. Use grep to speed this up considerably.
97+
regexp = make_regexp_for_settings(settings)
98+
outcome_lines = run_grep(regexp, outcome_file)
99+
config_data = extract_configuration_data(outcome_lines)
100+
return sorted(matching_configurations(config_data, settings))
101+
102+
103+
class TestSearch(unittest.TestCase):
104+
"""Tests of search functionality."""
105+
106+
OUTCOME_FILE_CONTENT = """\
107+
whatever;foobar;test_suite_config.part;Config: MBEDTLS_FOO;PASS;
108+
whatever;foobar;test_suite_config.part;Config: !MBEDTLS_FOO;SKIP;
109+
whatever;foobar;test_suite_config.part;Config: MBEDTLS_BAR;PASS;
110+
whatever;foobar;test_suite_config.part;Config: !MBEDTLS_BAR;SKIP;
111+
whatever;foobar;test_suite_config.part;Config: MBEDTLS_QUX;SKIP;
112+
whatever;foobar;test_suite_config.part;Config: !MBEDTLS_QUX;PASS;
113+
whatever;fooqux;test_suite_config.part;Config: MBEDTLS_FOO;PASS;
114+
whatever;fooqux;test_suite_config.part;Config: !MBEDTLS_FOO;SKIP;
115+
whatever;fooqux;test_suite_config.part;Config: MBEDTLS_BAR;SKIP;
116+
whatever;fooqux;test_suite_config.part;Config: !MBEDTLS_BAR;PASS;
117+
whatever;fooqux;test_suite_config.part;Config: MBEDTLS_QUX;PASS;
118+
whatever;fooqux;test_suite_config.part;Config: !MBEDTLS_QUX;SKIP;
119+
whatever;fooqux;test_suite_something.else;Config: MBEDTLS_BAR;PASS;
120+
whatever;boring;test_suite_config.part;Config: BORING;PASS;
121+
whatever;parasite;not_test_suite_config.not;Config: MBEDTLS_FOO;PASS;
122+
whatever;parasite;test_suite_config.but;Config: MBEDTLS_QUX with bells on;PASS;
123+
whatever;parasite;test_suite_config.but;Not Config: MBEDTLS_QUX;PASS;
124+
"""
125+
126+
def search(self, settings: List[str], expected: List[str]) -> None:
127+
"""Test the search functionality.
128+
129+
* settings: settings to search.
130+
* expected: expected search results.
131+
"""
132+
with tempfile.NamedTemporaryFile() as tmp:
133+
tmp.write(self.OUTCOME_FILE_CONTENT.encode())
134+
tmp.flush()
135+
actual = search_config_outcomes(tmp.name, settings)
136+
self.assertEqual(actual, expected)
137+
138+
def test_foo(self) -> None:
139+
self.search(['MBEDTLS_FOO'], ['foobar', 'fooqux'])
140+
141+
def test_bar(self) -> None:
142+
self.search(['MBEDTLS_BAR'], ['foobar'])
143+
144+
def test_foo_bar(self) -> None:
145+
self.search(['MBEDTLS_FOO', 'MBEDTLS_BAR'], ['foobar'])
146+
147+
def test_foo_notbar(self) -> None:
148+
self.search(['MBEDTLS_FOO', '!MBEDTLS_BAR'], ['fooqux'])
149+
150+
151+
class TestOutcome(unittest.TestCase):
152+
"""Tests of outcome file format expectations.
153+
154+
This class builds and runs the config tests in the current configuration.
155+
The configuration must have at least one feature enabled and at least
156+
one feature disabled in each category: MBEDTLS_xxx and PSA_WANT_xxx.
157+
It needs a C compiler.
158+
"""
159+
160+
outcome_content = '' # Let mypy know this field can be used in test case methods
161+
162+
@classmethod
163+
def setUpClass(cls) -> None:
164+
"""Generate, build and run the config tests."""
165+
root_dir = build_tree.guess_project_root()
166+
tests_dir = os.path.join(root_dir, 'tests')
167+
suites = ['test_suite_config.mbedtls_boolean',
168+
'test_suite_config.psa_boolean']
169+
_output = subprocess.check_output(['make'] + suites,
170+
cwd=tests_dir,
171+
stderr=subprocess.STDOUT)
172+
with tempfile.NamedTemporaryFile(dir=tests_dir) as outcome_file:
173+
env = os.environ.copy()
174+
env['MBEDTLS_TEST_PLATFORM'] = 'some_platform'
175+
env['MBEDTLS_TEST_CONFIGURATION'] = 'some_configuration'
176+
env['MBEDTLS_TEST_OUTCOME_FILE'] = outcome_file.name
177+
for suite in suites:
178+
_output = subprocess.check_output([os.path.join(os.path.curdir, suite)],
179+
cwd=tests_dir,
180+
env=env,
181+
stderr=subprocess.STDOUT)
182+
cls.outcome_content = outcome_file.read().decode('ascii')
183+
184+
def test_outcome_format(self) -> None:
185+
"""Check that there are outcome lines matching the expected general format."""
186+
def regex(prefix: str, result: str) -> str:
187+
return (r'(?:\A|\n)some_platform;some_configuration;'
188+
r'test_suite_config\.\w+;Config: {}_\w+;{};'
189+
.format(prefix, result))
190+
self.assertRegex(self.outcome_content, regex('MBEDTLS', 'PASS'))
191+
self.assertRegex(self.outcome_content, regex('MBEDTLS', 'SKIP'))
192+
self.assertRegex(self.outcome_content, regex('!MBEDTLS', 'PASS'))
193+
self.assertRegex(self.outcome_content, regex('!MBEDTLS', 'SKIP'))
194+
self.assertRegex(self.outcome_content, regex('PSA_WANT', 'PASS'))
195+
self.assertRegex(self.outcome_content, regex('PSA_WANT', 'SKIP'))
196+
self.assertRegex(self.outcome_content, regex('!PSA_WANT', 'PASS'))
197+
self.assertRegex(self.outcome_content, regex('!PSA_WANT', 'SKIP'))
198+
199+
def test_outcome_lines(self) -> None:
200+
"""Look for some sample outcome lines."""
201+
def regex(setting: str) -> str:
202+
return (r'(?:\A|\n)some_platform;some_configuration;'
203+
r'test_suite_config\.\w+;Config: {};(PASS|SKIP);'
204+
.format(setting))
205+
self.assertRegex(self.outcome_content, regex('MBEDTLS_AES_C'))
206+
self.assertRegex(self.outcome_content, regex('MBEDTLS_AES_ROM_TABLES'))
207+
self.assertRegex(self.outcome_content, regex('MBEDTLS_SSL_CLI_C'))
208+
self.assertRegex(self.outcome_content, regex('MBEDTLS_X509_CRT_PARSE_C'))
209+
self.assertRegex(self.outcome_content, regex('PSA_WANT_ALG_HMAC'))
210+
self.assertRegex(self.outcome_content, regex('PSA_WANT_KEY_TYPE_AES'))
211+
212+
def main() -> None:
213+
parser = argparse.ArgumentParser(description=__doc__)
214+
parser.add_argument('--outcome-file', '-f', metavar='FILE',
215+
default='outcomes.csv',
216+
help='Outcome file to read (default: outcomes.csv)')
217+
parser.add_argument('settings', metavar='SETTING', nargs='+',
218+
help='Required setting (e.g. "MBEDTLS_RSA_C" or "!PSA_WANT_ALG_SHA256")')
219+
options = parser.parse_args()
220+
found = search_config_outcomes(options.outcome_file, options.settings)
221+
for name in found:
222+
print(name)
223+
224+
if __name__ == '__main__':
225+
main()

0 commit comments

Comments
 (0)