Skip to content

Add support for full recipe loading#4661

Open
therazix wants to merge 2 commits intofvagner-conversion-methodsfrom
fvagner-full-recipe-loading
Open

Add support for full recipe loading#4661
therazix wants to merge 2 commits intofvagner-conversion-methodsfrom
fvagner-full-recipe-loading

Conversation

@therazix
Copy link
Contributor

@therazix therazix commented Mar 9, 2026

This PR implements a full recipe loading feature. All phases can now be loaded directly from the recipe. Serialization and deserialization were replaced with to_spec/from_spec methods, and the generated recipe will now contain only non-empty values to reduce its size.

Resolves: #4531

Pull Request Checklist

  • implement the feature
  • write the documentation
  • extend the test coverage
  • update the specification
  • adjust plugin docstring
  • modify the json schema
  • mention the version
  • include a release note

@therazix therazix added this to the 1.69 milestone Mar 9, 2026
@therazix therazix added the area | recipe Related to the tmt recipe handling label Mar 9, 2026
@therazix therazix added this to planning Mar 9, 2026
@github-project-automation github-project-automation bot moved this to backlog in planning Mar 9, 2026
@therazix therazix moved this from backlog to implement in planning Mar 9, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements full recipe loading, serializing the complete plan state into a recipe file to enable reproducible runs. A key change involves dynamically deriving the discover-phase attribute from test paths. However, a critical security vulnerability has been identified: the implementation of test path recreation in the discover step is susceptible to Path Traversal. The path attribute from the recipe is used to construct filesystem paths for directory creation without adequate sanitization, potentially allowing an attacker to create directories outside the intended workdir using .. sequences. This requires remediation by validating that the resulting paths do not escape the intended base directory. Additionally, there are a couple of suggestions to improve code robustness and clarity in tmt/recipe.py.

tmt/recipe.py Outdated
Comment on lines +86 to +91
def from_serialized(cls, serialized: dict[str, Any], logger: Logger) -> '_RecipeTest': # type: ignore[override]
raw_checks = serialized.pop('check', [])
serialized['check'] = []
recipe_test = super().from_serialized(serialized)
recipe_test.check = [Check.from_spec(check, logger) for check in raw_checks]
return recipe_test
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This method modifies the serialized dictionary in-place, which can be an unexpected side effect. To improve encapsulation and prevent potential issues, create a copy of the dictionary to work with.

Suggested change
def from_serialized(cls, serialized: dict[str, Any], logger: Logger) -> '_RecipeTest': # type: ignore[override]
raw_checks = serialized.pop('check', [])
serialized['check'] = []
recipe_test = super().from_serialized(serialized)
recipe_test.check = [Check.from_spec(check, logger) for check in raw_checks]
return recipe_test
def from_serialized(cls, serialized: dict[str, Any], logger: Logger) -> '_RecipeTest': # type: ignore[override]
serialized = serialized.copy()
raw_checks = serialized.pop('check', [])
recipe_test = super().from_serialized(serialized)
recipe_test.check = [Check.from_spec(check, logger) for check in raw_checks]
return recipe_test

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth documenting if expected

@thrix thrix self-requested a review March 9, 2026 19:09
@therazix therazix force-pushed the fvagner-full-recipe-loading branch from 3c63f94 to 18b7353 Compare March 9, 2026 22:04
@therazix therazix added the ci | full test Pull request is ready for the full test execution label Mar 9, 2026
Copy link
Contributor

@thrix thrix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The full recipe loading direction is good. Three findings, one requiring a change:

  1. Run.environment silently ignores CLI --environment when recipe is loaded - The recipe env unconditionally overrides CLI options. New --environment overrides on replay are silently lost. This should merge recipe env with CLI env, letting CLI take precedence.

  2. link field type mismatch after unserialization removal - Raw data stored where Links object is expected. Not an active runtime bug but incorrect typing that could break on re-serialization paths.

  3. Unrelated schema change - display-guest in report/display.yaml should be split out.

@skycastlelily
Copy link
Collaborator

Besides other comments, the code appears to assume the recipe file is well-formed, do you consider add some validation to the recipe file provided, say, a schema file?

@happz
Copy link
Contributor

happz commented Mar 10, 2026

@therazix please, set the "Size" of this PR.

Copy link
Contributor

@thrix thrix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Add support for full recipe loading

Good progress on extending recipe loading from discover-only to all plan steps. The environment simplification and removal of discover_phase are clean.

Issues

Blocking:

  • CLI environment silently ignored (tmt/base/core.py): The early return bypasses _environment_from_cli, so --environment FOO=bar is silently dropped when using --recipe. See inline comment for suggested fix.

Latent bug:

  • _RecipeTest.link unserialization removed (tmt/recipe.py): Works today because all test links are [] (falsy short-circuit), but would AttributeError on any recipe with non-empty test links. See inline comment.

Hygiene:

  • Unrelated schema change (tmt/schemas/report/display.yaml): display-guest addition is not related to recipe loading — should be a separate commit/PR.
  • PR checklist: All items are unchecked — docs, spec, schema, version, release note still needed.

Verified non-issues

  • Path traversal in discover_from_recipe: The relative_to() + resolve() + parent check is sufficient. The gemini-code-assist security warning is overblown.
  • Removed "non-existent plan" test: Correct — with tree.children.clear(), the tree IS the recipe, so the old error case no longer applies.
  • Only saving _environment_from_fmf in _RecipePlan: Reasonable — _RecipeRun.environment captures the full merged env, and intrinsics should be regenerated per run.

Generated-by: Claude Code

tmt/recipe.py Outdated
link: Optional['tmt.base.core.Links'] = field(
serialize=lambda value: value.to_spec() if value else [],
unserialize=lambda value: _unserialize_links(value),
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latent bug: the unserialize callback was removed, so when loading from YAML via from_serialized(), the raw value (list of dicts) is stored instead of a Links object.

This is fine when link is [] (falsy — the serialize callback short-circuits to []), which is the case for all current test data. However, if a recipe test ever has a non-empty link (e.g., [{"relates": "..."}]), the serialize callback lambda value: value.to_spec() if value else [] would call .to_spec() on a raw list, causing an AttributeError.

Either restore the unserialize callback or handle raw data in the serialize callback.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated-by: Claude Code

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been addressed. serialize/unserialize has been replaced with to_spec/from_spec.

@bajertom bajertom modified the milestones: 1.69, 1.70 Mar 12, 2026
@therazix therazix force-pushed the fvagner-full-recipe-loading branch from 18b7353 to 4201f23 Compare March 16, 2026 13:55
@therazix therazix force-pushed the fvagner-full-recipe-loading branch 3 times, most recently from 452c14b to 7165403 Compare March 17, 2026 11:54
@therazix therazix moved this from implement to review in planning Mar 17, 2026
@therazix therazix force-pushed the fvagner-full-recipe-loading branch from 7165403 to 8360382 Compare March 17, 2026 14:51
@therazix therazix force-pushed the fvagner-full-recipe-loading branch 2 times, most recently from 8810887 to 27d199a Compare March 17, 2026 18:06
@therazix therazix requested a review from lbrabec as a code owner March 17, 2026 18:06
@thrix
Copy link
Contributor

thrix commented Mar 17, 2026

@therazix would it be possible to provide a solid MR description for the changes, so it is easier to follow the changes, for example I am looking at this diff:

https://github.com/teemtee/tmt/pull/4661/changes#diff-f3bed0c5c8c7fcc2e8e296e3e85675c9c29c322c3a66f8def8f1d6d6d8dce260

And I would like to understand why this was changed, I would expect it would be mentioned that this is one of the improvements made to support full recipe loading ... (or something similar)


return spec

def to_minimal_spec(self) -> tmt.steps._RawStepData:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think even functions like these would deserve comments ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in 62ddebc.

Comment on lines +77 to +79
'hard-reboot': str(self.systemd_soft_reboot)
if isinstance(self.systemd_soft_reboot, ShellScript)
else None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hard-reboot referencing systemds-soft-reboot, does not sound right

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 62ddebc.

Comment on lines +94 to +95
for key, transform in field_map.items():
value = getattr(self, key, None)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

field_map items use hyphens, but this is accessing keys of self, which are with underscores?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 62ddebc.

Copy link
Contributor

@thrix thrix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed

@therazix therazix force-pushed the fvagner-full-recipe-loading branch from 27d199a to 62ddebc Compare March 18, 2026 12:38
@happz
Copy link
Contributor

happz commented Mar 18, 2026

Two observations, and I'm open to discussion:

  • quite a lot of code is related to addition of to_spec and to_minimal_spec - this would be nice to see as a standalone PR. There were some questions about how this works recently, refactoring this part of code would be worth on its own, with review not necessarily tied to recipes.

  • the number of "crossroads", with "if recipe" and "else" branches, tends to grow. I was hoping recipe handling would follow the path of replacing tmt entities with those loaded from recipe, restoring their state. Every place where we need to look whether we're in the "recipe mode" deserves a hard scrutiny and the utmost prejudice: why this test exists, is it a sign of us not replacing a tmt object completely, is it a temporary solution before the next patch?

    For example, in tmt.steps.Step.enabled:

    if self.plan.my_run.recipe:
        plan = self.plan.my_run.recipe.get_plan_by_name(self.plan.name)
        step = plan.get_step_by_name(self.step_name)
        return step.enabled
    ``
    
    What is `self` in this context? Isn't it an instance of `Step` subclass, restored from a recipe, in which case its `enabled` should be correctly set already by the recipe?
    
    I am not advocating against the existence of such checks, sometimes they might be unavoidable, I would just like to see every such check be explained, preferably with a hefty comment describing why it's needed, what would not work if the check wouldn't exist, and so on. Without a strict approach dscipline we can easily end up maintaining a growing list of tests and exceptions, eventually implement one behavior twice, once for "tmt as usual" and once for "tmt when loaded from recipe".

@therazix therazix force-pushed the fvagner-full-recipe-loading branch from 62ddebc to b430111 Compare March 20, 2026 17:56
@therazix therazix changed the base branch from main to fvagner-conversion-methods March 20, 2026 17:59
@therazix therazix force-pushed the fvagner-full-recipe-loading branch from b430111 to 207f6c7 Compare March 20, 2026 18:01
@therazix
Copy link
Contributor Author

quite a lot of code is related to addition of to_spec and to_minimal_spec - this would be nice to see as a standalone PR.

I created a standalone PR #4727.

the number of "crossroads", with "if recipe" and "else" branches, tends to grow.

I agree. I tried to get rid of them in 207f6c7.

What is self in this context? Isn't it an instance of Step subclass, restored from a recipe, in which case its enabled should be correctly set already by the recipe?

If I understand it correctly, a step is only enabled when its name is in _cli_context_object.steps. There is no attribute we could set during recipe loading to enable or disable the step. I tried inserting the step names directly into _cli_context_object.steps in 207f6c7, as it's basically the same as what is invoked during recipe creation, but I'm not sure if it's a good solution. Or should I just add an attribute to the Step that the recipe would set during loading?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area | recipe Related to the tmt recipe handling ci | full test Pull request is ready for the full test execution

Projects

Status: review

Development

Successfully merging this pull request may close these issues.

Implement full loading of the recipe

6 participants