diff --git a/tmt-recipe-tool/README.md b/tmt-recipe-tool/README.md index e8c2acd..9eb15ea 100644 --- a/tmt-recipe-tool/README.md +++ b/tmt-recipe-tool/README.md @@ -43,15 +43,16 @@ tmt-recipe-tool [OPTIONS] COMMAND [ARGS]... #### `filter-tests` -Filter recipe tests by their result outcome, keeping only those that match. +Filter recipe tests by their result outcome or name, keeping only those that match. ``` -tmt-recipe-tool -i RECIPE filter-tests [--use-reportportal] [--result RESULT]... +tmt-recipe-tool -i RECIPE filter-tests [--use-reportportal] [--result RESULT]... [--name NAME]... ``` | Option | Default | Description | |--------|---------|-------------| | `--result RESULT` | `fail`, `error`, `warn` | Keep tests with this outcome (repeatable) | +| `--name NAME` | | Keep tests whose name matches this regex pattern (repeatable). Combined with `--result` using OR logic. Uses [search mode](https://tmt.readthedocs.io/en/stable/overview.html#regular-expressions) for matching. | | `--use-reportportal` | `false` | Fetch test results from ReportPortal instead of a local results file | When `--use-reportportal` is used, results are fetched from any report phase in the recipe that has `how: reportportal`. The phase must include `launch-uuid` and `test-uuids` (populated automatically by the tmt [ReportPortal](https://tmt.readthedocs.io/en/stable/plugins/report/reportportal.html) plugin after a run finishes). The `launch-uuid` and `test-uuids` fields are stripped from the output recipe so that a subsequent run creates a new ReportPortal launch. Plans that do not have a `reportportal` report phase always fall back to their local `results.yaml` file, even when `--use-reportportal` is passed. @@ -113,3 +114,15 @@ Fetch failures from ReportPortal and rerun them immediately: ```bash tmt-recipe-tool -i recipe.yaml --run filter-tests --use-reportportal --result fail ``` + +Keep only tests whose name matches a regex pattern, regardless of their result: + +```bash +tmt-recipe-tool -i recipe.yaml -o subset.yaml filter-tests --name '/smoke' +``` + +Keep only tests whose name matches a pattern or whose result is `fail`: + +```bash +tmt-recipe-tool -i recipe.yaml -o subset.yaml filter-tests --result fail --name '/smoke' +``` diff --git a/tmt-recipe-tool/tmt_recipe_tool/cli.py b/tmt-recipe-tool/tmt_recipe_tool/cli.py index ba4e416..b31c4c8 100644 --- a/tmt-recipe-tool/tmt_recipe_tool/cli.py +++ b/tmt-recipe-tool/tmt_recipe_tool/cli.py @@ -81,6 +81,16 @@ def main( "Can be specified multiple times." ), ) +@click.option( + "--name", + "names", + metavar="NAME", + multiple=True, + default=(), + help=( + "Keep tests whose name matches the given regex pattern. Can be specified multiple times." + ), +) @click.option( "--use-reportportal", is_flag=True, @@ -89,14 +99,19 @@ def main( ) @click.pass_context def filter_tests( - ctx: click.Context, results: list[str], use_reportportal: bool, **kwargs: Any + ctx: click.Context, + results: list[str], + names: list[str], + use_reportportal: bool, + **kwargs: Any, ) -> None: - """Filter recipe tests by their result outcome.""" + """Filter recipe tests by their result outcome or name.""" assert ctx.parent is not None ctx.obj["recipe"] = filter_recipe( ctx.parent.params["input"], - results, + filter_results=results, + filter_names=names, run_workdir=ctx.parent.params.get("run_workdir"), use_reportportal=use_reportportal, ) diff --git a/tmt-recipe-tool/tmt_recipe_tool/recipe.py b/tmt-recipe-tool/tmt_recipe_tool/recipe.py index d686f5c..63399d9 100644 --- a/tmt-recipe-tool/tmt_recipe_tool/recipe.py +++ b/tmt-recipe-tool/tmt_recipe_tool/recipe.py @@ -1,3 +1,4 @@ +import re from collections.abc import Iterable from pathlib import Path from typing import Optional, cast @@ -71,9 +72,13 @@ def _filter_tests( tests: list[tmt.recipe._RecipeTest], results: list[Result], filter_results: list[str], + filter_names: list[str], ) -> Iterable[tmt.recipe._RecipeTest]: - """Return only the tests whose result outcome matches the filter.""" + """Return only the tests whose result outcome or name matches the filter.""" for test in tests: + if any(re.search(name, test.name) for name in filter_names): + yield test + continue for result in results: if ( test.name == result.name @@ -87,10 +92,11 @@ def _filter_tests( def filter_recipe( input_path: Path, filter_results: list[str], + filter_names: list[str], run_workdir: Optional[Path] = None, use_reportportal: bool = False, ) -> tmt.recipe.Recipe: - """Load a recipe and keep only tests matching the specified result outcomes.""" + """Load a recipe and keep only tests matching the specified result outcomes or names.""" recipe = _load_recipe(input_path) filtered_plans = [] @@ -101,14 +107,16 @@ def filter_recipe( rp_phases = get_rp_phases(plan) if rp_phases and use_reportportal: plan.discover.tests = list( - filter_tests_from_rp(plan.discover.tests, rp_phases, filter_results) + filter_tests_from_rp(plan.discover.tests, rp_phases, filter_results, filter_names) ) plan.report.phases = edit_rp_phases(plan.report.phases) else: results = _load_results( _resolve_results_path(plan, input_path, run_workdir), plan.name ) - plan.discover.tests = list(_filter_tests(plan.discover.tests, results, filter_results)) + plan.discover.tests = list( + _filter_tests(plan.discover.tests, results, filter_results, filter_names) + ) filtered_plans.append(plan) recipe.plans = filtered_plans diff --git a/tmt-recipe-tool/tmt_recipe_tool/reportportal.py b/tmt-recipe-tool/tmt_recipe_tool/reportportal.py index 6e70808..1e90b7f 100644 --- a/tmt-recipe-tool/tmt_recipe_tool/reportportal.py +++ b/tmt-recipe-tool/tmt_recipe_tool/reportportal.py @@ -1,5 +1,6 @@ from __future__ import annotations +import re from collections.abc import Iterable from typing import Union, cast @@ -121,6 +122,7 @@ def filter_tests_from_rp( tests: list[_RecipeTest], rp_phases: list[ReportPortalPhase], filter_results: list[str], + filter_names: list[str], ) -> Iterable[_RecipeTest]: """Filter the tests from the ReportPortal results""" filter_results = list( @@ -129,6 +131,11 @@ def filter_tests_from_rp( filtered_tests: dict[int, _RecipeTest] = {} + for test in tests: + if any(re.search(name, test.name) for name in filter_names): + filtered_tests[test.serial_number] = test + continue + for phase in rp_phases: with retry_session( status_forcelist=STATUS_FORCELIST,