Skip to content

Commit 48ca5f9

Browse files
authored
Merge pull request #4704 from mwichmann/runtest/external
Make runtest work for external tests again
2 parents be6c181 + 541edc4 commit 48ca5f9

File tree

4 files changed

+124
-32
lines changed

4 files changed

+124
-32
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

RELEASE.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ DOCUMENTATION
5757
DEVELOPMENT
5858
-----------
5959

60-
- List visible changes in the way SCons is developed
60+
- runtest.py once again finds "external" tests, such as the tests for
61+
tools in scons-contrib. An earlier rework had broken this. Fixes #4699.
6162

6263
Thanks to the following contributors listed below for their contributions to this release.
6364
==========================================================================================

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/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(one)
48+
test.write_passing_test(two)
49+
test.write_passing_test(three)
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)