Skip to content

Commit 8c0fbfd

Browse files
committed
Merge remote-tracking branch 'origin/topic/etyp/recurse-cfg-upwards'
* origin/topic/etyp/recurse-cfg-upwards: Allow executing tests from outside the suite Check if current directory has tests before error Check parent directories for `btest.cfg`
2 parents 0220f0a + 28b2f89 commit 8c0fbfd

File tree

14 files changed

+208
-31
lines changed

14 files changed

+208
-31
lines changed

CHANGES

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,40 @@
1+
1.2-15 | 2025-04-30 10:11:52 -0400
2+
3+
* Allow executing tests from outside the suite (Evan Typanski, Corelight)
4+
5+
If btest.cfg is in the `testing/btest` relative directory, this allows
6+
the user to call btest with `btest testing/btest/tests`, which will use
7+
the btest.cfg file in testing/btest. It will also check its parents, so
8+
you may use `btest testing/btest/language/while.zeek` - for example.
9+
10+
The first complication is that previously-valid uses of btest should
11+
remain the same. This is done by always checking the current directory
12+
for btest.cfg first.
13+
14+
The second is if there is a conflict between two tests' btest.cfg file.
15+
For example, the following may choose two different btest.cfg files:
16+
17+
.
18+
└── multiple-cfg
19+
├── Baseline
20+
├── btest.cfg
21+
└── tests
22+
├── no-cfg
23+
│   └── test2.test
24+
└── with-cfg
25+
├── btest.cfg
26+
└── tests
27+
└── test1.test
28+
29+
If there is a conflicting config file, then btest should raise an error,
30+
as it can currently only execute with one config file per run.
31+
32+
* Check if current directory has tests before erroring (Evan Typanski, Corelight)
33+
34+
After this, btest can execute tests relative to the current directory.
35+
36+
* Check parent directories for `btest.cfg` (Evan Typanski, Corelight)
37+
138
1.2-11 | 2025-04-28 11:42:33 +0200
239

340
* Enable lints around return statements (Benjamin Bannier, Corelight)

README

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
..
33
.. Version number is filled in automatically.
44

5-
.. |version| replace:: 1.2-11
5+
.. |version| replace:: 1.2-15
66

77
==================================================
88
BTest - A Generic Driver for Powerful System Tests
@@ -414,10 +414,11 @@ Configuration
414414
-------------
415415

416416
Specifics of ``btest``'s execution can be tuned with a configuration
417-
file, which by default is ``btest.cfg`` if that's found in the
418-
current directory. It can alternatively be specified with the
419-
``--config`` command line option, or a ``BTEST_CFG`` environment
420-
variable. The configuration file is
417+
file, which by default is ``btest.cfg`` if that's found in the current
418+
directory or parent directories. It can alternatively be specified
419+
with the ``--config`` command line option (which will not search
420+
parent directories), or a ``BTEST_CFG`` environment variable.
421+
The configuration file is
421422
"INI-style", and an example comes with the distribution, see
422423
``btest.cfg.example``. A configuration file has one main section,
423424
``btest``, that defines most options; as well as an optional section

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.2-11
1+
1.2-15

btest

Lines changed: 79 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -56,15 +56,14 @@ else:
5656
import multiprocessing.managers as mp_managers
5757
import multiprocessing.sharedctypes as mp_sharedctypes
5858

59-
VERSION = "1.2-11" # Automatically filled in.
59+
VERSION = "1.2-15" # Automatically filled in.
6060

6161
Name = "btest"
6262
Config = None
6363

64-
try:
65-
ConfigDefault = os.environ["BTEST_CFG"]
66-
except KeyError:
67-
ConfigDefault = "btest.cfg"
64+
DEFAULT_CONFIG_NAME = "btest.cfg"
65+
66+
ConfigDefault = os.environ.get("BTEST_CFG", DEFAULT_CONFIG_NAME)
6867

6968

7069
def normalize_path(path):
@@ -2443,7 +2442,7 @@ class LinuxTimer(TimerBase):
24432442

24442443

24452444
# Walk the given directory and return all test files.
2446-
def findTests(paths, expand_globs=False):
2445+
def findTests(paths, *, cwd=None, expand_globs=False):
24472446
tests = []
24482447

24492448
ignore_files = getOption("IgnoreFiles", "").split()
@@ -2452,14 +2451,20 @@ def findTests(paths, expand_globs=False):
24522451
expanded = set()
24532452

24542453
for p in paths:
2455-
p = os.path.join(TestBase, p)
2454+
anchored = os.path.join(TestBase, p)
24562455

24572456
if expand_globs:
2458-
for d in glob.glob(p):
2457+
for d in glob.glob(anchored):
24592458
if os.path.isdir(d):
24602459
expanded.add(d)
24612460
else:
2462-
expanded.add(p)
2461+
# Allow relative directories if this one does not exist
2462+
if cwd and not os.path.exists(anchored):
2463+
from_cwd = cwd / p
2464+
if from_cwd.exists():
2465+
anchored = str(from_cwd)
2466+
2467+
expanded.add(anchored)
24632468

24642469
for path in expanded:
24652470
rpath = os.path.relpath(path, TestBase)
@@ -2705,6 +2710,27 @@ def outputDocumentation(tests, fmt):
27052710
print()
27062711

27072712

2713+
# Finds the config file 'filename', recursing to parent directories from the
2714+
# 'start_dir' if not found.
2715+
def find_config_file(filename, *, start_dir, recurse):
2716+
# First check current dir
2717+
f = pathlib.Path(filename)
2718+
if f.is_file():
2719+
return str(f.absolute())
2720+
2721+
if not recurse:
2722+
return None
2723+
2724+
current_dir = start_dir.absolute()
2725+
2726+
for d in [current_dir, *current_dir.parents]:
2727+
f = d / filename
2728+
if f.is_file():
2729+
return str(f)
2730+
2731+
return None
2732+
2733+
27082734
def parse_options():
27092735
optparser = optparse.OptionParser(
27102736
usage="%prog [options] <directories>", version=VERSION
@@ -2947,9 +2973,6 @@ def parse_options():
29472973
warning("ignoring requested parallelism in interactive-update mode")
29482974
options.threads = 1
29492975

2950-
if not os.path.exists(options.config):
2951-
error(f"configuration file '{options.config}' not found")
2952-
29532976
return options, parsed_args
29542977

29552978

@@ -2988,18 +3011,52 @@ if __name__ == "__main__":
29883011

29893012
(Options, args) = parse_options()
29903013

3014+
found_configs = set()
3015+
3016+
if args:
3017+
for arg in args:
3018+
if found_config := find_config_file(
3019+
Options.config,
3020+
start_dir=pathlib.Path(arg).absolute(),
3021+
recurse=Options.config == DEFAULT_CONFIG_NAME,
3022+
):
3023+
found_configs.add(found_config)
3024+
else:
3025+
if found_config := find_config_file(
3026+
Options.config,
3027+
start_dir=pathlib.Path.cwd(),
3028+
recurse=Options.config == DEFAULT_CONFIG_NAME,
3029+
):
3030+
found_configs.add(found_config)
3031+
3032+
if len(found_configs) == 0:
3033+
error(f"configuration file '{Options.config}' not found")
3034+
3035+
if len(found_configs) > 1:
3036+
conflicting_configs = ", ".join(
3037+
sorted(f"'{config}'" for config in found_configs)
3038+
)
3039+
error(
3040+
f"cannot execute tests using different configuration files together: {conflicting_configs}"
3041+
)
3042+
3043+
btest_cfg = found_configs.pop()
3044+
3045+
dirname = os.path.dirname(btest_cfg)
3046+
3047+
# Special case for providing just a single path to btest that happens
3048+
# to be the TestBase. Reset args, assuming the user wants to run all tests.
3049+
if len(args) == 1 and os.path.abspath(args[0]) == dirname:
3050+
args = []
3051+
29913052
# The defaults come from environment variables, plus a few additional items.
29923053
defaults = {}
29933054
# Changes to defaults should not change os.environ
29943055
defaults.update(os.environ)
29953056
defaults["default_path"] = os.environ["PATH"]
29963057

2997-
dirname = os.path.dirname(Options.config)
2998-
if not dirname:
2999-
dirname = os.getcwd()
3000-
30013058
# If the BTEST_TEST_BASE envirnoment var is set, we'll use that as the testbase.
3002-
# If not, we'll use the current directory.
3059+
# If not, we'll use the config file's directory.
30033060
TestBase = normalize_path(os.environ.get("BTEST_TEST_BASE", dirname))
30043061
defaults["testbase"] = TestBase
30053062
defaults["baselinedir"] = normalize_path(
@@ -3009,7 +3066,7 @@ if __name__ == "__main__":
30093066

30103067
# Parse our config
30113068
Config = getcfgparser(defaults)
3012-
Config.read(Options.config, encoding="utf-8")
3069+
Config.read(btest_cfg, encoding="utf-8")
30133070

30143071
defaults["baselinedir"] = getOption("BaselineDir", defaults["baselinedir"])
30153072

@@ -3041,7 +3098,7 @@ if __name__ == "__main__":
30413098
# At this point, our defaults have changed, so we
30423099
# reread the configuration.
30433100
new_config = getcfgparser(defaults)
3044-
new_config.read(Options.config)
3101+
new_config.read(btest_cfg)
30453102
return new_config, value
30463103

30473104
return Config, default
@@ -3063,6 +3120,7 @@ if __name__ == "__main__":
30633120
transform=lambda x: normalize_path(x),
30643121
)
30653122

3123+
orig_cwd = pathlib.Path.cwd()
30663124
os.chdir(TestBase)
30673125

30683126
if Options.sphinx:
@@ -3198,13 +3256,13 @@ if __name__ == "__main__":
31983256

31993257
testdirs = getOption("TestDirs", "").split()
32003258
if testdirs:
3201-
Config.configured_tests = findTests(testdirs, True)
3259+
Config.configured_tests = findTests(testdirs, expand_globs=True)
32023260

32033261
if args:
32043262
if Options.tests_file:
32053263
error("cannot specify tests both on command line and with --tests-file")
32063264

3207-
tests = findTests(args)
3265+
tests = findTests(args, cwd=orig_cwd)
32083266

32093267
else:
32103268
if Options.rerun:

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
py_modules = ["btest-sphinx"]
66

77
setup(
8-
version="1.2.dev11", # Filled in automatically.
8+
version="1.2.dev15", # Filled in automatically.
99
py_modules=py_modules,
1010
)

testing/Baseline/tests.btest-cfg/nopath

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ btest-cfg ... ok
55
all 1 tests successful
66
btest-cfg ... ok
77
all 1 tests successful
8-
configuration file 'btest.cfg' not found
8+
configuration file 'nonexistant' not found
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
2+
## RUNNING NO CFG TEST ALONE
3+
tests.no-cfg.no-cfg ...
4+
> exit 0
5+
... tests.no-cfg.no-cfg ok
6+
all 1 tests successful
7+
## RUNNING WITH CFG TEST ALONE
8+
tests.with-cfg ...
9+
> exit 0
10+
... tests.with-cfg ok
11+
all 1 tests successful
12+
## RUNNING TESTS TOGETHER
13+
cannot execute tests using different configuration files together: '<...>/btest.cfg', '<...>/btest.cfg'
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63.
2+
## RUNNING DIRECTLY ON DIRECTORY WITH CONFIG:
3+
tests.one.a-relative-test ...
4+
> exit 0
5+
... tests.one.a-relative-test ok
6+
tests.two.another-relative-test ...
7+
> exit 0
8+
... tests.two.another-relative-test ok
9+
all 2 tests successful
10+
## RUNNING DIRECTLY ON TESTS DIRECTORY:
11+
tests.one.a-relative-test ...
12+
> exit 0
13+
... tests.one.a-relative-test ok
14+
tests.two.another-relative-test ...
15+
> exit 0
16+
... tests.two.another-relative-test ok
17+
all 2 tests successful

testing/Scripts/diff-remove-abspath

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@
22
#
33
# Replace absolute paths with the basename.
44

5-
sed 's#[a-zA-Z:]*/\([^/]\{1,\}/\)\{1,\}\([^/]\{1,\}\)#<...>/\2#g'
5+
# The drive letter portion of the Windows regex below is adapted from
6+
# https://github.com/stdlib-js/stdlib/blob/develop/lib/node_modules/%40stdlib/regexp/basename-windows/lib/regexp.js
7+
sed -E 's#/+#/#g' |
8+
sed -E 's#([a-zA-Z]:|[\\/]{2}[^\\/]+[\\/]+[^\\/]+)([\\/])([^ :\\/]{1,}[\\/]){1,}([^ :\\/]{1,})#<...>/\4#g' |
9+
sed -E 's#/([^ :/]{1,}/){1,}([^ :/]{1,})#<...>/\2#g'

testing/btest.tests.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ override=normal
1010
[btest]
1111
TmpDir = `echo .tmp`
1212
BaselineDir = %(testbase)s/Baseline
13+
TestDirs = tests
1314

1415
[environment]
1516
ORIGPATH=%(default_path)s

testing/tests/btest-cfg.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# %TEST-EXEC: btest -c myfile %INPUT > nopath 2>&1
33
# %TEST-EXEC: BTEST_CFG=myfile btest %INPUT >> nopath 2>&1
44
# %TEST-EXEC: BTEST_CFG=notexist btest -c myfile %INPUT >> nopath 2>&1
5-
# %TEST-EXEC-FAIL: btest %INPUT >> nopath 2>&1
5+
# %TEST-EXEC-FAIL: btest -c nonexistant %INPUT >> nopath 2>&1
66
# %TEST-EXEC: btest-diff nopath
77
# %TEST-EXEC: mkdir z
88
# %TEST-EXEC: mv myfile z/btest.cfg

testing/tests/multiple-cfg-fail.test

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# %TEST-DOC: Test that you can run btest from outside of the directory with btest.cfg
2+
#
3+
# %TEST-EXEC: mkdir -p base/tests/no-cfg
4+
# %TEST-EXEC: mkdir -p base/tests/with-cfg/tests
5+
# %TEST-EXEC: cat %INPUT >> base/tests/no-cfg/no-cfg.test
6+
# %TEST-EXEC: mv btest.cfg base/
7+
# %TEST-EXEC: echo "## RUNNING NO CFG TEST ALONE" >>output
8+
# %TEST-EXEC: btest -v base/tests/no-cfg/no-cfg.test >>output 2>&1
9+
#
10+
# %TEST-EXEC: cp base/btest.cfg base/tests/with-cfg
11+
# %TEST-EXEC: cat %INPUT >> base/tests/with-cfg/tests/with-cfg.test
12+
# %TEST-EXEC: echo "## RUNNING WITH CFG TEST ALONE" >>output
13+
# %TEST-EXEC: btest -v base/tests/with-cfg/tests/with-cfg.test >>output 2>&1
14+
#
15+
# But fail together
16+
# %TEST-EXEC: echo "## RUNNING TESTS TOGETHER" >>output
17+
# %TEST-EXEC-FAIL: btest -v base/tests/no-cfg/no-cfg.test base/tests/with-cfg/tests/with-cfg.test >>output 2>&1
18+
# %TEST-EXEC: TEST_DIFF_CANONIFIER=$SCRIPTS/diff-remove-abspath btest-diff output
19+
20+
@TEST-EXEC: exit 0

testing/tests/outside-btest-dir.test

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# %TEST-DOC: Test that you can run btest from outside of the directory with btest.cfg
2+
#
3+
# %TEST-EXEC: mkdir -p base/tests/one
4+
# %TEST-EXEC: mkdir -p base/tests/two
5+
# %TEST-EXEC: cat %INPUT >> base/tests/one/a-relative-test.test
6+
# %TEST-EXEC: cat %INPUT >> base/tests/two/another-relative-test.test
7+
# %TEST-EXEC: mv btest.cfg base/
8+
# %TEST-EXEC: btest base/tests/one/a-relative-test.test
9+
# %TEST-EXEC: btest base/tests/one/a-relative-test.test base/tests/two/another-relative-test.test
10+
#
11+
# Test the special case of running all tests from outside
12+
# %TEST-EXEC: echo "## RUNNING DIRECTLY ON DIRECTORY WITH CONFIG:" >>output
13+
# %TEST-EXEC: btest -v base/ >>output 2>&1
14+
# %TEST-EXEC: echo "## RUNNING DIRECTLY ON TESTS DIRECTORY:" >>output
15+
# %TEST-EXEC: btest -v base/tests >>output 2>&1
16+
# %TEST-EXEC: btest-diff output
17+
18+
@TEST-EXEC: exit 0

testing/tests/resolve-from-cwd.test

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# %TEST-DOC: Ensure btest can find tests from a path relative to the current directory
2+
#
3+
# %TEST-EXEC: mkdir -p my/relative/dir
4+
# %TEST-EXEC: cat %INPUT >> my/relative/dir/a-relative-test.test
5+
# %TEST-EXEC: cd my/relative/dir && btest a-relative-test.test
6+
# %TEST-EXEC: cd my/relative && btest dir/a-relative-test.test
7+
8+
@TEST-EXEC: exit 0

0 commit comments

Comments
 (0)