Skip to content

Commit 9506e7a

Browse files
committed
Refactor test suite & improve start up time
The test suite now runs litani add-job asynchronously. On a 12-core machine, this decreases test startup time by 22x.
2 parents 036a14a + 05962c8 commit 9506e7a

File tree

1 file changed

+97
-58
lines changed

1 file changed

+97
-58
lines changed

test/run

Lines changed: 97 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515

1616

1717
import argparse
18+
import asyncio
19+
import dataclasses
1820
import importlib
1921
import logging
22+
import math
2023
import os
2124
import pathlib
2225
import re
@@ -44,10 +47,11 @@ def get_args():
4447
return pars.parse_args()
4548

4649

47-
def run_cmd(cmd):
48-
try:
49-
subprocess.run([str(c) for c in cmd], check=True)
50-
except subprocess.CalledProcessError:
50+
async def run_cmd(cmd):
51+
proc = await asyncio.create_subprocess_exec(
52+
str(cmd[0]), *[str(arg) for arg in cmd[1:]])
53+
await proc.wait()
54+
if proc.returncode:
5155
logging.error(
5256
"Failed to run command '%s'", " ".join([str(c) for c in cmd]))
5357
sys.exit(1)
@@ -69,48 +73,52 @@ def has_phony_add_jobs(module_file):
6973
return False
7074

7175

72-
def litani_add(litani, counter, *args, **kwargs):
76+
async def litani_add(litani, **kwargs):
7377
cmd = [litani, "add-job"]
74-
for arg in args:
75-
switch = re.sub("_", "-", arg)
76-
cmd.append(f"--{switch}")
7778
for arg, value in kwargs.items():
7879
switch = re.sub("_", "-", arg)
7980
cmd.append(f"--{switch}")
8081
if isinstance(value, list):
8182
cmd.extend(value)
8283
else:
8384
cmd.append(value)
84-
run_cmd(cmd)
85-
counter["added"] += 1
86-
print_counter(counter)
85+
await run_cmd(cmd)
8786

8887

8988
def collapse(string):
9089
return re.sub(r"\s+", " ", string)
9190

9291

93-
def has_line(test_file, line):
94-
with open(test_file) as handle:
95-
for current_line in handle:
96-
if current_line.strip().startswith(line):
97-
return True
98-
return False
92+
93+
@dataclasses.dataclass
94+
class TestFileMethodChecker:
95+
test_file: pathlib.Path
96+
97+
def __call__(self, meth_name):
98+
pat = f"def {meth_name}(" # ) <- for syntax highlighting
99+
with open(self.test_file) as handle:
100+
for line in handle:
101+
if line.strip().startswith(pat):
102+
return True
103+
return False
99104

100105

101-
def add_e2e_tests(litani, test_dir, counter, output_dir, fast):
106+
def enqueue_job(queue, **kwargs):
107+
queue.put_nowait(kwargs)
108+
109+
110+
111+
def add_e2e_tests(test_dir, output_dir, fast, queue, litani):
102112
e2e_test_dir = test_dir / "e2e"
103113
# 4 jobs per test (init, add-jobs, run-build, check-run)
104114
# skip __init__.py and __pycache__
105-
counter["total"] += (len(os.listdir(e2e_test_dir / "tests")) - 2) * 4
106115
sys.path.insert(1, str(e2e_test_dir / "tests"))
116+
107117
for test_file in (e2e_test_dir / "tests").iterdir():
108118
if test_file.name in ["__init__.py", "__pycache__"]:
109119
continue
110120

111-
add_transform_jobs = has_line(test_file, "def transform_jobs(")
112-
add_get_jobs = has_line(test_file, "def check_get_jobs(")
113-
add_set_jobs = has_line(test_file, "def get_set_jobs_args(")
121+
test_has_method = TestFileMethodChecker(test_file)
114122

115123
if fast and is_slow_test(test_file):
116124
continue
@@ -119,8 +127,8 @@ def add_e2e_tests(litani, test_dir, counter, output_dir, fast):
119127

120128
timeout=10 if fast else 0
121129

122-
litani_add(
123-
litani, counter,
130+
enqueue_job(
131+
queue,
124132
command=collapse(f"""
125133
{e2e_test_dir / 'run'}
126134
--test-file {test_file}
@@ -144,8 +152,8 @@ def add_e2e_tests(litani, test_dir, counter, output_dir, fast):
144152
else:
145153
add_jobs_kwargs["outputs"] = [jobs_output]
146154

147-
litani_add(
148-
litani, counter,
155+
enqueue_job(
156+
queue,
149157
command=collapse(f"""
150158
{e2e_test_dir / 'run'}
151159
--test-file {test_file}
@@ -161,10 +169,10 @@ def add_e2e_tests(litani, test_dir, counter, output_dir, fast):
161169
**add_jobs_kwargs)
162170

163171
run_build_dependencies = [add_jobs_output]
164-
if add_transform_jobs:
172+
if test_has_method("transform_jobs"):
165173
transform_jobs_output = str(uuid.uuid4())
166-
litani_add(
167-
litani, counter,
174+
enqueue_job(
175+
queue,
168176
command=collapse(f"""
169177
{e2e_test_dir / 'run'}
170178
--test-file {test_file}
@@ -179,10 +187,10 @@ def add_e2e_tests(litani, test_dir, counter, output_dir, fast):
179187
cwd=run_dir)
180188
run_build_dependencies.append(transform_jobs_output)
181189

182-
if add_get_jobs:
190+
if test_has_method("check_get_jobs"):
183191
get_jobs_output = str(uuid.uuid4())
184-
litani_add(
185-
litani, counter,
192+
enqueue_job(
193+
queue,
186194
command=collapse(f"""
187195
{e2e_test_dir / 'run'}
188196
--test-file {test_file}
@@ -197,10 +205,10 @@ def add_e2e_tests(litani, test_dir, counter, output_dir, fast):
197205
cwd=run_dir)
198206
run_build_dependencies.append(get_jobs_output)
199207

200-
if add_set_jobs:
208+
if test_has_method("get_set_jobs_args"):
201209
set_jobs_output = str(uuid.uuid4())
202-
litani_add(
203-
litani, counter,
210+
enqueue_job(
211+
queue,
204212
command=collapse(f"""
205213
{e2e_test_dir / 'run'}
206214
--test-file {test_file}
@@ -215,8 +223,8 @@ def add_e2e_tests(litani, test_dir, counter, output_dir, fast):
215223
cwd=run_dir)
216224
run_build_dependencies.append(set_jobs_output)
217225

218-
litani_add(
219-
litani, counter,
226+
enqueue_job(
227+
queue,
220228
command=collapse(f"""
221229
{e2e_test_dir / 'run'}
222230
--test-file {test_file}
@@ -231,8 +239,8 @@ def add_e2e_tests(litani, test_dir, counter, output_dir, fast):
231239
cwd=run_dir,
232240
timeout=timeout)
233241

234-
litani_add(
235-
litani, counter,
242+
enqueue_job(
243+
queue,
236244
command=collapse(f"""
237245
{e2e_test_dir / 'run'}
238246
--test-file {test_file}
@@ -247,25 +255,55 @@ def add_e2e_tests(litani, test_dir, counter, output_dir, fast):
247255
timeout=timeout)
248256

249257

250-
def add_unit_tests(litani, test_dir, root_dir, counter):
258+
def add_unit_tests(test_dir, root_dir, queue):
251259
for fyle in (test_dir / "unit").iterdir():
252260
if fyle.name in ["__init__.py", "__pycache__"]:
253261
continue
254-
litani_add(
255-
litani, counter,
262+
enqueue_job(
263+
queue,
256264
command=f"python3 -m unittest test.unit.{fyle.stem}",
257265
pipeline="Unit tests",
258266
ci_stage="test",
259267
description=fyle.stem,
260268
cwd=root_dir)
261-
counter["total"] += 1
262269

263270

264-
def print_counter(counter):
265-
print("\r{added} / {total} tests added".format(**counter), end="")
271+
272+
@dataclasses.dataclass
273+
class Counter:
274+
total: int
275+
added: int = 0
276+
width: int = 0
266277

267278

268-
def main():
279+
def __post_init__(self):
280+
self.width = int(math.log10(self.total)) + 1
281+
282+
283+
def bump(self):
284+
self.added += 1
285+
msg = "\r{added:{width}}/{total:{width}} tests added".format(
286+
added=self.added, total=self.total, width=self.width)
287+
print(msg, end="", file=sys.stderr)
288+
289+
290+
291+
async def drain_job_queue(queue, counter, litani):
292+
while True:
293+
args = await queue.get()
294+
await litani_add(litani, **args)
295+
counter.bump()
296+
queue.task_done()
297+
298+
299+
def get_pool_size():
300+
ret = os.cpu_count()
301+
if ret is None or ret < 4:
302+
return 1
303+
return ret - 2
304+
305+
306+
async def main():
269307
args = get_args()
270308
logging.basicConfig(format="\nrun-tests: %(message)s")
271309
test_dir = pathlib.Path(__file__).resolve().parent
@@ -276,24 +314,25 @@ def main():
276314
output_dir.mkdir(exist_ok=True, parents=True)
277315
os.chdir(output_dir)
278316

279-
run_cmd([
317+
await run_cmd([
280318
litani, "init",
281319
"--project", "Litani Test Suite",
282320
"--output-prefix", ".",
283321
"--output-symlink", "latest"])
284322

285-
counter = {
286-
"added": 0,
287-
"total": 0,
288-
}
289-
290-
add_unit_tests(litani, test_dir, root, counter)
291-
add_e2e_tests(
292-
litani, test_dir, counter, output_dir, args.fast)
293-
print()
323+
job_queue = asyncio.Queue()
324+
add_unit_tests(test_dir, root, job_queue)
325+
add_e2e_tests(test_dir, output_dir, args.fast, job_queue, litani)
294326

295-
run_cmd([litani, "run-build", "--fail-on-pipeline-failure"])
327+
counter = Counter(job_queue.qsize())
328+
tasks = []
329+
for _ in range(get_pool_size()):
330+
task = asyncio.create_task(drain_job_queue(job_queue, counter, litani))
331+
tasks.append(task)
332+
await job_queue.join()
333+
print("", file=sys.stderr)
334+
await run_cmd([litani, "run-build", "--fail-on-pipeline-failure"])
296335

297336

298337
if __name__ == "__main__":
299-
main()
338+
asyncio.run(main())

0 commit comments

Comments
 (0)