Skip to content

Commit ba9592b

Browse files
committed
Add interleaved benchmark execution for test-suite
This adds support for separating build and test phases, and running benchmarks from multiple builds in an interleaved fashion to control for environmental factors (ambient temperature, general system load etc). New options: * --build-only: Build tests without running them * --test-prebuilt: Run tests from pre-built directory * --build-dir: Specify build directory (used with --test-prebuilt) * --exec-interleaved-builds: Comma-separated list of builds to interleave Usage: 1. Build two compiler versions: lnt runtest test-suite --build-only \ --sandbox /tmp/sandbox-a \ --cc /path/to/clang-a \ --test-suite ~/llvm-test-suite \ ... lnt runtest test-suite --build-only \ --sandbox /tmp/sandbox-b \ --cc /path/to/clang-b \ --test-suite ~/llvm-test-suite \ ... 2. Run with interleaved execution: lnt runtest test-suite \ --sandbox /tmp/results \ --exec-interleaved-builds /tmp/sandbox-a/build,/tmp/sandbox-b/build \ --exec-multisample 3 This runs tests in the pattern: - Sample 0: build-a -> build-b - Sample 1: build-a -> build-b - Sample 2: build-a -> build-b Temporal interleaving controls for environmental changes that could bias results toward one build. Or, test single build: lnt runtest test-suite --test-prebuilt \ --build-dir /tmp/sandbox-a/build \ --exec-multisample 5 Reports are written to each build directory (report.json, test-results.xunit.xml, test-results.csv).
1 parent ed38846 commit ba9592b

File tree

1 file changed

+269
-41
lines changed

1 file changed

+269
-41
lines changed

lnt/tests/test_suite.py

Lines changed: 269 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import lnt.testing
2525
import lnt.testing.profile
2626
import lnt.testing.util.compilers
27+
import lnt.util.ImportData
2728
from lnt.testing.util.misc import timestamp
2829
from lnt.testing.util.commands import fatal
2930
from lnt.testing.util.commands import mkdir_p
@@ -186,6 +187,44 @@ def __init__(self):
186187

187188
def run_test(self, opts):
188189

190+
# Validate new build/test mode options
191+
if opts.build_only and opts.test_prebuilt:
192+
self._fatal("--build-only and --test-prebuilt are mutually exclusive")
193+
194+
if opts.test_prebuilt and opts.build_dir is None and not opts.exec_interleaved_builds:
195+
self._fatal("--test-prebuilt requires --build-dir (or use --exec-interleaved-builds)")
196+
197+
if opts.build_dir and not opts.test_prebuilt and not opts.exec_interleaved_builds:
198+
self._fatal("--build-dir can only be used with --test-prebuilt or --exec-interleaved-builds")
199+
200+
if opts.exec_interleaved_builds:
201+
# --exec-interleaved-builds implies --test-prebuilt
202+
opts.test_prebuilt = True
203+
# Parse and validate build directories
204+
opts.exec_interleaved_builds_list = [
205+
os.path.abspath(d.strip())
206+
for d in opts.exec_interleaved_builds.split(',')
207+
]
208+
for build_dir in opts.exec_interleaved_builds_list:
209+
if not os.path.exists(build_dir):
210+
self._fatal(
211+
"--exec-interleaved-builds directory does not exist: %r" %
212+
build_dir)
213+
cmakecache = os.path.join(build_dir, 'CMakeCache.txt')
214+
if not os.path.exists(cmakecache):
215+
self._fatal(
216+
"--exec-interleaved-builds directory is not a configured build: %r" %
217+
build_dir)
218+
219+
if opts.build_dir:
220+
# Validate build directory
221+
opts.build_dir = os.path.abspath(opts.build_dir)
222+
if not os.path.exists(opts.build_dir):
223+
self._fatal("--build-dir does not exist: %r" % opts.build_dir)
224+
cmakecache = os.path.join(opts.build_dir, 'CMakeCache.txt')
225+
if not os.path.exists(cmakecache):
226+
self._fatal("--build-dir is not a configured build: %r" % opts.build_dir)
227+
189228
if opts.cc is not None:
190229
opts.cc = resolve_command_path(opts.cc)
191230

@@ -207,13 +246,20 @@ def run_test(self, opts):
207246
if not os.path.exists(opts.cxx):
208247
self._fatal("invalid --cxx argument %r, does not exist"
209248
% (opts.cxx))
210-
211-
if opts.test_suite_root is None:
212-
self._fatal('--test-suite is required')
213-
if not os.path.exists(opts.test_suite_root):
214-
self._fatal("invalid --test-suite argument, does not exist: %r" % (
215-
opts.test_suite_root))
216-
opts.test_suite_root = os.path.abspath(opts.test_suite_root)
249+
else:
250+
# If --cc not specified, CMake will use its default compiler discovery
251+
# We'll validate that a compiler was found after configuration
252+
if opts.cc is None and not opts.test_prebuilt:
253+
logger.info("No --cc specified, will use CMake's default compiler discovery")
254+
255+
if not opts.test_prebuilt:
256+
# These are only required when building
257+
if opts.test_suite_root is None:
258+
self._fatal('--test-suite is required')
259+
if not os.path.exists(opts.test_suite_root):
260+
self._fatal("invalid --test-suite argument, does not exist: %r" % (
261+
opts.test_suite_root))
262+
opts.test_suite_root = os.path.abspath(opts.test_suite_root)
217263

218264
if opts.test_suite_externals:
219265
if not os.path.exists(opts.test_suite_externals):
@@ -241,20 +287,23 @@ def run_test(self, opts):
241287
opts.only_test = opts.single_result
242288

243289
if opts.only_test:
244-
# --only-test can either point to a particular test or a directory.
245-
# Therefore, test_suite_root + opts.only_test or
246-
# test_suite_root + dirname(opts.only_test) must be a directory.
247-
path = os.path.join(opts.test_suite_root, opts.only_test)
248-
parent_path = os.path.dirname(path)
249-
250-
if os.path.isdir(path):
251-
opts.only_test = (opts.only_test, None)
252-
elif os.path.isdir(parent_path):
253-
opts.only_test = (os.path.dirname(opts.only_test),
254-
os.path.basename(opts.only_test))
255-
else:
256-
self._fatal("--only-test argument not understood (must be a " +
257-
" test or directory name)")
290+
if not opts.test_prebuilt:
291+
# Only validate against test_suite_root if we're not in test-prebuilt mode
292+
# --only-test can either point to a particular test or a directory.
293+
# Therefore, test_suite_root + opts.only_test or
294+
# test_suite_root + dirname(opts.only_test) must be a directory.
295+
path = os.path.join(opts.test_suite_root, opts.only_test)
296+
parent_path = os.path.dirname(path)
297+
298+
if os.path.isdir(path):
299+
opts.only_test = (opts.only_test, None)
300+
elif os.path.isdir(parent_path):
301+
opts.only_test = (os.path.dirname(opts.only_test),
302+
os.path.basename(opts.only_test))
303+
else:
304+
self._fatal("--only-test argument not understood (must be a " +
305+
" test or directory name)")
306+
# else: in test-prebuilt mode, we'll use only_test as-is for filtering
258307

259308
if opts.single_result and not opts.only_test[1]:
260309
self._fatal("--single-result must be given a single test name, "
@@ -271,25 +320,49 @@ def run_test(self, opts):
271320
self.start_time = timestamp()
272321

273322
# Work out where to put our build stuff
274-
if opts.timestamp_build:
275-
ts = self.start_time.replace(' ', '_').replace(':', '-')
276-
build_dir_name = "test-%s" % ts
323+
if opts.test_prebuilt and opts.build_dir:
324+
# In test-prebuilt mode with --build-dir, use the specified build directory
325+
basedir = opts.build_dir
326+
elif opts.exec_interleaved_builds:
327+
# For exec-interleaved-builds, each build uses its own directory
328+
# We'll return early from _run_interleaved_builds(), so basedir doesn't matter
329+
basedir = opts.sandbox_path
277330
else:
278-
build_dir_name = "build"
279-
basedir = os.path.join(opts.sandbox_path, build_dir_name)
331+
# Normal mode or build-only mode: use sandbox/build or sandbox/test-<timestamp>
332+
if opts.timestamp_build:
333+
ts = self.start_time.replace(' ', '_').replace(':', '-')
334+
build_dir_name = "test-%s" % ts
335+
else:
336+
build_dir_name = "build"
337+
basedir = os.path.join(opts.sandbox_path, build_dir_name)
338+
280339
self._base_path = basedir
281340

282341
cmakecache = os.path.join(self._base_path, 'CMakeCache.txt')
283-
self.configured = not opts.run_configure and \
284-
os.path.exists(cmakecache)
342+
if opts.test_prebuilt:
343+
# In test-prebuilt mode, the build is already configured
344+
self.configured = True
345+
else:
346+
# In normal/build-only mode, check if we should skip reconfiguration
347+
self.configured = not opts.run_configure and \
348+
os.path.exists(cmakecache)
349+
350+
# No additional validation needed - CMake will find default compiler if needed
351+
# The validation after _configure_if_needed() will catch if no compiler found
285352

286353
# If we are doing diagnostics, skip the usual run and do them now.
287354
if opts.diagnose:
288355
return self.diagnose()
289356

290-
# configure, so we can extract toolchain information from the cmake
291-
# output.
292-
self._configure_if_needed()
357+
# Handle exec-interleaved-builds mode separately
358+
if opts.exec_interleaved_builds:
359+
return self._run_interleaved_builds(opts)
360+
361+
# Configure if needed (skip in test-prebuilt mode)
362+
if not opts.test_prebuilt:
363+
# configure, so we can extract toolchain information from the cmake
364+
# output.
365+
self._configure_if_needed()
293366

294367
# Verify that we can actually find a compiler before continuing
295368
cmake_vars = self._extract_cmake_vars_from_cache()
@@ -323,18 +396,37 @@ def run_test(self, opts):
323396
fatal("Cannot detect compiler version. Specify --run-order"
324397
" to manually define it.")
325398

399+
# Handle --build-only mode
400+
if opts.build_only:
401+
logger.info("Building tests (--build-only mode)...")
402+
self.run(cmake_vars, compile=True, test=False, skip_lit=True)
403+
logger.info("Build complete. Build directory: %s" % self._base_path)
404+
logger.info("Use --test-prebuilt --build-dir %s to run tests." % self._base_path)
405+
return lnt.util.ImportData.no_submit()
406+
326407
# Now do the actual run.
327408
reports = []
328409
json_reports = []
329-
for i in range(max(opts.exec_multisample, opts.compile_multisample)):
330-
c = i < opts.compile_multisample
331-
e = i < opts.exec_multisample
332-
# only gather perf profiles on a single run.
333-
p = i == 0 and opts.use_perf in ('profile', 'all')
334-
run_report, json_data = self.run(cmake_vars, compile=c, test=e,
335-
profile=p)
336-
reports.append(run_report)
337-
json_reports.append(json_data)
410+
# In test-prebuilt mode, we only run tests, no compilation
411+
if opts.test_prebuilt:
412+
for i in range(opts.exec_multisample):
413+
# only gather perf profiles on a single run.
414+
p = i == 0 and opts.use_perf in ('profile', 'all')
415+
run_report, json_data = self.run(cmake_vars, compile=False, test=True,
416+
profile=p)
417+
reports.append(run_report)
418+
json_reports.append(json_data)
419+
else:
420+
# Normal mode: build and test
421+
for i in range(max(opts.exec_multisample, opts.compile_multisample)):
422+
c = i < opts.compile_multisample
423+
e = i < opts.exec_multisample
424+
# only gather perf profiles on a single run.
425+
p = i == 0 and opts.use_perf in ('profile', 'all')
426+
run_report, json_data = self.run(cmake_vars, compile=c, test=e,
427+
profile=p)
428+
reports.append(run_report)
429+
json_reports.append(json_data)
338430

339431
report = self._create_merged_report(reports)
340432

@@ -362,14 +454,124 @@ def run_test(self, opts):
362454

363455
return self.submit(report_path, opts, 'nts')
364456

457+
def _run_interleaved_builds(self, opts):
458+
"""Run tests from multiple builds in an interleaved fashion."""
459+
logger.info("Running interleaved builds mode with %d builds" %
460+
len(opts.exec_interleaved_builds_list))
461+
462+
# Collect information about each build
463+
build_infos = []
464+
for build_dir in opts.exec_interleaved_builds_list:
465+
logger.info("Loading build from: %s" % build_dir)
466+
467+
# Temporarily set _base_path and configured to this build directory
468+
saved_base_path = self._base_path
469+
saved_configured = self.configured
470+
self._base_path = build_dir
471+
self.configured = True # Build directories are already configured
472+
473+
# Extract cmake vars from this build
474+
cmake_vars = self._extract_cmake_vars_from_cache()
475+
if "CMAKE_C_COMPILER" not in cmake_vars or \
476+
not os.path.exists(cmake_vars["CMAKE_C_COMPILER"]):
477+
self._fatal(
478+
"Couldn't find C compiler in build %s (%s)." %
479+
(build_dir, cmake_vars.get("CMAKE_C_COMPILER")))
480+
481+
cc_info = self._get_cc_info(cmake_vars)
482+
logger.info(" Compiler: %s %s" % (cc_info['cc_name'], cc_info['cc_build']))
483+
484+
build_infos.append({
485+
'build_dir': build_dir,
486+
'cmake_vars': cmake_vars,
487+
'cc_info': cc_info
488+
})
489+
490+
# Restore _base_path and configured
491+
self._base_path = saved_base_path
492+
self.configured = saved_configured
493+
494+
# Now run tests in interleaved fashion
495+
all_reports = []
496+
all_json_reports = []
497+
498+
for sample_idx in range(opts.exec_multisample):
499+
logger.info("Running sample %d of %d" % (sample_idx + 1, opts.exec_multisample))
500+
501+
for build_idx, build_info in enumerate(build_infos):
502+
logger.info(" Testing build %d/%d: %s" %
503+
(build_idx + 1, len(build_infos), build_info['build_dir']))
504+
505+
# Set _base_path and configured to this build directory
506+
self._base_path = build_info['build_dir']
507+
self.configured = True # Build is already configured, skip reconfiguration
508+
509+
# Run tests (no compilation)
510+
p = sample_idx == 0 and opts.use_perf in ('profile', 'all')
511+
run_report, json_data = self.run(
512+
build_info['cmake_vars'],
513+
compile=False,
514+
test=True,
515+
profile=p
516+
)
517+
518+
all_reports.append(run_report)
519+
all_json_reports.append(json_data)
520+
521+
logger.info("Interleaved testing complete. Generating reports...")
522+
523+
# For now, we'll create separate reports for each build
524+
# Group reports by build
525+
reports_by_build = {}
526+
json_by_build = {}
527+
for i, (report, json_data) in enumerate(zip(all_reports, all_json_reports)):
528+
build_idx = i % len(build_infos)
529+
if build_idx not in reports_by_build:
530+
reports_by_build[build_idx] = []
531+
json_by_build[build_idx] = []
532+
reports_by_build[build_idx].append(report)
533+
json_by_build[build_idx].append(json_data)
534+
535+
# Write reports for each build to its own directory
536+
for build_idx, build_info in enumerate(build_infos):
537+
build_dir = build_info['build_dir']
538+
logger.info("Writing report for build: %s" % build_dir)
539+
540+
# Merge reports for this build
541+
merged_report = self._create_merged_report(reports_by_build[build_idx])
542+
543+
# Write JSON report to build directory
544+
report_path = os.path.join(build_dir, 'report.json')
545+
with open(report_path, 'w') as fd:
546+
fd.write(merged_report.render())
547+
logger.info(" Report: %s" % report_path)
548+
549+
# Write xUnit XML to build directory
550+
xml_path = os.path.join(build_dir, 'test-results.xunit.xml')
551+
str_template = _lit_json_to_xunit_xml(json_by_build[build_idx])
552+
with open(xml_path, 'w') as fd:
553+
fd.write(str_template)
554+
555+
# Write CSV to build directory
556+
csv_path = os.path.join(build_dir, 'test-results.csv')
557+
str_template = _lit_json_to_csv(json_by_build[build_idx])
558+
with open(csv_path, 'w') as fd:
559+
fd.write(str_template)
560+
561+
logger.info("Reports written to each build directory.")
562+
logger.info("To submit results, use 'lnt submit' with each report file.")
563+
564+
# Return no_submit since we have multiple reports
565+
return lnt.util.ImportData.no_submit()
566+
365567
def _configure_if_needed(self):
366568
mkdir_p(self._base_path)
367569
if not self.configured:
368570
self._configure(self._base_path)
369571
self._clean(self._base_path)
370572
self.configured = True
371573

372-
def run(self, cmake_vars, compile=True, test=True, profile=False):
574+
def run(self, cmake_vars, compile=True, test=True, profile=False, skip_lit=False):
373575
mkdir_p(self._base_path)
374576

375577
# FIXME: should we only run PGO collection once, even when
@@ -389,6 +591,9 @@ def run(self, cmake_vars, compile=True, test=True, profile=False):
389591
self._install_benchmark(self._base_path)
390592
self.compiled = True
391593

594+
if skip_lit:
595+
return None, None
596+
392597
data = self._lit(self._base_path, test, profile)
393598
return self._parse_lit_output(self._base_path, data, cmake_vars), data
394599

@@ -1148,6 +1353,29 @@ def diagnose(self):
11481353
is_flag=True, default=False,)
11491354
@click.option("--remote-host", metavar="HOST",
11501355
help="Run tests on a remote machine")
1356+
@click.option("--build-only", "build_only",
1357+
help="Only build the tests, don't run them. Useful for "
1358+
"preparing builds for later interleaved execution.",
1359+
is_flag=True, default=False)
1360+
@click.option("--test-prebuilt", "test_prebuilt",
1361+
help="Only run tests from pre-built directory, skip configure "
1362+
"and build steps. Use with --build-dir to specify the "
1363+
"build directory.",
1364+
is_flag=True, default=False)
1365+
@click.option("--build-dir", "build_dir",
1366+
metavar="PATH",
1367+
help="Path to pre-built test directory (used with --test-prebuilt). "
1368+
"This is the actual build directory (e.g., sandbox/build), "
1369+
"not the sandbox parent directory.",
1370+
type=click.UNPROCESSED, default=None)
1371+
@click.option("--exec-interleaved-builds", "exec_interleaved_builds",
1372+
metavar="BUILD1,BUILD2,...",
1373+
help="Comma-separated list of build directories to interleave "
1374+
"execution from. Implies --test-prebuilt. Each path should be "
1375+
"a build directory (e.g., sandbox/build). For each multisample, "
1376+
"runs all tests from each build in sequence to control for "
1377+
"environmental changes.",
1378+
type=click.UNPROCESSED, default=None)
11511379
# Output Options
11521380
@click.option("--auto-name/--no-auto-name", "auto_name", default=True, show_default=True,
11531381
help="Whether to automatically derive the submission name")

0 commit comments

Comments
 (0)