Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions tmt/guest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
container,
field,
key_to_option,
option_to_key,
)
from tmt.package_managers import (
FileSystemPath,
Expand Down Expand Up @@ -1325,13 +1326,46 @@ def from_spec( # type: ignore[override]
return super().from_spec(raw_data, logger) # type: ignore[call-arg]

def to_spec(self) -> tmt.steps._RawStepData:
"""Convert to a form suitable for saving in a specification file"""
spec = super().to_spec()

spec.pop('facts', None) # type: ignore[typeddict-item]
spec.pop('-OPTIONLESS-FIELDS', None) # type: ignore[typeddict-item]
spec['ansible'] = self.ansible.to_spec() if self.ansible else {} # type: ignore[typeddict-unknown-key]
spec['environment'] = self.environment.to_fmf_spec() if self.environment else {} # type: ignore[typeddict-unknown-key]
spec['hardware'] = self.hardware.to_spec() if self.hardware else None # type: ignore[typeddict-unknown-key]

return spec

def to_minimal_spec(self) -> tmt.steps._RawStepData:
"""
Convert to a form suitable for saving in a specification
file while skipping empty values.
"""
spec = {**super().to_minimal_spec()}

spec.pop('facts', None)
spec.pop('-OPTIONLESS-FIELDS', None)

# Some fields need special handling.
# Map them to functions that will correctly convert them.
field_map: dict[str, Callable[[Any], Any]] = {
'ansible': lambda ansible: ansible.to_spec() if self.ansible else {},
'environment': lambda environment: environment.to_fmf_spec(),
'hardware': lambda hardware: hardware.to_minimal_spec() if hardware else None,
}
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(tmt.steps._RawStepData, spec)

# TODO: find out whether this could live in DataContainer. It probably could,
# but there are containers not backed by options... Maybe a mixin then?
@classmethod
Expand Down Expand Up @@ -2756,6 +2790,18 @@ class GuestSshData(GuestData):
normalize=tmt.utils.normalize_string_list,
)

def to_spec(self) -> tmt.steps._RawStepData:
spec = super().to_spec()
spec['key'] = [str(key) for key in self.key] # type: ignore[typeddict-unknown-key]
return spec

def to_minimal_spec(self) -> tmt.steps._RawStepData:
spec = super().to_minimal_spec()
spec.pop('key', None) # type: ignore[typeddict-item]
if self.key:
spec['key'] = [str(key) for key in self.key] # type: ignore[typeddict-unknown-key]
return spec


class GuestSsh(Guest, CommandCollector):
"""
Expand Down
18 changes: 18 additions & 0 deletions tmt/steps/discover/fmf.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ def to_spec(self) -> Union[str, _RawTestsWithAdjusts]:
)
return self.name

def to_minimal_spec(self) -> Union[str, _RawTestsWithAdjusts]:
return self.to_spec()


def normalize_tests_with_adjusts(
key_address: str,
Expand Down Expand Up @@ -307,6 +310,21 @@ class DiscoverFmfStepData(tmt.steps.discover.DiscoverStepData):
deprecated=tmt.options.Deprecated(since="1.66", hint="use 'ref' instead"),
)

def to_spec(self) -> tmt.steps._RawStepData:
return cast(
tmt.steps._RawStepData,
{**super().to_spec(), 'test': [test.to_spec() for test in self.test]},
)

def to_minimal_spec(self) -> tmt.steps._RawStepData:
spec = super().to_minimal_spec()
spec.pop('test', None) # type: ignore[typeddict-item]

tests = [test.to_minimal_spec() for test in self.test]
if tests:
spec['test'] = tests # type: ignore[typeddict-unknown-key]
return spec

def post_normalization(
self,
raw_data: tmt.steps._RawStepData,
Expand Down
35 changes: 32 additions & 3 deletions tmt/steps/discover/shell.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import copy
import shutil
from typing import Any, Optional, TypeVar, cast
from typing import Any, Callable, Optional, TypeVar, cast

import click
import fmf
Expand All @@ -14,7 +14,13 @@
import tmt.utils
import tmt.utils.git
from tmt._compat.typing import Self
from tmt.container import SerializableContainer, SpecBasedContainer, container, field
from tmt.container import (
SerializableContainer,
SpecBasedContainer,
container,
field,
option_to_key,
)
from tmt.steps import _RawStepData
from tmt.steps.prepare.distgit import insert_to_prepare_step
from tmt.utils import (
Expand Down Expand Up @@ -185,7 +191,29 @@ def to_spec(self) -> dict[str, Any]:
return data

def to_minimal_spec(self) -> dict[str, Any]:
return {key: value for key, value in self.to_spec().items() if value not in (None, [], {})}
data = {key: value for key, value in self.items() if value not in (None, [], {})}

# Some fields need special handling.
# Map them to functions that will correctly convert them.
field_map: dict[str, Callable[[Any], Any]] = {
'link': lambda link: link.to_spec(),
'require': lambda requires: [require.to_spec() for require in requires],
'recommend': lambda recommends: [recommend.to_spec() for recommend in recommends],
'check': lambda checks: [check.to_spec() for check in checks],
'test': str,
}

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, [], {}):
data.pop(key, None)
else:
data[key] = value

return data


@container
Expand Down Expand Up @@ -226,6 +254,7 @@ def to_spec(self) -> _RawDiscoverShellData:

def to_minimal_spec(self) -> _RawDiscoverShellData:
spec = {**super().to_minimal_spec()}
spec.pop('tests', None)
if self.tests:
spec['tests'] = [test.to_minimal_spec() for test in self.tests]
return cast(_RawDiscoverShellData, spec)
Expand Down
9 changes: 9 additions & 0 deletions tmt/steps/execute/internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ def to_spec(self) -> dict[str, Any]: # type: ignore[override]

return data

# ignore[override] & cast: two base classes define to_spec(), with conflicting
# formal types.
def to_minimal_spec(self) -> dict[str, Any]: # type: ignore[override]
data = cast(dict[str, Any], super().to_minimal_spec())
data.pop('script', None)
if self.script:
data['script'] = [str(script) for script in self.script]
return data


@tmt.steps.provides_method('tmt')
class ExecuteInternal(tmt.steps.execute.ExecutePlugin[ExecuteInternalData]):
Expand Down
14 changes: 14 additions & 0 deletions tmt/steps/execute/upgrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,20 @@ class ExecuteUpgradeData(ExecuteInternalData):
normalize=tmt.utils.normalize_string_list,
)

# ignore[override] & cast: two base classes define to_spec(), with conflicting
# formal types.
def to_spec(self) -> dict[str, Any]: # type: ignore[override]
return {**super().to_spec(), 'test': [test.to_spec() for test in self.test]}

# ignore[override] & cast: two base classes define to_spec(), with conflicting
# formal types.
def to_minimal_spec(self) -> dict[str, Any]: # type: ignore[override]
spec = super().to_minimal_spec()
spec.pop('test', None)
if self.test:
spec['test'] = [test.to_minimal_spec() for test in self.test]
return spec


@tmt.steps.provides_method('upgrade')
class ExecuteUpgrade(ExecuteInternal):
Expand Down
10 changes: 10 additions & 0 deletions tmt/steps/prepare/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ def to_spec(self) -> dict[str, Any]: # type: ignore[override]

return data

# ignore[override] & cast: two base classes define to_spec(), with conflicting
# formal types.
def to_minimal_spec(self) -> dict[str, Any]: # type: ignore[override]
data = cast(dict[str, Any], super().to_minimal_spec())
data.pop('script', None)
if self.script:
data['script'] = [str(script) for script in self.script]

return data


@tmt.steps.provides_method('shell')
class PrepareShell(tmt.steps.prepare.PreparePlugin[PrepareShellData]):
Expand Down
12 changes: 12 additions & 0 deletions tmt/steps/provision/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ class ProvisionStepData(tmt.steps.StepData):
),
)

def to_spec(self) -> tmt.steps._RawStepData:
spec = super().to_spec()
spec['hardware'] = self.hardware.to_spec() if self.hardware else None # type: ignore[typeddict-unknown-key]
return spec

def to_minimal_spec(self) -> tmt.steps._RawStepData:
spec = super().to_minimal_spec()
spec.pop('hardware', None) # type: ignore[typeddict-item]
if self.hardware:
spec['hardware'] = self.hardware.to_minimal_spec() # type: ignore[typeddict-unknown-key]
return spec


ProvisionStepDataT = TypeVar('ProvisionStepDataT', bound=ProvisionStepData)

Expand Down
46 changes: 44 additions & 2 deletions tmt/steps/provision/connect.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from typing import Any, Optional, Union
from typing import Any, Callable, Optional, Union, cast

import tmt
import tmt.guest
import tmt.steps
import tmt.steps.provision
import tmt.utils
from tmt.container import container, field
from tmt.container import container, field, option_to_key
from tmt.guest import RebootMode
from tmt.utils import Command, ShellScript
from tmt.utils.wait import Waiting
Expand Down Expand Up @@ -63,6 +63,48 @@ class ConnectGuestData(tmt.guest.GuestSshData):
unserialize=lambda serialized: None if serialized is None else ShellScript(serialized),
)

def to_spec(self) -> tmt.steps._RawStepData:
return cast(
tmt.steps._RawStepData,
{
**super().to_spec(),
'soft-reboot': str(self.soft_reboot)
if isinstance(self.soft_reboot, ShellScript)
else None,
'systemd-soft-reboot': str(self.systemd_soft_reboot)
if isinstance(self.systemd_soft_reboot, ShellScript)
else None,
'hard-reboot': str(self.hard_reboot)
if isinstance(self.hard_reboot, ShellScript)
else None,
},
)

def to_minimal_spec(self) -> tmt.steps._RawStepData:
spec = {**super().to_minimal_spec()}

# Some fields need special handling.
# Map them to functions that will correctly convert them.
field_map: dict[str, Callable[[Any], Any]] = {
'soft-reboot': lambda reboot: str(reboot) if isinstance(reboot, ShellScript) else None,
'systemd-soft-reboot': lambda reboot: (
str(reboot) if isinstance(reboot, ShellScript) else None
),
'hard-reboot': lambda reboot: str(reboot) if isinstance(reboot, ShellScript) else None,
}

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(tmt.steps._RawStepData, spec)

@classmethod
def from_plugin(
cls: type['ConnectGuestData'],
Expand Down
16 changes: 16 additions & 0 deletions tmt/steps/provision/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,22 @@ class MockGuestData(tmt.guest.GuestData):
normalize=tmt.utils.normalize_path,
)

def to_spec(self) -> tmt.steps._RawStepData:
return cast(
tmt.steps._RawStepData,
{
**super().to_spec(),
'rootdir': str(self.rootdir) if self.rootdir else None,
},
)

def to_minimal_spec(self) -> tmt.steps._RawStepData:
spec = {**super().to_minimal_spec()}
spec.pop('rootdir', None)
if self.rootdir:
spec['rootdir'] = str(self.rootdir)
return cast(tmt.steps._RawStepData, spec)


@container
class ProvisionMockData(MockGuestData, tmt.steps.provision.ProvisionStepData):
Expand Down
20 changes: 20 additions & 0 deletions tmt/steps/provision/testcloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,26 @@ class TestcloudGuestData(tmt.guest.GuestSshData):
""",
)

def to_spec(self) -> tmt.steps._RawStepData:
return cast(
tmt.steps._RawStepData,
{
**super().to_spec(),
'memory': str(self.memory) if self.memory is not None else None,
'disk': str(self.disk) if self.disk is not None else None,
},
)

def to_minimal_spec(self) -> tmt.steps._RawStepData:
spec = {**super().to_minimal_spec()}
spec.pop('memory', None)
spec.pop('disk', None)
if self.memory is not None:
spec['memory'] = str(self.memory)
if self.disk is not None:
spec['disk'] = str(self.disk)
return cast(tmt.steps._RawStepData, spec)

# TODO: custom handling for two fields - when the formatting moves into
# field(), this should not be necessary.
def show(
Expand Down
Loading