From ac92ce24e2f971423d804920262a244b613d0c98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20V=C3=A1gner?= Date: Mon, 20 Apr 2026 10:23:16 +0200 Subject: [PATCH 1/8] Fix recipe generation and loading --- spec/recipe.fmf | 4 ++-- tests/recipe/data/import.yaml | 2 +- tests/recipe/data/insert.yaml | 2 +- tests/recipe/data/local.yaml | 2 +- tests/recipe/data/remote.yaml | 2 +- tests/recipe/generate.sh | 1 + tmt/base/core.py | 2 +- tmt/recipe.py | 27 ++++++++++++++++++--------- tmt/steps/report/reportportal.py | 32 ++++++++++++++++++++++++++++++-- 9 files changed, 56 insertions(+), 18 deletions(-) diff --git a/spec/recipe.fmf b/spec/recipe.fmf index 68e580c1df..a59453bc44 100644 --- a/spec/recipe.fmf +++ b/spec/recipe.fmf @@ -88,8 +88,8 @@ description: | how: tmt order: 50 ... - # String, relative path to the results file from the previous run - results-path: plans/name/execute/results.yaml + # String, absolute path to the results file from the previous run + results-path: /var/tmp/tmt/run-001/plans/name/execute/results.yaml report: enabled: true diff --git a/tests/recipe/data/import.yaml b/tests/recipe/data/import.yaml index 4416c62485..9bbf418f93 100644 --- a/tests/recipe/data/import.yaml +++ b/tests/recipe/data/import.yaml @@ -76,7 +76,7 @@ plans: execute: enabled: false phases: [] - results-path: plans/import/execute/results.yaml + results-path: /run_path/plans/import/execute/results.yaml report: enabled: false phases: [] diff --git a/tests/recipe/data/insert.yaml b/tests/recipe/data/insert.yaml index 79bb1307b9..8626960cf2 100644 --- a/tests/recipe/data/insert.yaml +++ b/tests/recipe/data/insert.yaml @@ -126,7 +126,7 @@ plans: interactive: false restraint-compatible: false no-progress-bar: false - results-path: plans/insert/execute/results.yaml + results-path: /run_path/plans/insert/execute/results.yaml report: enabled: true phases: diff --git a/tests/recipe/data/local.yaml b/tests/recipe/data/local.yaml index 2b9f1ec9ad..34211fb796 100644 --- a/tests/recipe/data/local.yaml +++ b/tests/recipe/data/local.yaml @@ -140,7 +140,7 @@ plans: interactive: false restraint-compatible: false no-progress-bar: false - results-path: plans/local/execute/results.yaml + results-path: /run_path/plans/local/execute/results.yaml report: enabled: true phases: diff --git a/tests/recipe/data/remote.yaml b/tests/recipe/data/remote.yaml index c32f13b1a5..d1722726a1 100644 --- a/tests/recipe/data/remote.yaml +++ b/tests/recipe/data/remote.yaml @@ -82,7 +82,7 @@ plans: interactive: false restraint-compatible: false no-progress-bar: false - results-path: plans/remote/execute/results.yaml + results-path: /run_path/plans/remote/execute/results.yaml report: enabled: true phases: diff --git a/tests/recipe/generate.sh b/tests/recipe/generate.sh index 21dd9e31c0..02e5258779 100755 --- a/tests/recipe/generate.sh +++ b/tests/recipe/generate.sh @@ -11,6 +11,7 @@ rlJournalStart function replace_values () { temp_recipe=$(mktemp) yq '.run.root = "/path/to/fmf_root"' "$recipe" > "$temp_recipe" + sed -i "s#$run#/run_path#g" "$temp_recipe" mv "$temp_recipe" "$recipe" } diff --git a/tmt/base/core.py b/tmt/base/core.py index b08da78511..87a5adb3cf 100644 --- a/tmt/base/core.py +++ b/tmt/base/core.py @@ -2924,7 +2924,7 @@ def __init__( self.recipe_manager = RecipeManager(logger) self.recipe = None if recipe_path is not None and self._tree is not None: - self.recipe = self.recipe_manager.load(self, recipe_path, self._tree.tree) + self.recipe = self.recipe_manager.load(self, recipe_path) @property def run_workdir(self) -> Path: diff --git a/tmt/recipe.py b/tmt/recipe.py index 783101bc9f..20cbcca745 100644 --- a/tmt/recipe.py +++ b/tmt/recipe.py @@ -1,6 +1,6 @@ from typing import TYPE_CHECKING, Any, Callable, Optional, TypedDict, cast -from fmf import Tree +import fmf import tmt.utils from tmt.checks import Check, _RawCheck, normalize_test_checks @@ -368,7 +368,7 @@ def from_step(cls, step: 'Step') -> '_RecipeExecuteStep': return _RecipeExecuteStep( enabled=enabled, phases=[phase.to_minimal_spec() for phase in step.data] if enabled else [], - results_path=(step.step_workdir / 'results.yaml').relative_to(step.run_workdir), + results_path=(step.step_workdir / 'results.yaml').resolve(), ) def to_fmf_spec(self) -> list[_RawStepData]: @@ -562,11 +562,11 @@ class RecipeManager(Common): def __init__(self, logger: Logger): super().__init__(logger=logger) - def load(self, run: 'Run', recipe_path: Path, fmf_tree: Tree) -> Recipe: + def load(self, run: 'Run', recipe_path: Path) -> Recipe: recipe = Recipe.from_spec( cast(_RawRecipe, tmt.utils.yaml_to_dict(self.read(recipe_path))), self._logger ) - self._update_tree(recipe, fmf_tree) + self._update_tree(run, recipe) # TODO: We should have a way to set which steps are enabled # without modifying the CLI context directly. self._update_cli_context(recipe) @@ -584,7 +584,9 @@ def save(self, run: 'Run') -> None: ), plans=[_RecipePlan.from_plan(plan) for plan in run.plans], ) - self.write(run.run_workdir / 'recipe.yaml', tmt.utils.to_yaml(recipe.to_spec())) + self.write( + run.run_workdir / 'recipe.yaml', tmt.utils.to_yaml(recipe.to_spec(), yaml_type='rt') + ) def tests(self, recipe: Recipe, plan_name: str) -> list[TestOrigin]: """ @@ -603,12 +605,19 @@ def tests(self, recipe: Recipe, plan_name: str) -> list[TestOrigin]: raise tmt.utils.GeneralError(f"Plan '{plan_name}' not found in the recipe.") @staticmethod - def _update_tree(recipe: Recipe, tree: Tree) -> None: + def _update_tree(run: 'Run', recipe: Recipe) -> None: """ - Load the plans from the recipe and update the given fmf tree with their specifications. + Create a new fmf tree from the recipe's plan specifications. """ - tree.children.clear() - tree.update({plan.name: plan.to_fmf_spec() for plan in recipe.plans}) + from tmt.base.core import Tree + + fmf_tree = fmf.Tree({plan.name: plan.to_fmf_spec() for plan in recipe.plans}) + root = str(Path.cwd()) + fmf_tree.root = root + for node in fmf_tree.climb(): # pyright: ignore[reportUnknownVariableType] + if isinstance(node, fmf.Tree): + node.root = root + run._tree = Tree(logger=run._logger, tree=fmf_tree) def _update_cli_context(self, recipe: Recipe) -> None: """ diff --git a/tmt/steps/report/reportportal.py b/tmt/steps/report/reportportal.py index 07a4874333..86f3361eaa 100644 --- a/tmt/steps/report/reportportal.py +++ b/tmt/steps/report/reportportal.py @@ -2,7 +2,7 @@ import os import re from re import Pattern -from typing import TYPE_CHECKING, Any, Optional, Union, overload +from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast, overload import requests import urllib3 @@ -14,8 +14,9 @@ import tmt.utils.templates from tmt._compat.pathlib import Path from tmt.base.core import Test -from tmt.container import container, field +from tmt.container import container, field, option_to_key from tmt.result import Result, ResultOutcome +from tmt.steps import _RawStepData from tmt.utils import ( ActionType, catch_warnings_safe, @@ -380,6 +381,33 @@ class ReportReportPortalData(tmt.steps.report.ReportStepData): suite_uuid: Optional[str] = None test_uuids: dict[int, str] = field(default_factory=dict) + def to_spec(self) -> _RawStepData: + spec = super().to_spec() + spec['log-size-limit'] = str(self.log_size_limit) # type: ignore[reportGeneralTypeIssues,typeddict-unknown-key,unused-ignore] + spec['traceback-size-limit'] = str(self.traceback_size_limit) # type: ignore[reportGeneralTypeIssues,typeddict-unknown-key,unused-ignore] + spec['upload-log-pattern'] = [pattern.pattern for pattern in self.upload_log_pattern] # type: ignore[reportGeneralTypeIssues,typeddict-unknown-key,unused-ignore] + return spec + + def to_minimal_spec(self) -> _RawStepData: + spec = {**super().to_minimal_spec()} + + field_map: dict[str, Callable[[Any], Any]] = { + 'log-size-limit': lambda limit: str(limit), + 'traceback-size-limit': lambda limit: str(limit), + 'upload-log-pattern': lambda patterns: [pattern.pattern for pattern in patterns], + } + for key, transform in field_map.items(): + value = getattr(self, option_to_key(key), None) + if value is not None: + value = transform(value) + # Do not include empty values + if value in (None, [], {}): + spec.pop(key, None) + else: + spec[key] = value + + return cast(_RawStepData, spec) + @tmt.steps.provides_method("reportportal") class ReportReportPortal(tmt.steps.report.ReportPlugin[ReportReportPortalData]): From 907d6c856f1028da7399065bf08a3f359b630487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20V=C3=A1gner?= Date: Mon, 20 Apr 2026 11:31:55 +0200 Subject: [PATCH 2/8] squash: remove forgotten check --- tmt/base/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tmt/base/core.py b/tmt/base/core.py index 87a5adb3cf..a59342a475 100644 --- a/tmt/base/core.py +++ b/tmt/base/core.py @@ -2923,7 +2923,7 @@ def __init__( self.policies = policies or [] self.recipe_manager = RecipeManager(logger) self.recipe = None - if recipe_path is not None and self._tree is not None: + if recipe_path is not None: self.recipe = self.recipe_manager.load(self, recipe_path) @property From 5a0cee0c43ba3fb7b3c63069fbe6a7739795e8ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20V=C3=A1gner?= Date: Mon, 20 Apr 2026 16:40:14 +0200 Subject: [PATCH 3/8] squash: change yaml_type --- tmt/recipe.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tmt/recipe.py b/tmt/recipe.py index 20cbcca745..1a787b3165 100644 --- a/tmt/recipe.py +++ b/tmt/recipe.py @@ -564,7 +564,8 @@ def __init__(self, logger: Logger): def load(self, run: 'Run', recipe_path: Path) -> Recipe: recipe = Recipe.from_spec( - cast(_RawRecipe, tmt.utils.yaml_to_dict(self.read(recipe_path))), self._logger + cast(_RawRecipe, tmt.utils.yaml_to_dict(self.read(recipe_path), yaml_type='rt')), + self._logger, ) self._update_tree(run, recipe) # TODO: We should have a way to set which steps are enabled From d3effef1fbb2e4976de44bca5b36c2512721beca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20V=C3=A1gner?= Date: Mon, 20 Apr 2026 17:32:05 +0200 Subject: [PATCH 4/8] squash: use relative path --- spec/recipe.fmf | 4 ++-- tests/recipe/data/import.yaml | 2 +- tests/recipe/data/insert.yaml | 2 +- tests/recipe/data/local.yaml | 2 +- tests/recipe/data/remote.yaml | 2 +- tests/recipe/generate.sh | 1 - tmt/recipe.py | 2 +- 7 files changed, 7 insertions(+), 8 deletions(-) diff --git a/spec/recipe.fmf b/spec/recipe.fmf index a59453bc44..68e580c1df 100644 --- a/spec/recipe.fmf +++ b/spec/recipe.fmf @@ -88,8 +88,8 @@ description: | how: tmt order: 50 ... - # String, absolute path to the results file from the previous run - results-path: /var/tmp/tmt/run-001/plans/name/execute/results.yaml + # String, relative path to the results file from the previous run + results-path: plans/name/execute/results.yaml report: enabled: true diff --git a/tests/recipe/data/import.yaml b/tests/recipe/data/import.yaml index 9bbf418f93..4416c62485 100644 --- a/tests/recipe/data/import.yaml +++ b/tests/recipe/data/import.yaml @@ -76,7 +76,7 @@ plans: execute: enabled: false phases: [] - results-path: /run_path/plans/import/execute/results.yaml + results-path: plans/import/execute/results.yaml report: enabled: false phases: [] diff --git a/tests/recipe/data/insert.yaml b/tests/recipe/data/insert.yaml index 8626960cf2..79bb1307b9 100644 --- a/tests/recipe/data/insert.yaml +++ b/tests/recipe/data/insert.yaml @@ -126,7 +126,7 @@ plans: interactive: false restraint-compatible: false no-progress-bar: false - results-path: /run_path/plans/insert/execute/results.yaml + results-path: plans/insert/execute/results.yaml report: enabled: true phases: diff --git a/tests/recipe/data/local.yaml b/tests/recipe/data/local.yaml index 34211fb796..2b9f1ec9ad 100644 --- a/tests/recipe/data/local.yaml +++ b/tests/recipe/data/local.yaml @@ -140,7 +140,7 @@ plans: interactive: false restraint-compatible: false no-progress-bar: false - results-path: /run_path/plans/local/execute/results.yaml + results-path: plans/local/execute/results.yaml report: enabled: true phases: diff --git a/tests/recipe/data/remote.yaml b/tests/recipe/data/remote.yaml index d1722726a1..c32f13b1a5 100644 --- a/tests/recipe/data/remote.yaml +++ b/tests/recipe/data/remote.yaml @@ -82,7 +82,7 @@ plans: interactive: false restraint-compatible: false no-progress-bar: false - results-path: /run_path/plans/remote/execute/results.yaml + results-path: plans/remote/execute/results.yaml report: enabled: true phases: diff --git a/tests/recipe/generate.sh b/tests/recipe/generate.sh index 02e5258779..21dd9e31c0 100755 --- a/tests/recipe/generate.sh +++ b/tests/recipe/generate.sh @@ -11,7 +11,6 @@ rlJournalStart function replace_values () { temp_recipe=$(mktemp) yq '.run.root = "/path/to/fmf_root"' "$recipe" > "$temp_recipe" - sed -i "s#$run#/run_path#g" "$temp_recipe" mv "$temp_recipe" "$recipe" } diff --git a/tmt/recipe.py b/tmt/recipe.py index 1a787b3165..fcfbb4b888 100644 --- a/tmt/recipe.py +++ b/tmt/recipe.py @@ -368,7 +368,7 @@ def from_step(cls, step: 'Step') -> '_RecipeExecuteStep': return _RecipeExecuteStep( enabled=enabled, phases=[phase.to_minimal_spec() for phase in step.data] if enabled else [], - results_path=(step.step_workdir / 'results.yaml').resolve(), + results_path=(step.step_workdir / 'results.yaml').relative_to(step.run_workdir), ) def to_fmf_spec(self) -> list[_RawStepData]: From 6fc0e76669c51a26f32d647c869d3ebcf68289a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20V=C3=A1gner?= Date: Tue, 21 Apr 2026 12:24:54 +0200 Subject: [PATCH 5/8] squash: revert reportportal changes --- tmt/steps/report/reportportal.py | 32 ++------------------------------ 1 file changed, 2 insertions(+), 30 deletions(-) diff --git a/tmt/steps/report/reportportal.py b/tmt/steps/report/reportportal.py index 86f3361eaa..07a4874333 100644 --- a/tmt/steps/report/reportportal.py +++ b/tmt/steps/report/reportportal.py @@ -2,7 +2,7 @@ import os import re from re import Pattern -from typing import TYPE_CHECKING, Any, Callable, Optional, Union, cast, overload +from typing import TYPE_CHECKING, Any, Optional, Union, overload import requests import urllib3 @@ -14,9 +14,8 @@ import tmt.utils.templates from tmt._compat.pathlib import Path from tmt.base.core import Test -from tmt.container import container, field, option_to_key +from tmt.container import container, field from tmt.result import Result, ResultOutcome -from tmt.steps import _RawStepData from tmt.utils import ( ActionType, catch_warnings_safe, @@ -381,33 +380,6 @@ class ReportReportPortalData(tmt.steps.report.ReportStepData): suite_uuid: Optional[str] = None test_uuids: dict[int, str] = field(default_factory=dict) - def to_spec(self) -> _RawStepData: - spec = super().to_spec() - spec['log-size-limit'] = str(self.log_size_limit) # type: ignore[reportGeneralTypeIssues,typeddict-unknown-key,unused-ignore] - spec['traceback-size-limit'] = str(self.traceback_size_limit) # type: ignore[reportGeneralTypeIssues,typeddict-unknown-key,unused-ignore] - spec['upload-log-pattern'] = [pattern.pattern for pattern in self.upload_log_pattern] # type: ignore[reportGeneralTypeIssues,typeddict-unknown-key,unused-ignore] - return spec - - def to_minimal_spec(self) -> _RawStepData: - spec = {**super().to_minimal_spec()} - - field_map: dict[str, Callable[[Any], Any]] = { - 'log-size-limit': lambda limit: str(limit), - 'traceback-size-limit': lambda limit: str(limit), - 'upload-log-pattern': lambda patterns: [pattern.pattern for pattern in patterns], - } - for key, transform in field_map.items(): - value = getattr(self, option_to_key(key), None) - if value is not None: - value = transform(value) - # Do not include empty values - if value in (None, [], {}): - spec.pop(key, None) - else: - spec[key] = value - - return cast(_RawStepData, spec) - @tmt.steps.provides_method("reportportal") class ReportReportPortal(tmt.steps.report.ReportPlugin[ReportReportPortalData]): From 4bfdcdb5d8889753524640faa1d766624d196b99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20V=C3=A1gner?= Date: Tue, 21 Apr 2026 12:25:33 +0200 Subject: [PATCH 6/8] squash: revert yaml_type change --- tmt/recipe.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tmt/recipe.py b/tmt/recipe.py index fcfbb4b888..5829fb48fa 100644 --- a/tmt/recipe.py +++ b/tmt/recipe.py @@ -564,7 +564,7 @@ def __init__(self, logger: Logger): def load(self, run: 'Run', recipe_path: Path) -> Recipe: recipe = Recipe.from_spec( - cast(_RawRecipe, tmt.utils.yaml_to_dict(self.read(recipe_path), yaml_type='rt')), + cast(_RawRecipe, tmt.utils.yaml_to_dict(self.read(recipe_path))), self._logger, ) self._update_tree(run, recipe) @@ -585,9 +585,7 @@ def save(self, run: 'Run') -> None: ), plans=[_RecipePlan.from_plan(plan) for plan in run.plans], ) - self.write( - run.run_workdir / 'recipe.yaml', tmt.utils.to_yaml(recipe.to_spec(), yaml_type='rt') - ) + self.write(run.run_workdir / 'recipe.yaml', tmt.utils.to_yaml(recipe.to_spec())) def tests(self, recipe: Recipe, plan_name: str) -> list[TestOrigin]: """ From f2512fc9431429eb51249f120857435db57f33ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20V=C3=A1gner?= Date: Tue, 21 Apr 2026 16:39:33 +0200 Subject: [PATCH 7/8] squash: use fmf root path from the recipe --- tmt/recipe.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tmt/recipe.py b/tmt/recipe.py index 5829fb48fa..c0214149a1 100644 --- a/tmt/recipe.py +++ b/tmt/recipe.py @@ -606,12 +606,12 @@ def tests(self, recipe: Recipe, plan_name: str) -> list[TestOrigin]: @staticmethod def _update_tree(run: 'Run', recipe: Recipe) -> None: """ - Create a new fmf tree from the recipe's plan specifications. + Create a new fmf tree from the recipe's run and plan specifications. """ from tmt.base.core import Tree fmf_tree = fmf.Tree({plan.name: plan.to_fmf_spec() for plan in recipe.plans}) - root = str(Path.cwd()) + root = recipe.run.root or str(Path.cwd()) fmf_tree.root = root for node in fmf_tree.climb(): # pyright: ignore[reportUnknownVariableType] if isinstance(node, fmf.Tree): From a95c1ee7f7fb25c2c15c84acc68eded5df9f73de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20V=C3=A1gner?= Date: Tue, 21 Apr 2026 17:58:00 +0200 Subject: [PATCH 8/8] squash: take fmf root from recipe only --- tmt/recipe.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tmt/recipe.py b/tmt/recipe.py index c0214149a1..5ff2adae54 100644 --- a/tmt/recipe.py +++ b/tmt/recipe.py @@ -611,11 +611,10 @@ def _update_tree(run: 'Run', recipe: Recipe) -> None: from tmt.base.core import Tree fmf_tree = fmf.Tree({plan.name: plan.to_fmf_spec() for plan in recipe.plans}) - root = recipe.run.root or str(Path.cwd()) - fmf_tree.root = root + fmf_tree.root = recipe.run.root for node in fmf_tree.climb(): # pyright: ignore[reportUnknownVariableType] if isinstance(node, fmf.Tree): - node.root = root + node.root = recipe.run.root run._tree = Tree(logger=run._logger, tree=fmf_tree) def _update_cli_context(self, recipe: Recipe) -> None: