2424import lnt .testing
2525import lnt .testing .profile
2626import lnt .testing .util .compilers
27+ import lnt .util .ImportData
2728from lnt .testing .util .misc import timestamp
2829from lnt .testing .util .commands import fatal
2930from 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