Skip to content

Commit e554de7

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 4819b7b commit e554de7

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
@@ -23,6 +23,7 @@
2323
import lnt.testing
2424
import lnt.testing.profile
2525
import lnt.testing.util.compilers
26+
import lnt.util.ImportData
2627
from lnt.testing.util.misc import timestamp
2728
from lnt.testing.util.commands import fatal
2829
from lnt.testing.util.commands import mkdir_p
@@ -185,6 +186,44 @@ def __init__(self):
185186

186187
def run_test(self, opts):
187188

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

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

217263
if opts.test_suite_externals:
218264
if not os.path.exists(opts.test_suite_externals):
@@ -240,20 +286,23 @@ def run_test(self, opts):
240286
opts.only_test = opts.single_result
241287

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

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

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

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

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

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

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

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

338430
report = self._create_merged_report(reports)
339431

@@ -361,14 +453,124 @@ def run_test(self, opts):
361453

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

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

371-
def run(self, cmake_vars, compile=True, test=True, profile=False):
573+
def run(self, cmake_vars, compile=True, test=True, profile=False, skip_lit=False):
372574
mkdir_p(self._base_path)
373575

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

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

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

0 commit comments

Comments
 (0)