Skip to content

Commit ef7eb6d

Browse files
committed
ACVP: Filter unsupported functions from test cases
The previous encapDecap-file gate only loaded the file when all four sub-functions (encapsulation, decapsulation, encapsulationKeyCheck, decapsulationKeyCheck) were compiled in. Under encaps-only, decaps-only, keygen-encaps, and keygen-decaps configurations that skipped the entire prompt file, so ACVP reported OK while driving zero test cases through the binary. This commit intead loads the encapDecap file whenever _any_ of its sub-functions are supported, and drops unsupported test cases via a filter that mutates both the prompt and the expectedResults data consistently. This mirrors the filter_test_cases machinery introduced for the same reason in mldsa-native (PR #1140). Reduced-API runs also now emit stderr warnings for both whole-prompt skips and per-test drops, both inline (context stays with the operation) and as a labelled summary block after "ALL GOOD!" so coverage gaps aren't buried in scrollback. Full-API runs stay silent. Signed-off-by: Hanno Becker <beckphan@amazon.co.uk>
1 parent 94550ab commit ef7eb6d

1 file changed

Lines changed: 94 additions & 16 deletions

File tree

test/acvp/acvp_client.py

Lines changed: 94 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -105,47 +105,96 @@ def detect_supported_functions():
105105
return set(ALL_ACVP_FUNCTIONS)
106106

107107

108+
ENCAP_DECAP_FUNCTIONS = {
109+
"encapsulation",
110+
"encapsulationKeyCheck",
111+
"decapsulation",
112+
"decapsulationKeyCheck",
113+
}
114+
115+
108116
def loadDefaultAcvpData(version, supported_functions=None):
109117
if supported_functions is None:
110118
supported_functions = set(ALL_ACVP_FUNCTIONS)
111119

112-
# The ACVP encapDecap file bundles encapsulation, decapsulation, and both
113-
# *KeyCheck helpers. To keep result comparison byte-identical against the
114-
# published expectedResults.json, we only load the file if all four
115-
# sub-functions are compiled in.
116-
encapDecap_functions = {
117-
"encapsulation",
118-
"encapsulationKeyCheck",
119-
"decapsulation",
120-
"decapsulationKeyCheck",
121-
}
122-
encapDecap_supported = encapDecap_functions.issubset(supported_functions)
123-
keyGen_supported = "keyGen" in supported_functions
120+
# Load a prompt whenever any of its sub-functions is compiled in.
121+
# Unsupported sub-functions are filtered out per test case (see
122+
# filter_test_cases) so result comparison stays byte-identical.
123+
keyGen_any = "keyGen" in supported_functions
124+
encapDecap_any = bool(ENCAP_DECAP_FUNCTIONS & supported_functions)
124125

125126
data_dir = f"test/acvp/.acvp-data/{version}/files"
126127
acvp_jsons_for_version = [
127128
(
128129
"keyGen",
129-
keyGen_supported,
130+
keyGen_any,
130131
f"{data_dir}/ML-KEM-keyGen-FIPS203/prompt.json",
131132
f"{data_dir}/ML-KEM-keyGen-FIPS203/expectedResults.json",
132133
),
133134
(
134135
"encapDecap",
135-
encapDecap_supported,
136+
encapDecap_any,
136137
f"{data_dir}/ML-KEM-encapDecap-FIPS203/prompt.json",
137138
f"{data_dir}/ML-KEM-encapDecap-FIPS203/expectedResults.json",
138139
),
139140
]
140141
acvp_data = []
141142
for mode, is_supported, prompt, expectedResults in acvp_jsons_for_version:
142143
if not is_supported:
143-
info(f"Skipping {mode} tests (mode not supported in this build)")
144+
warn(
145+
f"WARNING: Skipped {mode} ACVP prompt "
146+
f"(no sub-function supported by this build)."
147+
)
144148
continue
145149
acvp_data.append(loadAcvpData(prompt, expectedResults))
146150
return acvp_data
147151

148152

153+
def unwrap_acvts(data):
154+
# ACVTS files wrap the payload as [{"acvVersion": ...}, {...}].
155+
return data[1] if isinstance(data, list) else data
156+
157+
158+
def filter_test_cases(acvp_data, should_drop):
159+
"""Drop cases for which should_drop(tg, tc) returns a reason (None keeps).
160+
Reasons are computed from the prompt but applied consistently to both
161+
prompt and expected data, so downstream byte-identical result comparison
162+
still works. Returns the list of drop reasons for reporting."""
163+
reasons = []
164+
for _, promptData, _, expectedData in acvp_data:
165+
drop = {}
166+
for tg in unwrap_acvts(promptData).get("testGroups", []):
167+
for tc in tg["tests"]:
168+
reason = should_drop(tg, tc)
169+
if reason is not None:
170+
drop[tg["tgId"], tc["tcId"]] = reason
171+
for data in (promptData, expectedData):
172+
if data is None:
173+
continue
174+
for tg in unwrap_acvts(data).get("testGroups", []):
175+
tg["tests"] = [
176+
tc for tc in tg["tests"] if (tg["tgId"], tc["tcId"]) not in drop
177+
]
178+
reasons += drop.values()
179+
return reasons
180+
181+
182+
def unsupported_function(supported_functions):
183+
"""Return a should_drop predicate that skips encapDecap test cases whose
184+
tg['function'] is not in supported_functions. keyGen prompts have no
185+
tg['function'] field, so they are never dropped by this predicate.
186+
The predicate returns the bare function name so the caller can format
187+
a compact 'Unsupported functions: A, B, C' summary."""
188+
189+
def should_drop(tg, tc):
190+
fn = tg.get("function")
191+
if fn is None or fn in supported_functions:
192+
return None
193+
return fn
194+
195+
return should_drop
196+
197+
149198
def err(msg, **kwargs):
150199
print(msg, file=sys.stderr, **kwargs)
151200

@@ -154,6 +203,23 @@ def info(msg, **kwargs):
154203
print(msg, **kwargs)
155204

156205

206+
# Warnings deferred to the end of the run so they stand out after all the
207+
# per-test-case chatter. Collected by warn(), emitted once by flush_warnings().
208+
_warnings = []
209+
210+
211+
def warn(msg):
212+
_warnings.append(msg)
213+
214+
215+
def flush_warnings():
216+
if not _warnings:
217+
return
218+
err("")
219+
for msg in _warnings:
220+
err(msg)
221+
222+
157223
def get_acvp_binary(tg):
158224
"""Convert JSON dict for ACVP test group to suitable ACVP binary."""
159225
parameterSetToLevel = {
@@ -374,11 +440,23 @@ def test(prompt, expected, output, version, supported_functions=None):
374440
data = loadDefaultAcvpData(version, supported_functions)
375441

376442
if not data:
377-
info("No ACVP tests supported by this build")
443+
warn("WARNING: No ACVP tests supported by this build; no cases were run.")
378444
info("ALL GOOD!")
445+
flush_warnings()
379446
return
380447

448+
# Filter out test cases whose ACVP function isn't compiled in.
449+
if supported_functions is not None:
450+
reasons = filter_test_cases(data, unsupported_function(supported_functions))
451+
if reasons:
452+
fns = ", ".join(sorted(set(reasons)))
453+
warn(
454+
f"WARNING: Dropped {len(reasons)} ACVP test case(s). "
455+
f"Unsupported functions: {fns}."
456+
)
457+
381458
runTest(data, output)
459+
flush_warnings()
382460

383461

384462
parser = argparse.ArgumentParser()

0 commit comments

Comments
 (0)