Skip to content

Commit 80c10bd

Browse files
committed
Add helper script to run compliance test in parallel
1 parent 5945ebf commit 80c10bd

File tree

1 file changed

+125
-0
lines changed

1 file changed

+125
-0
lines changed

scripts/run_compliance_tests.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
2+
from enum import StrEnum
3+
import sys
4+
import re
5+
import os
6+
from pathlib import Path
7+
import asyncio
8+
from concurrent.futures import ThreadPoolExecutor
9+
import subprocess
10+
import argparse
11+
12+
from dataclasses import dataclass
13+
14+
15+
# Orginal CEC implementation (which serves as a oracle in complaiance tests) leaks memory and it breaks fuzztest test runner.
16+
_DISABLE_ASAN = {"ASAN_OPTIONS": "detect_leaks=0"}
17+
18+
ExitCode = int
19+
Seconds = int
20+
21+
22+
class CecEdition(StrEnum):
23+
CEC2013 = "2013"
24+
CEC2014 = "2014"
25+
26+
27+
@dataclass
28+
class FuzzTest:
29+
test_group: str
30+
test_name: str
31+
32+
def cec_edition(self) -> CecEdition:
33+
return CecEdition(re.findall(r"\d+", self.test_group)[0])
34+
35+
def __str__(self) -> str:
36+
return f"{self.test_group}.{self.test_name}"
37+
38+
39+
def open_range(start, end):
40+
return range(start + 1, end)
41+
42+
43+
def run_test(
44+
target: Path, test: FuzzTest, duration: Seconds
45+
) -> tuple[FuzzTest, ExitCode]:
46+
print(f"Running compliance test [{test}] for next {duration}s...")
47+
p = subprocess.run(
48+
[f"{target}", "--gtest_filter", str(test), "--fuzz_for", f"{duration}s"],
49+
env=os.environ | _DISABLE_ASAN,
50+
)
51+
return (test, p.returncode)
52+
53+
54+
def parse_fuzztest_list(input: list[str]) -> list[FuzzTest]:
55+
header_pos = [
56+
index for index, entry in enumerate(input) if "ComplianceTest" in entry
57+
]
58+
header_pos.append(len(input))
59+
zipped = zip(header_pos, header_pos[1:])
60+
tests = []
61+
for start, end in zipped:
62+
grp = input[start].removesuffix(".")
63+
tests.append([FuzzTest(grp, input[i]) for i in open_range(start, end)])
64+
return [t for ts in tests for t in ts]
65+
66+
67+
async def get_fuzztests(target: Path) -> list[FuzzTest]:
68+
p = await asyncio.create_subprocess_exec(
69+
f"{target}", "--gtest_list_tests", stdout=asyncio.subprocess.PIPE
70+
)
71+
stdout, _ = await p.communicate()
72+
return parse_fuzztest_list(stdout.decode().split())
73+
74+
75+
async def run_compliance_tests(args: list[str]) -> None:
76+
parser = argparse.ArgumentParser(prog="CEC compliance test running helper.")
77+
parser.add_argument(
78+
"-t",
79+
"--target",
80+
default="./build-clang++/test/compliance/cecxx-compliance-tests",
81+
type=Path,
82+
help="Path to compliance test executable.",
83+
)
84+
parser.add_argument(
85+
"-e",
86+
"--edition",
87+
nargs="+",
88+
help="List of CEC editions for which compliance tests will be run.",
89+
)
90+
parser.add_argument(
91+
"-d",
92+
"--duration",
93+
type=int,
94+
default=1,
95+
help="Duration of single compliance test in seconds. The longer duration, the more examples will be covered.",
96+
)
97+
parser.add_argument(
98+
"-j",
99+
"--jobs",
100+
default=1,
101+
type=int,
102+
help="Number of simultaneously executed tests.",
103+
)
104+
parsed = parser.parse_args(args)
105+
106+
tests = await get_fuzztests(parsed.target)
107+
if not tests:
108+
print(f"No fuzz tests for target [{parsed.target}].")
109+
with ThreadPoolExecutor(max_workers=parsed.jobs) as tp_ex:
110+
loop = asyncio.get_event_loop()
111+
futures = [
112+
loop.run_in_executor(tp_ex, run_test, parsed.target, t, parsed.duration)
113+
for t in tests
114+
if t.cec_edition() in parsed.edition
115+
]
116+
for test, excode in await asyncio.gather(*futures):
117+
print(f"Test [{test}] finished with exit code [{excode}].")
118+
119+
120+
if __name__ == "__main__":
121+
try:
122+
asyncio.run(run_compliance_tests(sys.argv[1:]))
123+
except Exception as err:
124+
print(f"Failed to run compliance tests. Error: {err}")
125+

0 commit comments

Comments
 (0)