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