From 7747eb7f308bfe9aade633fca5bd6c69a9f2284b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Prchl=C3=ADk?= Date: Thu, 2 Jul 2026 10:57:43 +0200 Subject: [PATCH 1/3] Plan source script shall be guest-specific file Similarly to plan environment file, plan source script also must be made guest-specific: as phases run on multiple guests at the same time, they modify their local file, stored on the guest, which may then be pulled by tmt into singular location, over writing each other. Now, the situation is not as worse as it was in case of the environment file, since tmt does not pull the plan source script on purpose, and it certainly is not pushed to guests, but the principle is the same: multiple phases touch "one" file, on their guests, and tmt is not aware the file may be very different. By moving the ownership of the path to guest, and adding the suffix, we prevent any future confusion should we start pulling/pushing the file(s). --- tmt/base/plan.py | 14 -------------- tmt/guest/__init__.py | 30 ++++++++++++++++++++++++++---- tmt/steps/execute/__init__.py | 20 ++++++++++++++------ tmt/steps/prepare/shell.py | 15 ++++++++++----- tmt/steps/provision/local.py | 11 +++++++---- 5 files changed, 57 insertions(+), 33 deletions(-) diff --git a/tmt/base/plan.py b/tmt/base/plan.py index 1b8550cd3f..f3cc2225f9 100644 --- a/tmt/base/plan.py +++ b/tmt/base/plan.py @@ -66,10 +66,6 @@ import tmt.log -#: Filename associated with ``TMT_PLAN_SOURCE_SCRIPT`` -PLAN_SOURCE_SCRIPT_NAME: str = "plan-source-script.sh" - - class _RemotePlanReference(_RawFmfId): importing: Optional[str] scope: Optional[str] @@ -476,7 +472,6 @@ def _environment_from_intrinsics(self) -> Environment: if self.my_run: environment['TMT_PLAN_DATA'] = EnvVarValue(self.data_directory) - environment['TMT_PLAN_SOURCE_SCRIPT'] = EnvVarValue(self.plan_source_script) return environment @@ -720,15 +715,6 @@ def data_directory(self) -> Path: return data_directory - @functools.cached_property - def plan_source_script(self) -> Path: - plan_sourced_file_path = self.data_directory / PLAN_SOURCE_SCRIPT_NAME - plan_sourced_file_path.touch(exist_ok=True) - - self.debug(f"Create the environment file '{plan_sourced_file_path}'.", level=2) - - return plan_sourced_file_path - @staticmethod def edit_template(raw_content: str) -> str: """ diff --git a/tmt/guest/__init__.py b/tmt/guest/__init__.py index b155beae09..81e67835f5 100644 --- a/tmt/guest/__init__.py +++ b/tmt/guest/__init__.py @@ -85,6 +85,9 @@ #: Name of the :ref:`plan environment file `. PLAN_ENVIRONMENT_FILENAME = 'variables.env' +#: Name of the :ref:`plan source script `. +PLAN_SOURCE_SCRIPT_FILENAME: str = "plan-source-script.sh" + #: How many seconds to wait for a connection to succeed after guest boot. #: This is the default value tmt would use unless told otherwise. DEFAULT_CONNECT_TIMEOUT = 2 * 60 @@ -1891,6 +1894,22 @@ def plan_environment_path(self) -> Optional[Path]: return path + @functools.cached_property + def plan_source_script_path(self) -> Optional[Path]: + """ + A path to the :ref:`plan source script ` file. + """ + + if not isinstance(self.parent, tmt.steps.provision.Provision): + return None + + path = self.parent.plan.data_directory / f'{PLAN_SOURCE_SCRIPT_FILENAME}-{self.safe_name}' + path.touch(exist_ok=True) + + self.debug(f"Create the plan source script '{path}'.", level=2) + + return path + @property def plan_environment(self) -> Environment: """ @@ -2236,13 +2255,16 @@ def _prepare_command_environment( if isinstance(self.parent, tmt.steps.Step): environment.update(self.parent.plan) - # TODO: this was owned by plan, but at wrong position, and it will - # be owned by plan again once the dust of environment untangling - # settles. Follow https://github.com/teemtee/tmt/issues/4241 for - # more. + # TODO: these are owned by plan, but at wrong position, and + # they will be owned by plan again once the dust of environment + # untangling settles. Follow https://github.com/teemtee/tmt/issues/4241 + # for more. if self.plan_environment_path: environment['TMT_PLAN_ENVIRONMENT_FILE'] = EnvVarValue(self.plan_environment_path) + if self.plan_source_script_path: + environment['TMT_PLAN_SOURCE_SCRIPT'] = EnvVarValue(self.plan_source_script_path) + else: # Create a copy of given environment - this prevents any # accidental modification of the given environment. diff --git a/tmt/steps/execute/__init__.py b/tmt/steps/execute/__init__.py index a76c8917b8..be76643fb4 100644 --- a/tmt/steps/execute/__init__.py +++ b/tmt/steps/execute/__init__.py @@ -388,11 +388,17 @@ def environment(self) -> Environment: else: environment = self._environment - # TODO: this was owned by plan, but at wrong position, and it will - # be owned by plan again once the dust of environment untangling - # settles. Follow https://github.com/teemtee/tmt/issues/4241 for - # more. - environment['TMT_PLAN_ENVIRONMENT_FILE'] = EnvVarValue(self.guest.plan_environment_path) + # TODO: these are owned by plan, but at wrong position, and + # they will be owned by plan again once the dust of environment + # untangling settles. Follow https://github.com/teemtee/tmt/issues/4241 + # for more. + if self.guest.plan_environment_path: + environment['TMT_PLAN_ENVIRONMENT_FILE'] = EnvVarValue( + self.guest.plan_environment_path + ) + + if self.guest.plan_source_script_path: + environment['TMT_PLAN_SOURCE_SCRIPT'] = EnvVarValue(self.guest.plan_source_script_path) environment.update( # Add variables from invocation contexts @@ -543,7 +549,9 @@ def _invoke(timeout: Optional[int] = None) -> CommandOutput: on_process_end=_reset_process, test_session=True, friendly_command=str(self.test.test), - sourced_files=[self.phase.step.plan.plan_source_script], + sourced_files=[self.guest.plan_source_script_path] + if self.guest.plan_source_script_path + else [], ) self.start_time = timer.start_time_formatted diff --git a/tmt/steps/prepare/shell.py b/tmt/steps/prepare/shell.py index 386eaa3cbd..0170953f27 100644 --- a/tmt/steps/prepare/shell.py +++ b/tmt/steps/prepare/shell.py @@ -150,13 +150,16 @@ def go( self.step.plan.environment, ) - # TODO: this was owned by plan, but at wrong position, and it will - # be owned by plan again once the dust of environment untangling - # settles. Follow https://github.com/teemtee/tmt/issues/4241 for - # more. + # TODO: these are owned by plan, but at wrong position, and + # they will be owned by plan again once the dust of environment + # untangling settles. Follow https://github.com/teemtee/tmt/issues/4241 + # for more. if guest.plan_environment_path: environment['TMT_PLAN_ENVIRONMENT_FILE'] = EnvVarValue(guest.plan_environment_path) + if guest.plan_source_script_path: + environment['TMT_PLAN_SOURCE_SCRIPT'] = EnvVarValue(guest.plan_source_script_path) + # Give a short summary overview = fmf.utils.listed(self.data.script, 'script') logger.info('overview', f'{overview} found', 'green') @@ -246,7 +249,9 @@ def _invoke_script( command=command, cwd=worktree, environment=environment, - sourced_files=[self.step.plan.plan_source_script], + sourced_files=[guest.plan_source_script_path] + if guest.plan_source_script_path + else [], immediately=False, ) diff --git a/tmt/steps/provision/local.py b/tmt/steps/provision/local.py index a8991449a0..cf1b0f0c2d 100644 --- a/tmt/steps/provision/local.py +++ b/tmt/steps/provision/local.py @@ -75,13 +75,16 @@ def _prepare_command_environment( if isinstance(self.parent, tmt.steps.Step): environment.update(self.parent.plan) - # TODO: this was owned by plan, but at wrong position, and it will - # be owned by plan again once the dust of environment untangling - # settles. Follow https://github.com/teemtee/tmt/issues/4241 for - # more. + # TODO: these are owned by plan, but at wrong position, and + # they will be owned by plan again once the dust of environment + # untangling settles. Follow https://github.com/teemtee/tmt/issues/4241 + # for more. if self.plan_environment_path: environment['TMT_PLAN_ENVIRONMENT_FILE'] = EnvVarValue(self.plan_environment_path) + if self.plan_source_script_path: + environment['TMT_PLAN_SOURCE_SCRIPT'] = EnvVarValue(self.plan_source_script_path) + else: environment = super()._prepare_command_environment(environment=environment) From 52d196cb20f596c02b483fa3365a6c393313a352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Prchl=C3=ADk?= Date: Thu, 2 Jul 2026 12:59:28 +0200 Subject: [PATCH 2/3] squash: fix test --- tests/core/source-script/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/source-script/test.sh b/tests/core/source-script/test.sh index d1062648ba..2b5376e184 100755 --- a/tests/core/source-script/test.sh +++ b/tests/core/source-script/test.sh @@ -13,7 +13,7 @@ rlJournalStart # All tests are included in the tmt plan/test itself rlRun -s "tmt run --id $run -a provision --how=${PROVISION_HOW}" 0 "Run tests" # Make sure the file is still there - rlAssertExists $run/plan/data/plan-source-script.sh + rlAssertExists $run/plan/data/plan-source-script.sh-default-0-default-0 rlPhaseEnd rlPhaseStartCleanup From 468d24c99f960c2093582a35b8cd54af8f6b96b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milo=C5=A1=20Prchl=C3=ADk?= Date: Fri, 3 Jul 2026 20:40:00 +0200 Subject: [PATCH 3/3] squash: fixing fixed test --- tests/core/source-script/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/core/source-script/test.sh b/tests/core/source-script/test.sh index 2b5376e184..a79e561a26 100755 --- a/tests/core/source-script/test.sh +++ b/tests/core/source-script/test.sh @@ -13,7 +13,7 @@ rlJournalStart # All tests are included in the tmt plan/test itself rlRun -s "tmt run --id $run -a provision --how=${PROVISION_HOW}" 0 "Run tests" # Make sure the file is still there - rlAssertExists $run/plan/data/plan-source-script.sh-default-0-default-0 + rlAssertExists $run/plan/data/plan-source-script.sh-default-0 rlPhaseEnd rlPhaseStartCleanup