Skip to content

Commit 1ce4895

Browse files
committed
Make runtest work for external tests again
An earlier rework had caused external directories not to be searched, even if specified in combination with --external. Fixes #4699. Signed-off-by: Mats Wichmann <mats@linux.com>
1 parent be6c181 commit 1ce4895

File tree

3 files changed

+122
-31
lines changed

3 files changed

+122
-31
lines changed

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER
2222
- Extended unittests (crudely) to test for correct/expected response
2323
when default setting is a boolean string.
2424

25+
From Mats Wichmann:
26+
- runtest.py once again finds "external" tests, such as the tests for
27+
tools in scons-contrib. An earlier rework had broken this. Fixes #4699.
28+
2529

2630
RELEASE 4.9.1 - Thu, 27 Mar 2025 11:40:20 -0700
2731

runtest.py

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
from io import StringIO
3030
from pathlib import Path, PurePath, PureWindowsPath
3131
from queue import Queue
32-
from typing import TextIO
3332

3433
cwd = os.getcwd()
3534
debug: str | None = None
@@ -575,38 +574,34 @@ def footer(self, f):
575574
del os.environ['_JAVA_OPTIONS']
576575

577576

578-
# ---[ test discovery ]------------------------------------
579-
# This section figures out which tests to run.
577+
# ---[ Test Discovery ]------------------------------------
578+
# This section determines which tests to run based on three
579+
# mutually exclusive options:
580+
# 1. Reading test paths from a testlist file (--file or --retry option)
581+
# 2. Using test paths given as command line arguments
582+
# 3. Automatically finding all tests (--all option)
580583
#
581-
# The initial testlist is made by reading from the testlistfile,
582-
# if supplied, or by looking at the test arguments, if supplied,
583-
# or by looking for all test files if the "all" argument is supplied.
584-
# One of the three is required.
584+
# Test paths can specify either individual test files, or directories to
585+
# scan for tests. The following test types are recognized:
585586
#
586-
# Each test path, whichever of the three sources it comes from,
587-
# specifies either a test file or a directory to search for
588-
# SCons tests. SCons code layout assumes that any file under the 'SCons'
589-
# subdirectory that ends with 'Tests.py' is a unit test, and any Python
590-
# script (*.py) under the 'test' subdirectory is an end-to-end test.
591-
# We need to track these because they are invoked differently.
592-
# find_unit_tests and find_e2e_tests are used for this searching.
587+
# - Unit tests: Files ending in 'Tests.py' under the 'SCons' directory
588+
# - End-to-end tests: Python scripts (*.py) under the 'test' directory
589+
# - External tests: End-to-end tests in paths containing a 'test'
590+
# component (not expected to be local)
593591
#
594-
# Note that there are some tests under 'SCons' that *begin* with
595-
# 'test_', but they're packaging and installation tests, not
596-
# functional tests, so we don't execute them by default. (They can
597-
# still be executed by hand, though).
592+
# find_unit_tests() and find_e2e_tests() perform the directory scanning.
598593
#
599-
# Test exclusions, if specified, are then applied.
600-
594+
# After the initial test list is built, any test exclusions specified via
595+
# --exclude-list are applied to produce the final test set.
601596

602597
def scanlist(testfile):
603598
""" Process a testlist file """
604599
data = StringIO(testfile.read_text())
605600
tests = [t.strip() for t in data.readlines() if not t.startswith('#')]
606601
# in order to allow scanned lists to work whether they use forward or
607-
# backward slashes, first create the object as a PureWindowsPath which
608-
# accepts either, then use that to make a Path object to use for
609-
# comparisons like "file in scanned_list".
602+
# backward slashes, on non-Windows first create the object as a
603+
# PureWindowsPath which accepts either, then use that to make a Path
604+
# object for use in comparisons like "if file in scanned_list".
610605
if sys.platform == 'win32':
611606
return [Path(t) for t in tests if t]
612607
else:
@@ -635,7 +630,7 @@ def find_e2e_tests(directory):
635630
if 'sconstest.skip' in filenames:
636631
continue
637632

638-
# Slurp in any tests in exclude lists
633+
# Gather up the data from any exclude lists
639634
excludes = []
640635
if ".exclude_tests" in filenames:
641636
excludefile = Path(dirpath, ".exclude_tests").resolve()
@@ -648,8 +643,7 @@ def find_e2e_tests(directory):
648643
return sorted(result)
649644

650645

651-
# initial selection:
652-
# if we have a testlist file read that, else hunt for tests.
646+
# Initial test selection:
653647
unittests = []
654648
endtests = []
655649
if args.testlistfile:
@@ -668,15 +662,16 @@ def find_e2e_tests(directory):
668662
# Clean up path removing leading ./ or .\
669663
name = str(path)
670664
if name.startswith('.') and name[1] in (os.sep, os.altsep):
671-
path = path.with_name(tn[2:])
665+
path = path.with_name(name[2:])
672666

673667
if path.exists():
674668
if path.is_dir():
675669
if path.parts[0] == "SCons" or path.parts[0] == "testing":
676670
unittests.extend(find_unit_tests(path))
677671
elif path.parts[0] == 'test':
678672
endtests.extend(find_e2e_tests(path))
679-
# else: TODO: what if user pointed to a dir outside scons tree?
673+
elif args.external and 'test' in path.parts:
674+
endtests.extend(find_e2e_tests(path))
680675
else:
681676
if path.match("*Tests.py"):
682677
unittests.append(path)
@@ -703,7 +698,7 @@ def find_e2e_tests(directory):
703698
""")
704699
sys.exit(1)
705700

706-
# ---[ test processing ]-----------------------------------
701+
# ---[ Test Processing ]-----------------------------------
707702
tests = [Test(t) for t in tests]
708703

709704
if args.list_only:
@@ -826,10 +821,11 @@ def run_test(t, io_lock=None, run_async=True):
826821

827822

828823
class RunTest(threading.Thread):
829-
""" Test Runner class.
824+
"""Test Runner thread.
830825
831-
One instance will be created for each job thread in multi-job mode
826+
One will be created for each job in multi-job mode
832827
"""
828+
833829
def __init__(self, queue=None, io_lock=None, group=None, target=None, name=None):
834830
super().__init__(group=group, target=target, name=name)
835831
self.queue = queue

test/runtest/external_tests.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env python
2+
#
3+
# MIT License
4+
#
5+
# Copyright The SCons Foundation
6+
#
7+
# Permission is hereby granted, free of charge, to any person obtaining
8+
# a copy of this software and associated documentation files (the
9+
# "Software"), to deal in the Software without restriction, including
10+
# without limitation the rights to use, copy, modify, merge, publish,
11+
# distribute, sublicense, and/or sell copies of the Software, and to
12+
# permit persons to whom the Software is furnished to do so, subject to
13+
# the following conditions:
14+
#
15+
# The above copyright notice and this permission notice shall be included
16+
# in all copies or substantial portions of the Software.
17+
#
18+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
19+
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
20+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25+
26+
"""
27+
Test that external subdirs are searched if --external is given:
28+
29+
python runtest.py --external ext/test/subdir
30+
31+
"""
32+
33+
import os
34+
35+
import TestRuntest
36+
37+
test = TestRuntest.TestRuntest()
38+
test.subdir('ext', ['ext', 'test'], ['ext', 'test', 'subdir'])
39+
40+
pythonstring = TestRuntest.pythonstring
41+
pythonflags = TestRuntest.pythonflags
42+
43+
one = os.path.join('ext', 'test', 'subdir', 'test_one.py')
44+
two = os.path.join('ext', 'test', 'subdir', 'two.py')
45+
three = os.path.join('ext', 'test', 'test_three.py')
46+
47+
test.write_passing_test(['ext', 'test', 'subdir', 'test_one.py'])
48+
test.write_passing_test(['ext', 'test', 'subdir', 'two.py'])
49+
test.write_passing_test(['ext', 'test', 'test_three.py'])
50+
51+
expect_stderr_noarg = """\
52+
usage: runtest.py [OPTIONS] [TEST ...]
53+
54+
error: no tests matching the specification were found.
55+
See "Test selection options" in the help for details on
56+
how to specify and/or exclude tests.
57+
"""
58+
59+
expect_stdout = f"""\
60+
{pythonstring}{pythonflags} {one}
61+
PASSING TEST STDOUT
62+
{pythonstring}{pythonflags} {two}
63+
PASSING TEST STDOUT
64+
"""
65+
66+
expect_stderr = """\
67+
PASSING TEST STDERR
68+
PASSING TEST STDERR
69+
"""
70+
71+
test.run(
72+
arguments='--no-progress ext/test/subdir',
73+
status=1,
74+
stdout=None,
75+
stderr=expect_stderr_noarg,
76+
)
77+
78+
test.run(
79+
arguments='--no-progress --external ext/test/subdir',
80+
status=0,
81+
stdout=expect_stdout,
82+
stderr=expect_stderr,
83+
)
84+
85+
test.pass_test()
86+
87+
# Local Variables:
88+
# tab-width:4
89+
# indent-tabs-mode:nil
90+
# End:
91+
# vim: set expandtab tabstop=4 shiftwidth=4:

0 commit comments

Comments
 (0)