diff --git a/README b/README index 70921ac0..13a179f4 100644 --- a/README +++ b/README @@ -414,10 +414,11 @@ Configuration ------------- Specifics of ``btest``'s execution can be tuned with a configuration -file, which by default is ``btest.cfg`` if that's found in the -current directory. It can alternatively be specified with the -``--config`` command line option, or a ``BTEST_CFG`` environment -variable. The configuration file is +file, which by default is ``btest.cfg`` if that's found in the current +directory or parent directories. It can alternatively be specified +with the ``--config`` command line option (which will not search +parent directories), or a ``BTEST_CFG`` environment variable. +The configuration file is "INI-style", and an example comes with the distribution, see ``btest.cfg.example``. A configuration file has one main section, ``btest``, that defines most options; as well as an optional section diff --git a/btest b/btest index b977f1d2..95a54a02 100755 --- a/btest +++ b/btest @@ -61,10 +61,9 @@ VERSION = "1.2-9" # Automatically filled in. Name = "btest" Config = None -try: - ConfigDefault = os.environ["BTEST_CFG"] -except KeyError: - ConfigDefault = "btest.cfg" +DEFAULT_CONFIG_NAME = "btest.cfg" + +ConfigDefault = os.environ.get("BTEST_CFG", DEFAULT_CONFIG_NAME) def normalize_path(path): @@ -2446,7 +2445,7 @@ class LinuxTimer(TimerBase): # Walk the given directory and return all test files. -def findTests(paths, expand_globs=False): +def findTests(paths, *, cwd=None, expand_globs=False): tests = [] ignore_files = getOption("IgnoreFiles", "").split() @@ -2455,14 +2454,20 @@ def findTests(paths, expand_globs=False): expanded = set() for p in paths: - p = os.path.join(TestBase, p) + anchored = os.path.join(TestBase, p) if expand_globs: - for d in glob.glob(p): + for d in glob.glob(anchored): if os.path.isdir(d): expanded.add(d) else: - expanded.add(p) + # Allow relative directories if this one does not exist + if cwd and not os.path.exists(anchored): + from_cwd = cwd / p + if from_cwd.exists(): + anchored = str(from_cwd) + + expanded.add(anchored) for path in expanded: rpath = os.path.relpath(path, TestBase) @@ -2708,6 +2713,27 @@ def outputDocumentation(tests, fmt): print() +# Finds the config file 'filename', recursing to parent directories from the +# 'start_dir' if not found. +def find_config_file(filename, *, start_dir, recurse): + # First check current dir + f = pathlib.Path(filename) + if f.is_file(): + return str(f.absolute()) + + if not recurse: + return None + + current_dir = start_dir.absolute() + + for d in [current_dir, *current_dir.parents]: + f = d / filename + if f.is_file(): + return str(f) + + return None + + def parse_options(): optparser = optparse.OptionParser( usage="%prog [options] ", version=VERSION @@ -2950,9 +2976,6 @@ def parse_options(): warning("ignoring requested parallelism in interactive-update mode") options.threads = 1 - if not os.path.exists(options.config): - error(f"configuration file '{options.config}' not found") - return options, parsed_args @@ -2991,18 +3014,52 @@ if __name__ == "__main__": (Options, args) = parse_options() + found_configs = set() + + if args: + for arg in args: + if found_config := find_config_file( + Options.config, + start_dir=pathlib.Path(arg).absolute(), + recurse=Options.config == DEFAULT_CONFIG_NAME, + ): + found_configs.add(found_config) + else: + if found_config := find_config_file( + Options.config, + start_dir=pathlib.Path.cwd(), + recurse=Options.config == DEFAULT_CONFIG_NAME, + ): + found_configs.add(found_config) + + if len(found_configs) == 0: + error(f"configuration file '{Options.config}' not found") + + if len(found_configs) > 1: + conflicting_configs = ", ".join( + sorted(f"'{config}'" for config in found_configs) + ) + error( + f"cannot execute tests using different configuration files together: {conflicting_configs}" + ) + + btest_cfg = found_configs.pop() + + dirname = os.path.dirname(btest_cfg) + + # Special case for providing just a single path to btest that happens + # to be the TestBase. Reset args, assuming the user wants to run all tests. + if len(args) == 1 and os.path.abspath(args[0]) == dirname: + args = [] + # The defaults come from environment variables, plus a few additional items. defaults = {} # Changes to defaults should not change os.environ defaults.update(os.environ) defaults["default_path"] = os.environ["PATH"] - dirname = os.path.dirname(Options.config) - if not dirname: - dirname = os.getcwd() - # If the BTEST_TEST_BASE envirnoment var is set, we'll use that as the testbase. - # If not, we'll use the current directory. + # If not, we'll use the config file's directory. TestBase = normalize_path(os.environ.get("BTEST_TEST_BASE", dirname)) defaults["testbase"] = TestBase defaults["baselinedir"] = normalize_path( @@ -3012,7 +3069,7 @@ if __name__ == "__main__": # Parse our config Config = getcfgparser(defaults) - Config.read(Options.config, encoding="utf-8") + Config.read(btest_cfg, encoding="utf-8") defaults["baselinedir"] = getOption("BaselineDir", defaults["baselinedir"]) @@ -3044,7 +3101,7 @@ if __name__ == "__main__": # At this point, our defaults have changed, so we # reread the configuration. new_config = getcfgparser(defaults) - new_config.read(Options.config) + new_config.read(btest_cfg) return new_config, value return Config, default @@ -3066,6 +3123,7 @@ if __name__ == "__main__": transform=lambda x: normalize_path(x), ) + orig_cwd = pathlib.Path.cwd() os.chdir(TestBase) if Options.sphinx: @@ -3201,13 +3259,13 @@ if __name__ == "__main__": testdirs = getOption("TestDirs", "").split() if testdirs: - Config.configured_tests = findTests(testdirs, True) + Config.configured_tests = findTests(testdirs, expand_globs=True) if args: if Options.tests_file: error("cannot specify tests both on command line and with --tests-file") - tests = findTests(args) + tests = findTests(args, cwd=orig_cwd) else: if Options.rerun: diff --git a/testing/Baseline/tests.btest-cfg/nopath b/testing/Baseline/tests.btest-cfg/nopath index cd5e5d2c..2ca40a79 100644 --- a/testing/Baseline/tests.btest-cfg/nopath +++ b/testing/Baseline/tests.btest-cfg/nopath @@ -5,4 +5,4 @@ btest-cfg ... ok all 1 tests successful btest-cfg ... ok all 1 tests successful -configuration file 'btest.cfg' not found +configuration file 'nonexistant' not found diff --git a/testing/Baseline/tests.multiple-cfg-fail/output b/testing/Baseline/tests.multiple-cfg-fail/output new file mode 100644 index 00000000..7b2fdff2 --- /dev/null +++ b/testing/Baseline/tests.multiple-cfg-fail/output @@ -0,0 +1,13 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +## RUNNING NO CFG TEST ALONE +tests.no-cfg.no-cfg ... + > exit 0 +... tests.no-cfg.no-cfg ok +all 1 tests successful +## RUNNING WITH CFG TEST ALONE +tests.with-cfg ... + > exit 0 +... tests.with-cfg ok +all 1 tests successful +## RUNNING TESTS TOGETHER +cannot execute tests using different configuration files together: '<...>/btest.cfg', '<...>/btest.cfg' diff --git a/testing/Baseline/tests.outside-btest-dir/output b/testing/Baseline/tests.outside-btest-dir/output new file mode 100644 index 00000000..30d5d52e --- /dev/null +++ b/testing/Baseline/tests.outside-btest-dir/output @@ -0,0 +1,17 @@ +### BTest baseline data generated by btest-diff. Do not edit. Use "btest -U/-u" to update. Requires BTest >= 0.63. +## RUNNING DIRECTLY ON DIRECTORY WITH CONFIG: +tests.one.a-relative-test ... + > exit 0 +... tests.one.a-relative-test ok +tests.two.another-relative-test ... + > exit 0 +... tests.two.another-relative-test ok +all 2 tests successful +## RUNNING DIRECTLY ON TESTS DIRECTORY: +tests.one.a-relative-test ... + > exit 0 +... tests.one.a-relative-test ok +tests.two.another-relative-test ... + > exit 0 +... tests.two.another-relative-test ok +all 2 tests successful diff --git a/testing/Scripts/diff-remove-abspath b/testing/Scripts/diff-remove-abspath index 93ca4eee..428e16d7 100755 --- a/testing/Scripts/diff-remove-abspath +++ b/testing/Scripts/diff-remove-abspath @@ -2,4 +2,8 @@ # # Replace absolute paths with the basename. -sed 's#[a-zA-Z:]*/\([^/]\{1,\}/\)\{1,\}\([^/]\{1,\}\)#<...>/\2#g' +# The drive letter portion of the Windows regex below is adapted from +# https://github.com/stdlib-js/stdlib/blob/develop/lib/node_modules/%40stdlib/regexp/basename-windows/lib/regexp.js +sed -E 's#/+#/#g' | + sed -E 's#([a-zA-Z]:|[\\/]{2}[^\\/]+[\\/]+[^\\/]+)([\\/])([^ :\\/]{1,}[\\/]){1,}([^ :\\/]{1,})#<...>/\4#g' | + sed -E 's#/([^ :/]{1,}/){1,}([^ :/]{1,})#<...>/\2#g' diff --git a/testing/btest.tests.cfg b/testing/btest.tests.cfg index f4335e52..675d0d5e 100644 --- a/testing/btest.tests.cfg +++ b/testing/btest.tests.cfg @@ -10,6 +10,7 @@ override=normal [btest] TmpDir = `echo .tmp` BaselineDir = %(testbase)s/Baseline +TestDirs = tests [environment] ORIGPATH=%(default_path)s diff --git a/testing/tests/btest-cfg.test b/testing/tests/btest-cfg.test index b89370b9..080dc734 100644 --- a/testing/tests/btest-cfg.test +++ b/testing/tests/btest-cfg.test @@ -2,7 +2,7 @@ # %TEST-EXEC: btest -c myfile %INPUT > nopath 2>&1 # %TEST-EXEC: BTEST_CFG=myfile btest %INPUT >> nopath 2>&1 # %TEST-EXEC: BTEST_CFG=notexist btest -c myfile %INPUT >> nopath 2>&1 -# %TEST-EXEC-FAIL: btest %INPUT >> nopath 2>&1 +# %TEST-EXEC-FAIL: btest -c nonexistant %INPUT >> nopath 2>&1 # %TEST-EXEC: btest-diff nopath # %TEST-EXEC: mkdir z # %TEST-EXEC: mv myfile z/btest.cfg diff --git a/testing/tests/multiple-cfg-fail.test b/testing/tests/multiple-cfg-fail.test new file mode 100644 index 00000000..b518a51c --- /dev/null +++ b/testing/tests/multiple-cfg-fail.test @@ -0,0 +1,20 @@ +# %TEST-DOC: Test that you can run btest from outside of the directory with btest.cfg +# +# %TEST-EXEC: mkdir -p base/tests/no-cfg +# %TEST-EXEC: mkdir -p base/tests/with-cfg/tests +# %TEST-EXEC: cat %INPUT >> base/tests/no-cfg/no-cfg.test +# %TEST-EXEC: mv btest.cfg base/ +# %TEST-EXEC: echo "## RUNNING NO CFG TEST ALONE" >>output +# %TEST-EXEC: btest -v base/tests/no-cfg/no-cfg.test >>output 2>&1 +# +# %TEST-EXEC: cp base/btest.cfg base/tests/with-cfg +# %TEST-EXEC: cat %INPUT >> base/tests/with-cfg/tests/with-cfg.test +# %TEST-EXEC: echo "## RUNNING WITH CFG TEST ALONE" >>output +# %TEST-EXEC: btest -v base/tests/with-cfg/tests/with-cfg.test >>output 2>&1 +# +# But fail together +# %TEST-EXEC: echo "## RUNNING TESTS TOGETHER" >>output +# %TEST-EXEC-FAIL: btest -v base/tests/no-cfg/no-cfg.test base/tests/with-cfg/tests/with-cfg.test >>output 2>&1 +# %TEST-EXEC: TEST_DIFF_CANONIFIER=$SCRIPTS/diff-remove-abspath btest-diff output + +@TEST-EXEC: exit 0 diff --git a/testing/tests/outside-btest-dir.test b/testing/tests/outside-btest-dir.test new file mode 100644 index 00000000..e88bfff8 --- /dev/null +++ b/testing/tests/outside-btest-dir.test @@ -0,0 +1,18 @@ +# %TEST-DOC: Test that you can run btest from outside of the directory with btest.cfg +# +# %TEST-EXEC: mkdir -p base/tests/one +# %TEST-EXEC: mkdir -p base/tests/two +# %TEST-EXEC: cat %INPUT >> base/tests/one/a-relative-test.test +# %TEST-EXEC: cat %INPUT >> base/tests/two/another-relative-test.test +# %TEST-EXEC: mv btest.cfg base/ +# %TEST-EXEC: btest base/tests/one/a-relative-test.test +# %TEST-EXEC: btest base/tests/one/a-relative-test.test base/tests/two/another-relative-test.test +# +# Test the special case of running all tests from outside +# %TEST-EXEC: echo "## RUNNING DIRECTLY ON DIRECTORY WITH CONFIG:" >>output +# %TEST-EXEC: btest -v base/ >>output 2>&1 +# %TEST-EXEC: echo "## RUNNING DIRECTLY ON TESTS DIRECTORY:" >>output +# %TEST-EXEC: btest -v base/tests >>output 2>&1 +# %TEST-EXEC: btest-diff output + +@TEST-EXEC: exit 0 diff --git a/testing/tests/resolve-from-cwd.test b/testing/tests/resolve-from-cwd.test new file mode 100644 index 00000000..c6267860 --- /dev/null +++ b/testing/tests/resolve-from-cwd.test @@ -0,0 +1,8 @@ +# %TEST-DOC: Ensure btest can find tests from a path relative to the current directory +# +# %TEST-EXEC: mkdir -p my/relative/dir +# %TEST-EXEC: cat %INPUT >> my/relative/dir/a-relative-test.test +# %TEST-EXEC: cd my/relative/dir && btest a-relative-test.test +# %TEST-EXEC: cd my/relative && btest dir/a-relative-test.test + +@TEST-EXEC: exit 0