Skip to content

Commit 1a8ef80

Browse files
authored
Merge pull request #3807 from mwichmann/runtest-selector
Add selection options to test runner
2 parents 279359e + 1a14204 commit 1a8ef80

File tree

1 file changed

+80
-81
lines changed

1 file changed

+80
-81
lines changed

runtest.py

Lines changed: 80 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,20 @@
66
#
77
# The SCons test suite consists of:
88
#
9-
# - unit tests - included in *Tests.py files from src/ dir
9+
# - unit tests - included in *Tests.py files from SCons/ dir
1010
# - end-to-end tests - these are *.py files in test/ directory that
1111
# require custom SCons framework from testing/
1212
#
1313
# This script adds SCons/ and testing/ directories to PYTHONPATH,
1414
# performs test discovery and processes them according to options.
15-
#
16-
# With -p (--package) option, script tests specified package from
17-
# build directory and sets PYTHONPATH to reference modules unpacked
18-
# during build process for testing purposes (build/test-*).
1915

2016
"""
2117
Options:
2218
-a --all Run all tests.
2319
-b --baseline BASE Run test scripts against baseline BASE.
24-
--builddir DIR Directory in which packages were built.
2520
-d --debug Run test scripts under the Python debugger.
2621
-D --devmode Run tests in Python's development mode (3.7+ only)
22+
--e2e-only Run only the end-to-end tests
2723
-e --external Run the script in external mode (for external Tools)
2824
-f --file FILE Only run tests listed in FILE.
2925
-j --jobs JOBS Run tests in JOBS parallel jobs.
@@ -37,15 +33,6 @@
3733
chars! You might run into some deadlocks else.
3834
-o --output FILE Save the output from a test run to the log file.
3935
-P PYTHON Use the specified Python interpreter.
40-
-p --package PACKAGE Test against the specified PACKAGE:
41-
deb Debian
42-
local-tar-gz .tar.gz standalone package
43-
local-zip .zip standalone package
44-
rpm Red Hat
45-
src-tar-gz .tar.gz source package
46-
src-zip .zip source package
47-
tar-gz .tar.gz distribution
48-
zip .zip distribution
4936
--passed Summarize which tests passed.
5037
-q --quiet Don't print the test being executed.
5138
--quit-on-failure Quit on any test failure.
@@ -54,6 +41,7 @@
5441
and a percentage value, based on the total and
5542
current number of tests.
5643
-t --time Print test execution time.
44+
--unit-only Run only the unit tests
5745
-v VERSION Specify the SCons version.
5846
--verbose=LEVEL Set verbose level:
5947
1 = print executed commands,
@@ -87,29 +75,27 @@
8775
cwd = os.getcwd()
8876

8977
baseline = None
90-
builddir = os.path.join(cwd, 'build')
9178
external = 0
9279
devmode = False
9380
debug = ''
9481
execute_tests = True
9582
jobs = 1
9683
list_only = False
9784
printcommand = True
98-
package = None
9985
print_passed_summary = False
10086
scons = None
10187
scons_exec = False
10288
testlistfile = None
10389
version = ''
10490
print_times = False
10591
python = None
106-
sp = []
10792
print_progress = True
10893
catch_output = False
10994
suppress_output = False
11095
allow_pipe_files = True
11196
quit_on_failure = False
11297
excludelistfile = None
98+
e2e_only = unit_only = False
11399

114100
script = sys.argv[0].split("/")[-1]
115101
usagestr = """\
@@ -153,7 +139,6 @@ def _process_short_opts(self, rargs, values):
153139
"b:dDef:hj:klnP:p:qsv:Xx:t",
154140
[
155141
"baseline=",
156-
"builddir=",
157142
"debug",
158143
"devmode",
159144
"external",
@@ -164,7 +149,6 @@ def _process_short_opts(self, rargs, values):
164149
"list",
165150
"no-exec",
166151
"nopipefiles",
167-
"package=",
168152
"passed",
169153
"python=",
170154
"quiet",
@@ -175,16 +159,14 @@ def _process_short_opts(self, rargs, values):
175159
"exec=",
176160
"verbose=",
177161
"exclude-list=",
162+
"e2e-only",
163+
"unit-only",
178164
],
179165
)
180166

181167
for o, a in opts:
182168
if o in ['-b', '--baseline']:
183169
baseline = a
184-
elif o in ['--builddir']:
185-
builddir = a
186-
if not os.path.isabs(builddir):
187-
builddir = os.path.normpath(os.path.join(cwd, builddir))
188170
elif o in ['-d', '--debug']:
189171
for d in sys.path:
190172
pdb = os.path.join(d, 'pdb.py')
@@ -215,8 +197,6 @@ def _process_short_opts(self, rargs, values):
215197
execute_tests = False
216198
elif o in ['--nopipefiles']:
217199
allow_pipe_files = False
218-
elif o in ['-p', '--package']:
219-
package = a
220200
elif o in ['--passed']:
221201
print_passed_summary = True
222202
elif o in ['-P', '--python']:
@@ -241,9 +221,13 @@ def _process_short_opts(self, rargs, values):
241221
scons = a
242222
elif o in ['--exclude-list']:
243223
excludelistfile = a
224+
elif o in ['--e2e-only']:
225+
e2e_only = True
226+
elif o in ['--unit-only']:
227+
unit_only = True
244228

245229

246-
class Unbuffered():
230+
class Unbuffered:
247231
""" class to arrange for stdout/stderr to be unbuffered """
248232
def __init__(self, file):
249233
self.file = file
@@ -261,7 +245,7 @@ def __getattr__(self, attr):
261245

262246
if options.output:
263247
logfile = open(options.output, 'w')
264-
class Tee():
248+
class Tee:
265249
def __init__(self, openfile, stream):
266250
self.file = openfile
267251
self.stream = stream
@@ -296,13 +280,9 @@ def whereis(file):
296280
return f
297281
return None
298282

299-
sp.append(builddir)
300-
sp.append(cwd)
301283

302-
#
303284
_ws = re.compile(r'\s')
304285

305-
306286
def escape(s):
307287
if _ws.search(s):
308288
s = '"' + s + '"'
@@ -414,7 +394,7 @@ class PopenExecutor(RuntestBase):
414394
by calling subprocess.run (behind the covers uses Popen.
415395
Very similar to SystemExecutor, but uses command_str
416396
instead of command_args, and doesn't allow for not catching
417-
the output.
397+
the output).
418398
"""
419399
# For an explanation of the following 'if ... else'
420400
# and the 'allow_pipe_files' option, please check out the
@@ -553,13 +533,38 @@ def footer(self, f):
553533

554534

555535
# ---[ test discovery ]------------------------------------
536+
# This section figures which tests to run.
537+
#
538+
# The initial testlist is made by reading from the testlistfile,
539+
# if supplied, or by looking at the test arguments, if supplied,
540+
# or by looking for all test files if the "all" argument is supplied.
541+
# One of the three is required.
542+
#
543+
# Each test path, whichever of the three sources it comes from,
544+
# specifies either a test file or a directory to search for
545+
# SCons tests. SCons code layout assumes that any file under the 'SCons'
546+
# subdirectory that ends with 'Tests.py' is a unit test, and any Python
547+
# script (*.py) under the 'test' subdirectory is an end-to-end test.
548+
# We need to track these because they are invoked differently.
549+
# find_unit_tests and find_e2e_tests are used for this searching.
550+
#
551+
# Note that there are some tests under 'SCons' that *begin* with
552+
# 'test_', but they're packaging and installation tests, not
553+
# functional tests, so we don't execute them by default. (They can
554+
# still be executed by hand, though).
555+
#
556+
# Test exclusions, if specified, are then applied.
556557

557-
tests = []
558-
excludetests = []
559558
unittests = []
560559
endtests = []
561560

562561

562+
def scanlist(testlist):
563+
"""Process a testlist file"""
564+
tests = [t.strip() for t in testlist if not t.startswith('#')]
565+
return [t for t in tests if t]
566+
567+
563568
def find_unit_tests(directory):
564569
""" Look for unit tests """
565570
result = []
@@ -583,7 +588,7 @@ def find_e2e_tests(directory):
583588
continue
584589
try:
585590
with open(os.path.join(dirpath, ".exclude_tests")) as f:
586-
excludes = [e.split("#", 1)[0].strip() for e in f.readlines()]
591+
excludes = scanlist(f)
587592
except EnvironmentError:
588593
excludes = []
589594
for fname in filenames:
@@ -592,27 +597,12 @@ def find_e2e_tests(directory):
592597
return sorted(result)
593598

594599

600+
# initial selection:
595601
if testlistfile:
596602
with open(testlistfile, 'r') as f:
597-
tests = f.readlines()
598-
tests = [x for x in tests if x[0] != '#']
599-
tests = [x[:-1] for x in tests]
600-
tests = [x.strip() for x in tests]
601-
tests = [x for x in tests if x]
603+
tests = scanlist(f)
602604
else:
603605
testpaths = []
604-
605-
# Each test path specifies a test file, or a directory to search for
606-
# SCons tests. SCons code layout assumes that any file under the 'SCons'
607-
# subdirectory that ends with 'Tests.py' is a unit test, and any Python
608-
# script (*.py) under the 'test' subdirectory an end-to-end test.
609-
# We need to track these because they are invoked differently.
610-
#
611-
# Note that there are some tests under 'SCons' that *begin* with
612-
# 'test_', but they're packaging and installation tests, not
613-
# functional tests, so we don't execute them by default. (They can
614-
# still be executed by hand, though).
615-
616606
if options.all:
617607
testpaths = ['SCons', 'test']
618608
elif args:
@@ -622,47 +612,44 @@ def find_e2e_tests(directory):
622612
# Clean up path so it can match startswith's below
623613
# sys.stderr.write("Changed:%s->"%tp)
624614
# remove leading ./ or .\
625-
if tp[0] == '.' and tp[1] in (os.sep, os.altsep):
615+
if tp.startswith('.') and tp[1] in (os.sep, os.altsep):
626616
tp = tp[2:]
627617
# tp = os.path.normpath(tp)
628618
# sys.stderr.write('->%s<-'%tp)
629619
# sys.stderr.write("to:%s\n"%tp)
630620
for path in glob.glob(tp):
631621
if os.path.isdir(path):
632-
if path.startswith('SCons') or path.startswith('testing'):
633-
for p in find_unit_tests(path):
634-
unittests.append(p)
622+
if path.startswith(('SCons', 'testing')):
623+
unittests.extend(find_unit_tests(path))
635624
elif path.startswith('test'):
636-
for p in find_e2e_tests(path):
637-
endtests.append(p)
625+
endtests.extend(find_e2e_tests(path))
638626
else:
639627
if path.endswith("Tests.py"):
640628
unittests.append(path)
641-
else:
629+
elif path.endswith(".py"):
642630
endtests.append(path)
631+
tests = unittests + endtests
643632

644-
tests.extend(unittests)
645-
tests.extend(endtests)
646-
tests.sort()
633+
# Remove exclusions:
634+
if e2e_only:
635+
tests = [t for t in tests if not t.endswith("Tests.py")]
636+
if unit_only:
637+
tests = [t for t in tests if t.endswith("Tests.py")]
638+
if excludelistfile:
639+
with open(excludelistfile, 'r') as f:
640+
excludetests = scanlist(f)
641+
tests = [t for t in tests if t not in excludetests]
647642

648643
if not tests:
649644
sys.stderr.write(usagestr + """
650-
runtest.py: No tests were found.
651-
Tests can be specified on the command line, read from file
652-
with -f option, or discovered with -a to run all tests.
645+
runtest: no tests were found.
646+
Tests can be specified on the command line, read from a file with
647+
the -f/--file option, or discovered with -a/--all to run all tests.
653648
""")
654649
sys.exit(1)
655650

656-
if excludelistfile:
657-
with open(excludelistfile, 'r') as f:
658-
excludetests = f.readlines()
659-
excludetests = [x for x in excludetests if x[0] != '#']
660-
excludetests = [x[:-1] for x in excludetests]
661-
excludetests = [x.strip() for x in excludetests]
662-
excludetests = [x for x in excludetests if x]
663651

664652
# ---[ test processing ]-----------------------------------
665-
tests = [t for t in tests if t not in excludetests]
666653
tests = [Test(t, n + 1) for n, t in enumerate(tests)]
667654

668655
if list_only:
@@ -697,10 +684,9 @@ def log_result(t, io_lock=None):
697684
we need to lock access to the log to avoid interleaving. The same
698685
would apply if output was a file.
699686
700-
:param t: a completed testcase
701-
:type t: Test
702-
:param io_lock:
703-
:type io_lock: threading.Lock
687+
Args:
688+
t (Test): (completed) testcase instance
689+
io_lock (threading.lock): (optional) lock to use
704690
"""
705691

706692
# there is no lock in single-job run, which includes
@@ -727,6 +713,18 @@ def log_result(t, io_lock=None):
727713

728714

729715
def run_test(t, io_lock=None, run_async=True):
716+
""" Run a testcase.
717+
718+
Builds the command line to give to execute().
719+
Also the best place to record some information that will be
720+
used in output, which in some conditions is printed here.
721+
722+
Args:
723+
t (Test): testcase instance
724+
io_lock (threading.Lock): (optional) lock to use
725+
run_async (bool): whether to run asynchronously
726+
"""
727+
730728
t.headline = ""
731729
command_args = []
732730
if debug:
@@ -738,7 +736,7 @@ def run_test(t, io_lock=None, run_async=True):
738736
# For example --runner TestUnit.TAPTestRunner
739737
command_args.append('--runner ' + options.runner)
740738
t.command_args = [escape(python)] + command_args
741-
t.command_str = " ".join([escape(python)] + command_args)
739+
t.command_str = " ".join(t.command_args)
742740
if printcommand:
743741
if print_progress:
744742
t.headline += "%d/%d (%.2f%s) %s\n" % (
@@ -753,7 +751,7 @@ def run_test(t, io_lock=None, run_async=True):
753751
if not suppress_output and not catch_output:
754752
# defer printing the headline until test is done
755753
sys.stdout.write(t.headline)
756-
head, tail = os.path.split(t.abspath)
754+
head, _ = os.path.split(t.abspath)
757755
fixture_dirs = []
758756
if head:
759757
fixture_dirs.append(head)
@@ -775,7 +773,7 @@ class RunTest(threading.Thread):
775773
"""
776774
def __init__(self, queue=None, io_lock=None,
777775
group=None, target=None, name=None, args=(), kwargs=None):
778-
super(RunTest, self).__init__(group=group, target=target, name=name)
776+
super().__init__(group=group, target=target, name=name)
779777
self.queue = queue
780778
self.io_lock = io_lock
781779

@@ -856,6 +854,7 @@ def run(self):
856854
if fail:
857855
sys.exit(1)
858856
elif no_result:
857+
# if no fails, but skips were found
859858
sys.exit(2)
860859
else:
861860
sys.exit(0)

0 commit comments

Comments
 (0)