3 # Copyright The SCons Foundation
5 """runtest - wrapper script for running SCons tests
7 The SCons test suite consists of:
9 * unit tests - *Tests.py files from the SCons/ dir
10 * end-to-end tests - *.py files in the test/ directory that
11 require the custom SCons framework from testing/
13 This script adds SCons/ and testing/ directories to PYTHONPATH,
14 performs test discovery and processes tests according to options.
25 from abc
import ABC
, abstractmethod
26 from io
import StringIO
27 from pathlib
import Path
, PurePath
, PureWindowsPath
28 from queue
import Queue
35 suppress_output
= False
37 script
= PurePath(sys
.argv
[0]).name
39 %(script)s [OPTIONS] [TEST ...]
43 Environment Variables:
44 PRESERVE, PRESERVE_{PASS,FAIL,NO_RESULT}: preserve test subdirs
45 TESTCMD_VERBOSE: turn on verbosity in TestCommand\
48 parser
= argparse
.ArgumentParser(
49 usage
=usagestr
, epilog
=epilogstr
, allow_abbrev
=False,
50 formatter_class
=argparse
.RawDescriptionHelpFormatter
53 # test selection options:
54 testsel
= parser
.add_argument_group(description
='Test selection options:')
55 testsel
.add_argument(metavar
='TEST', nargs
='*', dest
='testlist',
56 help="Select TEST(s) (tests and/or directories) to run")
57 testlisting
= testsel
.add_mutually_exclusive_group()
58 testlisting
.add_argument('-f', '--file', metavar
='FILE', dest
='testlistfile',
59 help="Select only tests in FILE")
60 testlisting
.add_argument('-a', '--all', action
='store_true',
61 help="Select all tests")
62 testlisting
.add_argument('--retry', action
='store_true',
63 help="Rerun the last failed tests in 'failed_tests.log'")
64 testsel
.add_argument('--exclude-list', metavar
="FILE", dest
='excludelistfile',
65 help="""Exclude tests in FILE from current selection""")
66 testtype
= testsel
.add_mutually_exclusive_group()
67 testtype
.add_argument('--e2e-only', action
='store_true',
68 help="Exclude unit tests from selection")
69 testtype
.add_argument('--unit-only', action
='store_true',
70 help="Exclude end-to-end tests from selection")
72 # miscellaneous options
73 parser
.add_argument('-b', '--baseline', metavar
='BASE',
74 help="Run test scripts against baseline BASE.")
75 parser
.add_argument('-d', '--debug', action
='store_true',
76 help="Run test scripts under the Python debugger.")
77 parser
.add_argument('-D', '--devmode', action
='store_true',
78 help="Run tests in Python's development mode (Py3.7+ only).")
79 parser
.add_argument('-e', '--external', action
='store_true',
80 help="Run the script in external mode (for external Tools)")
81 parser
.add_argument('-j', '--jobs', metavar
='JOBS', default
=1, type=int,
82 help="Run tests in JOBS parallel jobs (0 for cpu_count).")
83 parser
.add_argument('-l', '--list', action
='store_true', dest
='list_only',
84 help="List available tests and exit.")
85 parser
.add_argument('-n', '--no-exec', action
='store_false',
87 help="No execute, just print command lines.")
88 parser
.add_argument('--nopipefiles', action
='store_false',
89 dest
='allow_pipe_files',
90 help="""Do not use the "file pipe" workaround for subprocess
91 for starting tests. See source code for warnings.""")
92 parser
.add_argument('-P', '--python', metavar
='PYTHON',
93 help="Use the specified Python interpreter.")
94 parser
.add_argument('--quit-on-failure', action
='store_true',
95 help="Quit on any test failure.")
96 parser
.add_argument('--runner', metavar
='CLASS',
97 help="Test runner class for unit tests.")
98 parser
.add_argument('-X', dest
='scons_exec', action
='store_true',
99 help="Test script is executable, don't feed to Python.")
100 parser
.add_argument('-x', '--exec', metavar
="SCRIPT",
101 help="Test using SCRIPT as path to SCons.")
102 parser
.add_argument('--faillog', dest
='error_log', metavar
="FILE",
103 default
='failed_tests.log',
104 help="Log failed tests to FILE (enabled by default, "
105 "default file 'failed_tests.log')")
106 parser
.add_argument('--no-faillog', dest
='error_log',
107 action
='store_const', const
=None,
108 default
='failed_tests.log',
109 help="Do not log failed tests to a file")
111 parser
.add_argument('--no-ignore-skips', dest
='dont_ignore_skips',
114 help="If any tests are skipped, exit status 2")
116 outctl
= parser
.add_argument_group(description
='Output control options:')
117 outctl
.add_argument('-k', '--no-progress', action
='store_false',
118 dest
='print_progress',
119 help="Suppress count and progress percentage messages.")
120 outctl
.add_argument('--passed', action
='store_true',
121 dest
='print_passed_summary',
122 help="Summarize which tests passed.")
123 outctl
.add_argument('-q', '--quiet', action
='store_false',
125 help="Don't print the test being executed.")
126 outctl
.add_argument('-s', '--short-progress', action
='store_true',
127 help="""Short progress, prints only the command line
128 and a progress percentage.""")
129 outctl
.add_argument('-t', '--time', action
='store_true', dest
='print_times',
130 help="Print test execution time.")
131 outctl
.add_argument('--verbose', metavar
='LEVEL', type=int, choices
=range(1, 4),
132 help="""Set verbose level
133 (1=print executed commands,
134 2=print commands and non-zero output,
135 3=print commands and all output).""")
137 # outctl.add_argument('--version', action='version', version='%s 1.0' % script)
139 logctl
= parser
.add_argument_group(description
='Log control options:')
140 logctl
.add_argument('-o', '--output', metavar
='LOG', help="Save console output to LOG.")
141 logctl
.add_argument('--xml', metavar
='XML', help="Save results to XML in SCons XML format.")
143 # process args and handle a few specific cases:
144 args
= parser
.parse_args()
146 # we can't do this check with an argparse exclusive group, since those
147 # only work with optional args, and the cmdline tests (args.testlist)
148 # are not optional args,
149 if args
.testlist
and (args
.testlistfile
or args
.all
or args
.retry
):
151 parser
.format_usage()
152 + "error: command line tests cannot be combined with -f/--file, -a/--all or --retry\n"
157 args
.testlistfile
= 'failed_tests.log'
159 if args
.testlistfile
:
160 # args.testlistfile changes from a string to a pathlib Path object
162 p
= Path(args
.testlistfile
)
163 # TODO simplify when Py3.5 dropped
164 if sys
.version_info
.major
== 3 and sys
.version_info
.minor
< 6:
165 args
.testlistfile
= p
.resolve()
167 args
.testlistfile
= p
.resolve(strict
=True)
168 except FileNotFoundError
:
170 parser
.format_usage()
171 + 'error: -f/--file testlist file "%s" not found\n' % p
175 if args
.excludelistfile
:
176 # args.excludelistfile changes from a string to a pathlib Path object
178 p
= Path(args
.excludelistfile
)
179 # TODO simplify when Py3.5 dropped
180 if sys
.version_info
.major
== 3 and sys
.version_info
.minor
< 6:
181 args
.excludelistfile
= p
.resolve()
183 args
.excludelistfile
= p
.resolve(strict
=True)
184 except FileNotFoundError
:
186 parser
.format_usage()
187 + 'error: --exclude-list file "%s" not found\n' % p
193 # on Linux, check available rather then physical CPUs
194 args
.jobs
= len(os
.sched_getaffinity(0))
195 except AttributeError:
197 args
.jobs
= os
.cpu_count()
202 parser
.format_usage()
203 + "Unable to detect CPU count, give -j a non-zero value\n"
207 if args
.jobs
> 1 or args
.output
:
208 # 1. don't let tests write stdout/stderr directly if multi-job,
209 # else outputs will interleave and be hard to read.
210 # 2. If we're going to write a logfile, we also need to catch the output.
213 if not args
.printcommand
:
214 suppress_output
= catch_output
= True
217 os
.environ
['TESTCMD_VERBOSE'] = str(args
.verbose
)
219 if args
.short_progress
:
220 args
.print_progress
= True
221 suppress_output
= catch_output
= True
224 # TODO: add a way to pass a specific debugger
230 # --- setup stdout/stderr ---
232 def __init__(self
, file):
235 def write(self
, arg
):
239 def __getattr__(self
, attr
):
240 return getattr(self
.file, attr
)
242 sys
.stdout
= Unbuffered(sys
.stdout
)
243 sys
.stderr
= Unbuffered(sys
.stderr
)
245 # possible alternative: switch to using print, and:
246 # print = functools.partial(print, flush)
250 def __init__(self
, openfile
, stream
):
254 def write(self
, data
):
255 self
.file.write(data
)
256 self
.stream
.write(data
)
258 def flush(self
, data
):
259 self
.file.flush(data
)
260 self
.stream
.flush(data
)
262 logfile
= open(args
.output
, 'w')
263 # this is not ideal: we monkeypatch stdout/stderr a second time
264 # (already did for Unbuffered), so here we can't easily detect what
265 # state we're in on closedown. Just hope it's okay...
266 sys
.stdout
= Tee(logfile
, sys
.stdout
)
267 sys
.stderr
= Tee(logfile
, sys
.stderr
)
269 # --- define helpers ----
270 if sys
.platform
== 'win32':
271 # thanks to Eryk Sun for this recipe
274 shlwapi
= ctypes
.OleDLL('shlwapi')
275 shlwapi
.AssocQueryStringW
.argtypes
= (
276 ctypes
.c_ulong
, # flags
277 ctypes
.c_ulong
, # str
278 ctypes
.c_wchar_p
, # pszAssoc
279 ctypes
.c_wchar_p
, # pszExtra
280 ctypes
.c_wchar_p
, # pszOut
281 ctypes
.POINTER(ctypes
.c_ulong
), # pcchOut
284 ASSOCF_NOTRUNCATE
= 0x00000020
285 ASSOCF_INIT_IGNOREUNKNOWN
= 0x00000400
287 ASSOCSTR_EXECUTABLE
= 2
288 E_POINTER
= ctypes
.c_long(0x80004003).value
290 def get_template_command(filetype
, verb
=None):
291 flags
= ASSOCF_INIT_IGNOREUNKNOWN | ASSOCF_NOTRUNCATE
292 assoc_str
= ASSOCSTR_COMMAND
293 cch
= ctypes
.c_ulong(260)
295 buf
= (ctypes
.c_wchar
* cch
.value
)()
297 shlwapi
.AssocQueryStringW(
298 flags
, assoc_str
, filetype
, verb
, buf
, ctypes
.byref(cch
)
301 if e
.winerror
!= E_POINTER
:
309 # Without any output suppressed, we let the subprocess
310 # write its stuff freely to stdout/stderr.
312 def spawn_it(command_args
, env
):
313 cp
= subprocess
.run(command_args
, shell
=False, env
=env
)
314 return cp
.stdout
, cp
.stderr
, cp
.returncode
317 # Else, we catch the output of both pipes...
318 if args
.allow_pipe_files
:
319 # The subprocess.Popen() suffers from a well-known
320 # problem. Data for stdout/stderr is read into a
321 # memory buffer of fixed size, 65K which is not very much.
322 # When it fills up, it simply stops letting the child process
323 # write to it. The child will then sit and patiently wait to
324 # be able to write the rest of its output. Hang!
325 # In order to work around this, we follow a suggestion
326 # by Anders Pearson in
327 # https://thraxil.org/users/anders/posts/2008/03/13/Subprocess-Hanging-PIPE-is-your-enemy/
328 # and pass temp file objects to Popen() instead of the ubiquitous
331 def spawn_it(command_args
, env
):
332 # Create temporary files
333 tmp_stdout
= tempfile
.TemporaryFile(mode
='w+t')
334 tmp_stderr
= tempfile
.TemporaryFile(mode
='w+t')
335 # Start subprocess...
345 # Rewind to start of files
349 spawned_stdout
= tmp_stdout
.read()
350 spawned_stderr
= tmp_stderr
.read()
352 # Remove temp files by closing them
357 return spawned_stderr
, spawned_stdout
, cp
.returncode
360 # We get here only if the user gave the '--nopipefiles'
361 # option, meaning the "temp file" approach for
362 # subprocess.communicate() above shouldn't be used.
363 # He hopefully knows what he's doing, but again we have a
364 # potential deadlock situation in the following code:
365 # If the subprocess writes a lot of data to its stderr,
366 # the pipe will fill up (nobody's reading it yet) and the
367 # subprocess will wait for someone to read it.
368 # But the parent process is trying to read from stdin
369 # (but the subprocess isn't writing anything there).
371 # Be dragons here! Better don't use this!
373 def spawn_it(command_args
, env
):
376 stdout
=subprocess
.PIPE
,
377 stderr
=subprocess
.PIPE
,
381 return cp
.stdout
, cp
.stderr
, cp
.returncode
384 class RuntestBase(ABC
):
385 """ Base class for tests """
386 _ids
= itertools
.count(1) # to geenerate test # automatically
388 def __init__(self
, path
, spe
=None):
389 self
.path
= str(path
)
390 self
.testno
= next(self
._ids
)
391 self
.stdout
= self
.stderr
= self
.status
= None
392 self
.abspath
= path
.absolute()
393 self
.command_args
= []
394 self
.command_str
= ""
395 self
.test_time
= self
.total_time
= 0
398 f
= os
.path
.join(d
, path
)
399 if os
.path
.isfile(f
):
404 def execute(self
, env
):
408 class SystemExecutor(RuntestBase
):
409 """ Test class for tests executed with spawn_it() """
410 def execute(self
, env
):
411 self
.stderr
, self
.stdout
, s
= spawn_it(self
.command_args
, env
)
414 sys
.stdout
.write("Unexpected exit status %d\n" % s
)
417 class PopenExecutor(RuntestBase
):
418 """ Test class for tests executed with Popen
420 A bit of a misnomer as the Popen call is now wrapped
421 by calling subprocess.run (behind the covers uses Popen.
422 Very similar to SystemExecutor, but doesn't allow for not catching
425 # For an explanation of the following 'if ... else'
426 # and the 'allow_pipe_files' option, please check out the
427 # definition of spawn_it() above.
428 if args
.allow_pipe_files
:
430 def execute(self
, env
):
431 # Create temporary files
432 tmp_stdout
= tempfile
.TemporaryFile(mode
='w+t')
433 tmp_stderr
= tempfile
.TemporaryFile(mode
='w+t')
434 # Start subprocess...
442 self
.status
= cp
.returncode
445 # Rewind to start of files
449 self
.stdout
= tmp_stdout
.read()
450 self
.stderr
= tmp_stderr
.read()
452 # Remove temp files by closing them
457 def execute(self
, env
):
460 stdout
=subprocess
.PIPE
,
461 stderr
=subprocess
.PIPE
,
465 self
.status
, self
.stdout
, self
.stderr
= cp
.returncode
, cp
.stdout
, cp
.stderr
467 class XML(PopenExecutor
):
468 """ Test class for tests that will output in scons xml """
471 f
.write(' <results>\n')
475 f
.write(' <file_name>%s</file_name>\n' % self
.path
)
476 f
.write(' <command_line>%s</command_line>\n' % self
.command_str
)
477 f
.write(' <exit_status>%s</exit_status>\n' % self
.status
)
478 f
.write(' <stdout>%s</stdout>\n' % self
.stdout
)
479 f
.write(' <stderr>%s</stderr>\n' % self
.stderr
)
480 f
.write(' <time>%.1f</time>\n' % self
.test_time
)
481 f
.write(' </test>\n')
484 f
.write(' <time>%.1f</time>\n' % self
.total_time
)
485 f
.write(' </results>\n')
490 Test
= SystemExecutor
492 # --- start processing ---
494 if not args
.baseline
or args
.baseline
== '.':
496 elif args
.baseline
== '-':
497 print("This logic used to checkout from svn. It's been removed. If you used this, please let us know on devel mailing list, IRC, or discord server")
500 baseline
= args
.baseline
501 scons_runtest_dir
= baseline
503 if not args
.external
:
504 scons_script_dir
= os
.path
.join(baseline
, 'scripts')
505 scons_tools_dir
= os
.path
.join(baseline
, 'bin')
506 scons_lib_dir
= baseline
508 scons_script_dir
= ''
513 'SCONS_RUNTEST_DIR': scons_runtest_dir
,
514 'SCONS_TOOLS_DIR': scons_tools_dir
,
515 'SCONS_SCRIPT_DIR': scons_script_dir
,
520 # Let the version of SCons that the -x option pointed to find
522 testenv
['SCONS'] = scons
524 # Because SCons is really aggressive about finding its modules,
525 # it sometimes finds SCons modules elsewhere on the system.
526 # This forces SCons to use the modules that are being tested.
527 testenv
['SCONS_LIB_DIR'] = scons_lib_dir
530 testenv
['SCONS_EXEC'] = '1'
533 testenv
['SCONS_EXTERNAL_TEST'] = '1'
535 # Insert scons path and path for testing framework to PYTHONPATH
536 scriptpath
= os
.path
.dirname(os
.path
.realpath(__file__
))
537 frameworkpath
= os
.path
.join(scriptpath
, 'testing', 'framework')
538 testenv
['PYTHONPATH'] = os
.pathsep
.join((scons_lib_dir
, frameworkpath
))
539 pythonpath
= os
.environ
.get('PYTHONPATH')
541 testenv
['PYTHONPATH'] = testenv
['PYTHONPATH'] + os
.pathsep
+ pythonpath
543 if sys
.platform
== 'win32':
544 # Windows doesn't support "shebang" lines directly (the Python launcher
545 # and Windows Store version do, but you have to get them launched first)
546 # so to directly launch a script we depend on an assoc for .py to work.
547 # Some systems may have none, and in some cases IDE programs take over
548 # the assoc. Detect this so the small number of tests affected can skip.
550 python_assoc
= get_template_command('.py')
553 if not python_assoc
or "py" not in python_assoc
:
554 testenv
['SCONS_NO_DIRECT_SCRIPT'] = '1'
556 os
.environ
.update(testenv
)
558 # Clear _JAVA_OPTIONS which java tools output to stderr when run breaking tests
559 if '_JAVA_OPTIONS' in os
.environ
:
560 del os
.environ
['_JAVA_OPTIONS']
563 # ---[ test discovery ]------------------------------------
564 # This section figures out which tests to run.
566 # The initial testlist is made by reading from the testlistfile,
567 # if supplied, or by looking at the test arguments, if supplied,
568 # or by looking for all test files if the "all" argument is supplied.
569 # One of the three is required.
571 # Each test path, whichever of the three sources it comes from,
572 # specifies either a test file or a directory to search for
573 # SCons tests. SCons code layout assumes that any file under the 'SCons'
574 # subdirectory that ends with 'Tests.py' is a unit test, and any Python
575 # script (*.py) under the 'test' subdirectory is an end-to-end test.
576 # We need to track these because they are invoked differently.
577 # find_unit_tests and find_e2e_tests are used for this searching.
579 # Note that there are some tests under 'SCons' that *begin* with
580 # 'test_', but they're packaging and installation tests, not
581 # functional tests, so we don't execute them by default. (They can
582 # still be executed by hand, though).
584 # Test exclusions, if specified, are then applied.
587 def scanlist(testfile
):
588 """ Process a testlist file """
589 data
= StringIO(testfile
.read_text())
590 tests
= [t
.strip() for t
in data
.readlines() if not t
.startswith('#')]
591 # in order to allow scanned lists to work whether they use forward or
592 # backward slashes, first create the object as a PureWindowsPath which
593 # accepts either, then use that to make a Path object to use for
594 # comparisons like "file in scanned_list".
595 if sys
.platform
== 'win32':
596 return [Path(t
) for t
in tests
if t
]
598 return [Path(PureWindowsPath(t
).as_posix()) for t
in tests
if t
]
601 def find_unit_tests(directory
):
602 """ Look for unit tests """
604 for dirpath
, dirnames
, filenames
in os
.walk(directory
):
605 # Skip folders containing a sconstest.skip file
606 if 'sconstest.skip' in filenames
:
608 for fname
in filenames
:
609 if fname
.endswith("Tests.py"):
610 result
.append(Path(dirpath
, fname
))
612 return sorted(result
)
615 def find_e2e_tests(directory
):
616 """ Look for end-to-end tests """
618 for dirpath
, dirnames
, filenames
in os
.walk(directory
):
619 # Skip folders containing a sconstest.skip file
620 if 'sconstest.skip' in filenames
:
623 # Slurp in any tests in exclude lists
625 if ".exclude_tests" in filenames
:
626 excludefile
= Path(dirpath
, ".exclude_tests").resolve()
627 excludes
= scanlist(excludefile
)
629 for fname
in filenames
:
630 if fname
.endswith(".py") and Path(fname
) not in excludes
:
631 result
.append(Path(dirpath
, fname
))
633 return sorted(result
)
637 # if we have a testlist file read that, else hunt for tests.
640 if args
.testlistfile
:
641 tests
= scanlist(args
.testlistfile
)
644 if args
.all
: # -a flag
645 testpaths
= [Path('SCons'), Path('test')]
646 elif args
.testlist
: # paths given on cmdline
647 if sys
.platform
== 'win32':
648 testpaths
= [Path(t
) for t
in args
.testlist
]
650 testpaths
= [Path(PureWindowsPath(t
).as_posix()) for t
in args
.testlist
]
652 for path
in testpaths
:
653 # Clean up path removing leading ./ or .\
655 if name
.startswith('.') and name
[1] in (os
.sep
, os
.altsep
):
656 path
= path
.with_name(tn
[2:])
660 if path
.parts
[0] == "SCons" or path
.parts
[0] == "testing":
661 unittests
.extend(find_unit_tests(path
))
662 elif path
.parts
[0] == 'test':
663 endtests
.extend(find_e2e_tests(path
))
664 # else: TODO: what if user pointed to a dir outside scons tree?
666 if path
.match("*Tests.py"):
667 unittests
.append(path
)
668 elif path
.match("*.py"):
669 endtests
.append(path
)
671 tests
= sorted(unittests
+ endtests
)
675 tests
= [t
for t
in tests
if not t
.match("*Tests.py")]
677 tests
= [t
for t
in tests
if t
.match("*Tests.py")]
678 if args
.excludelistfile
:
679 excludetests
= scanlist(args
.excludelistfile
)
680 tests
= [t
for t
in tests
if t
not in excludetests
]
682 # did we end up with any tests?
684 sys
.stderr
.write(parser
.format_usage() + """
685 error: no tests matching the specification were found.
686 See "Test selection options" in the help for details on
687 how to specify and/or exclude tests.
691 # ---[ test processing ]-----------------------------------
692 tests
= [Test(t
) for t
in tests
]
700 if os
.name
== 'java':
701 args
.python
= os
.path
.join(sys
.prefix
, 'jython')
703 args
.python
= sys
.executable
704 os
.environ
["python_executable"] = args
.python
708 def print_time(fmt
, tm
):
713 def print_time(fmt
, tm
):
716 time_func
= time
.perf_counter
717 total_start_time
= time_func()
718 total_num_tests
= len(tests
)
721 def log_result(t
, io_lock
=None):
722 """ log the result of a test.
724 "log" in this case means writing to stdout. Since we might be
725 called from from any of several different threads (multi-job run),
726 we need to lock access to the log to avoid interleaving. The same
727 would apply if output was a file.
730 t (Test): (completed) testcase instance
731 io_lock (threading.lock): (optional) lock to use
734 # there is no lock in single-job run, which includes
735 # running test/runtest tests from multi-job run, so check.
739 if suppress_output
or catch_output
:
740 sys
.stdout
.write(t
.headline
)
741 if not suppress_output
:
746 print_time("Test execution time: %.1f seconds", t
.test_time
)
751 if args
.quit_on_failure
and t
.status
== 1:
752 print("Exiting due to error")
757 def run_test(t
, io_lock
=None, run_async
=True):
760 Builds the command line to give to execute().
761 Also the best place to record some information that will be
762 used in output, which in some conditions is printed here.
765 t (Test): testcase instance
766 io_lock (threading.Lock): (optional) lock to use
767 run_async (bool): whether to run asynchronously
773 command_args
.extend(['-m', debug
])
774 if args
.devmode
and sys
.version_info
>= (3, 7, 0):
775 command_args
.append('-X dev')
776 command_args
.append(t
.path
)
777 if args
.runner
and t
.path
in unittests
:
778 # For example --runner TestUnit.TAPTestRunner
779 command_args
.append('--runner ' + args
.runner
)
780 t
.command_args
= [args
.python
] + command_args
781 t
.command_str
= " ".join(t
.command_args
)
782 if args
.printcommand
:
783 if args
.print_progress
:
784 t
.headline
+= "%d/%d (%.2f%s) %s\n" % (
785 t
.testno
, total_num_tests
,
786 float(t
.testno
) * 100.0 / float(total_num_tests
),
791 t
.headline
+= t
.command_str
+ "\n"
792 if not suppress_output
and not catch_output
:
793 # defer printing the headline until test is done
794 sys
.stdout
.write(t
.headline
)
795 head
, _
= os
.path
.split(t
.abspath
)
798 fixture_dirs
.append(head
)
799 fixture_dirs
.append(os
.path
.join(scriptpath
, 'test', 'fixture'))
801 # Set the list of fixture dirs directly in the environment. Just putting
802 # it in os.environ and spawning the process is racy. Make it reliable by
803 # overriding the environment passed to execute().
804 env
= dict(os
.environ
)
805 env
['FIXTURE_DIRS'] = os
.pathsep
.join(fixture_dirs
)
807 test_start_time
= time_func()
808 if args
.execute_tests
:
811 t
.test_time
= time_func() - test_start_time
812 log_result(t
, io_lock
=io_lock
)
815 class RunTest(threading
.Thread
):
816 """ Test Runner class.
818 One instance will be created for each job thread in multi-job mode
820 def __init__(self
, queue
=None, io_lock
=None, group
=None, target
=None, name
=None):
821 super().__init
__(group
=group
, target
=target
, name
=name
)
823 self
.io_lock
= io_lock
826 for t
in iter(self
.queue
.get
, None):
827 run_test(t
, io_lock
=self
.io_lock
, run_async
=True)
828 self
.queue
.task_done()
831 print("Running tests using %d jobs" % args
.jobs
)
835 testlock
= threading
.Lock()
836 # Start worker threads to consume the queue
837 threads
= [RunTest(queue
=testq
, io_lock
=testlock
) for _
in range(args
.jobs
)]
841 # wait on the queue rather than the individual threads
845 run_test(t
, io_lock
=None, run_async
=False)
847 # --- all tests are complete by the time we get here ---
849 tests
[0].total_time
= time_func() - total_start_time
850 print_time("Total execution time for all tests: %.1f seconds", tests
[0].total_time
)
852 passed
= [t
for t
in tests
if t
.status
== 0]
853 fail
= [t
for t
in tests
if t
.status
== 1]
854 no_result
= [t
for t
in tests
if t
.status
== 2]
856 # print summaries, but only if multiple tests were run
857 if len(tests
) != 1 and args
.execute_tests
:
858 if passed
and args
.print_passed_summary
:
860 sys
.stdout
.write("\nPassed the following test:\n")
862 sys
.stdout
.write("\nPassed the following %d tests:\n" % len(passed
))
863 paths
= [x
.path
for x
in passed
]
864 sys
.stdout
.write("\t" + "\n\t".join(paths
) + "\n")
867 sys
.stdout
.write("\nFailed the following test:\n")
869 sys
.stdout
.write("\nFailed the following %d tests:\n" % len(fail
))
870 paths
= [x
.path
for x
in fail
]
871 sys
.stdout
.write("\t" + "\n\t".join(paths
) + "\n")
873 if len(no_result
) == 1:
874 sys
.stdout
.write("\nNO RESULT from the following test:\n")
876 sys
.stdout
.write("\nNO RESULT from the following %d tests:\n" % len(no_result
))
877 paths
= [x
.path
for x
in no_result
]
878 sys
.stdout
.write("\t" + "\n\t".join(paths
) + "\n")
880 # save the fails to a file
882 with
open(args
.error_log
, "w") as f
:
884 paths
= [x
.path
for x
in fail
]
887 # if there are no fails, file will be cleared
890 if args
.output
== '-':
893 f
= open(args
.xml
, 'w')
895 #f.write("test_result = [\n")
900 if args
.output
!= '-':
904 if isinstance(sys
.stdout
, Tee
):
905 sys
.stdout
.file.close()
906 if isinstance(sys
.stderr
, Tee
):
907 sys
.stderr
.file.close()
911 elif no_result
and args
.dont_ignore_skips
:
912 # if no fails, but skips were found
919 # indent-tabs-mode:nil
921 # vim: set expandtab tabstop=4 shiftwidth=4: