From e7b9dfac98d30eb4ea5489c10b8dfef3bb8331f4 Mon Sep 17 00:00:00 2001 From: Charles Brunet Date: Mon, 3 Apr 2023 09:46:26 -0400 Subject: [PATCH] mtest: wildcard selection Allow the use of wildcards (e.g. *) to match test names in `meson test`. Raise an error is given test name does not match any test. Optimize the search by looping through the list of tests only once. --- docs/markdown/Commands.md | 6 +++ docs/markdown/Unit-tests.md | 21 ++++++++++ docs/markdown/snippets/test_name_filters.md | 9 ++++ mesonbuild/mtest.py | 46 +++++++++++++++++---- unittests/allplatformstests.py | 24 +++++++++++ 5 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 docs/markdown/snippets/test_name_filters.md diff --git a/docs/markdown/Commands.md b/docs/markdown/Commands.md index 52eb76c1d..75b22817d 100644 --- a/docs/markdown/Commands.md +++ b/docs/markdown/Commands.md @@ -281,6 +281,12 @@ Run tests for the configure Meson project. See [the unit test documentation](Unit-tests.md) for more info. +Since *1.2.0* you can use wildcards in *args* for test names. +For example, "bas*" will match all test with names beginning with "bas". + +Since *1.2.0* it is an error to provide a test name or wildcard that +does not match any test. + #### Examples: Run tests for the project: diff --git a/docs/markdown/Unit-tests.md b/docs/markdown/Unit-tests.md index 36fbdcfc1..18ab11165 100644 --- a/docs/markdown/Unit-tests.md +++ b/docs/markdown/Unit-tests.md @@ -153,6 +153,27 @@ Specify test(s) by name like: $ meson test A D ``` +You can run tests from specific (sub)project: + +```console +$ meson test (sub)project_name: +``` + +or a specific test in a specific project: + +```console +$ meson test (sub)project_name:test_name +``` + +Since version *1.2.0*, you can use wildcards in project +and test names. For instance, to run all tests beginning with +"foo" and all tests from projects beginning with "bar": + +```console +$ meson test "foo*" "bar*:" +``` + + Tests belonging to a suite `suite` can be run as follows ```console diff --git a/docs/markdown/snippets/test_name_filters.md b/docs/markdown/snippets/test_name_filters.md new file mode 100644 index 000000000..14e62a9d5 --- /dev/null +++ b/docs/markdown/snippets/test_name_filters.md @@ -0,0 +1,9 @@ +## Wildcards in list of tests to run + +The `meson test` command now accepts wildcards in the list of test names. +For example `meson test basic*` will run all tests whose name begins +with "basic". + +meson will report an error if the given test name does not match any +existing test. meson will log a warning if two redundant test names +are given (for example if you give both "proj:basic" and "proj:"). diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py index cbf1c193d..63041bd2c 100644 --- a/mesonbuild/mtest.py +++ b/mesonbuild/mtest.py @@ -19,6 +19,7 @@ from pathlib import Path from collections import deque from contextlib import suppress from copy import deepcopy +from fnmatch import fnmatch import argparse import asyncio import datetime @@ -1859,17 +1860,48 @@ class TestHarness: run all tests with that name across all subprojects, which is identical to "meson test foo1" ''' + patterns: T.Dict[T.Tuple[str, str], bool] = {} for arg in self.options.args: + # Replace empty components by wildcards: + # '' -> '*:*' + # 'name' -> '*:name' + # ':name' -> '*:name' + # 'proj:' -> 'proj:*' if ':' in arg: subproj, name = arg.split(':', maxsplit=1) + if name == '': + name = '*' + if subproj == '': # in case arg was ':' + subproj = '*' else: - subproj, name = '', arg - for t in tests: - if subproj and t.project_name != subproj: - continue - if name and t.name != name: - continue - yield t + subproj, name = '*', arg + patterns[(subproj, name)] = False + + for t in tests: + # For each test, find the first matching pattern + # and mark it as used. yield the matching tests. + for subproj, name in list(patterns): + if fnmatch(t.project_name, subproj) and fnmatch(t.name, name): + patterns[(subproj, name)] = True + yield t + break + + for (subproj, name), was_used in patterns.items(): + if not was_used: + # For each unused pattern... + arg = f'{subproj}:{name}' + for t in tests: + # ... if it matches a test, then it wasn't used because another + # pattern matched the same test before. + # Report it as a warning. + if fnmatch(t.project_name, subproj) and fnmatch(t.name, name): + mlog.warning(f'{arg} test name is redundant and was not used') + break + else: + # If the pattern doesn't match any test, + # report it as an error. We don't want the `test` command to + # succeed on an invalid pattern. + raise MesonException(f'{arg} test name does not match any test') def get_tests(self) -> T.List[TestSerialisation]: if not self.tests: diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py index b04ee2f25..6f9185732 100644 --- a/unittests/allplatformstests.py +++ b/unittests/allplatformstests.py @@ -818,6 +818,30 @@ class AllPlatformTests(BasePlatformTests): o = self._run(self.mtest_command + ['--list', '--no-rebuild']) self.assertNotIn('Regenerating build files.', o) + def test_unexisting_test_name(self): + testdir = os.path.join(self.unit_test_dir, '4 suite selection') + self.init(testdir) + self.build() + + self.assertRaises(subprocess.CalledProcessError, self._run, self.mtest_command + ['notatest']) + + def test_select_test_using_wildcards(self): + testdir = os.path.join(self.unit_test_dir, '4 suite selection') + self.init(testdir) + self.build() + + o = self._run(self.mtest_command + ['--list', 'mainprj*']) + self.assertIn('mainprj-failing_test', o) + self.assertIn('mainprj-successful_test_no_suite', o) + self.assertNotIn('subprj', o) + + o = self._run(self.mtest_command + ['--list', '*succ*', 'subprjm*:']) + self.assertIn('mainprj-successful_test_no_suite', o) + self.assertIn('subprjmix-failing_test', o) + self.assertIn('subprjmix-successful_test', o) + self.assertIn('subprjsucc-successful_test_no_suite', o) + self.assertNotIn('subprjfail-failing_test', o) + def test_build_by_default(self): testdir = os.path.join(self.common_test_dir, '129 build by default') self.init(testdir)