diff --git a/docs/reference/imagecraft-yaml.rst b/docs/reference/imagecraft-yaml.rst index 344e8fbb..85fc9ed3 100644 --- a/docs/reference/imagecraft-yaml.rst +++ b/docs/reference/imagecraft-yaml.rst @@ -74,10 +74,10 @@ partitions. .. kitbash-field:: Project volumes :override-type: dict[str, Volume] -.. kitbash-field:: Volume volume_schema +.. kitbash-field:: GPTVolume volume_schema :prepend-name: volumes. -.. kitbash-field:: Volume structure +.. kitbash-field:: GPTVolume structure :prepend-name: volumes. :override-type: list[Partition] @@ -88,28 +88,28 @@ Partition keys The following keys can be declared for each partition listed in the volume's ``structure`` key. -.. kitbash-field:: StructureItem name +.. kitbash-field:: GPTStructureItem name :prepend-name: volumes..structure. -.. kitbash-field:: StructureItem id +.. kitbash-field:: GPTStructureItem id :prepend-name: volumes..structure. -.. kitbash-field:: StructureItem role +.. kitbash-field:: GPTStructureItem role :prepend-name: volumes..structure. -.. kitbash-field:: StructureItem structure_type +.. kitbash-field:: GPTStructureItem structure_type :prepend-name: volumes..structure. -.. kitbash-field:: StructureItem size +.. kitbash-field:: GPTStructureItem size :prepend-name: volumes..structure. -.. kitbash-field:: StructureItem filesystem +.. kitbash-field:: GPTStructureItem filesystem :prepend-name: volumes..structure. -.. kitbash-field:: StructureItem filesystem_label +.. kitbash-field:: GPTStructureItem filesystem_label :prepend-name: volumes..structure. -.. kitbash-field:: StructureItem partition_number +.. kitbash-field:: GPTStructureItem partition_number :prepend-name: volumes..structure. diff --git a/imagecraft/models/__init__.py b/imagecraft/models/__init__.py index cdda7ee3..0ea32e0b 100644 --- a/imagecraft/models/__init__.py +++ b/imagecraft/models/__init__.py @@ -22,17 +22,25 @@ get_partition_name, ) -from imagecraft.models.volume import FileSystem, Volume, Role, StructureItem +from imagecraft.models.volume import ( + FileSystem, + BaseVolume, + GPTVolume, + Volume, + Role, + GPTStructureItem, +) from imagecraft.models.grammar import get_grammar_aware_volume_keywords __all__ = [ "FileSystem", "Project", "Platform", + "GPTVolume", "Volume", "VolumeFilesystemsModel", "Role", - "StructureItem", + "GPTStructureItem", "get_partition_name", "get_grammar_aware_volume_keywords", ] diff --git a/imagecraft/models/volume.py b/imagecraft/models/volume.py index 80bdf7a3..997f60cb 100644 --- a/imagecraft/models/volume.py +++ b/imagecraft/models/volume.py @@ -38,9 +38,11 @@ ByteSize, Field, StringConstraints, + ValidationInfo, field_validator, model_validator, ) +from pydantic_core import PydanticCustomError MIB = 1 << 20 # 1 MiB (2^20) GIB = 1 << 30 # 1 GiB (2^30) @@ -54,6 +56,12 @@ STRUCTURE_NAME_COMPILED_REGEX = PARTITION_COMPILED_STRICT_REGEX STRUCTURE_SIZE_COMPILED_REGEX = re.compile(r"^(?P\d+)\s*(?P[M,G]{0,1})$") +_UUID_PATTERN = ( + r"[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}" +) +_MBR_TYPE_PATTERN = r"(?:0[Cc]|83)" +HYBRID_PARTITION_TYPE_REGEX = re.compile(rf"^{_MBR_TYPE_PATTERN},{_UUID_PATTERN}$") + GPT_NAME_MAX_LENGTH = 36 VOLUME_INVALID_MSG = ( @@ -114,6 +122,13 @@ def _validate_structure_size(value: str) -> str: ] +class MBRPartitionType(str, enum.Enum): + """Supported MBR volume types.""" + + FAT32 = "0C" + LINUX = "83" + + class GptType(str, enum.Enum): """Supported GUID Partition types.""" @@ -162,11 +177,14 @@ class Role(str, enum.Enum): """The partition stores the image's boot assets.""" SYSTEM_SEED = "system-seed" - """The partition stores the image's initial seed data used during first boot.""" + """The partition stores the seed used to provision the device.""" + + SYSTEM_SAVE = "system-save" + """The partition stores data preserved across factory resets.""" class StructureItem(CraftBaseModel): - """Structure item of the image.""" + """A single structure inside a volume.""" name: StructureName = Field( description="The name of the partition.", @@ -185,41 +203,11 @@ class StructureItem(CraftBaseModel): The name is interpreted as a UTF-16 encoded string. """ - id: uuid.UUID | None = Field( - default=None, - description="The partition's unique identifier.", - examples=[ - "6F8C47A6-1C2D-4B35-8B1E-9DE3C4E9E3FF", - "E3B0C442-98FC-1FC0-9B42-9AC7E5BD4B35", - ], - ) - """The partition's unique identifier. - - The identifier must be a unique 32-digit hexadecimal number in the GPT UUID format. - """ - role: Role = Field( description="The partition's purpose in the image.", examples=["system-data", "system-boot"], ) - structure_type: GptType = Field( - alias="type", - description="The type of the partition.", - examples=[ - "0FC63DAF-8483-4772-8E79-3D69D8477DE4", - "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", - ], - ) - """The type of the partition. - - For GPT partitions, the value must be the standard 32-digit hexadecimal number - associated with the type. - - This is distinct from the ``structure..id`` key, which is unique among - all partitions, regardless of type. - """ - size: StructureSize = Field( description="The size of the partition, in bytes.", examples=["256M", "6G"], @@ -247,24 +235,36 @@ class StructureItem(CraftBaseModel): Labels must be unique to their volume. """ - partition_number: int | None = Field( + content: None = Field( default=None, - description="(Optional) The partition number for this partition.", - ge=1, # GPT partitions are numbered 1-128 - le=128, + deprecated="Imagecraft does not support the content field.", ) - """The partition number for this partition. - If unset, partitions will start at 1 and be read in list order. If set, all - other partitions must also explicitly set their partition number as unique integers. - """ + min_size: None = Field( + default=None, deprecated="Imagecraft does not support the min-size field." + ) + + @field_validator("content", "min_size", mode="before") + @classmethod + def _field_not_supported(cls, value: object, info: ValidationInfo) -> None: + if value is not None: + field_alias = ( + info.field_name.replace("_", "-") + if info.field_name is not None + else "" + ) + raise PydanticCustomError( + "field_not_supported", + "Imagecraft does not support the '{field_alias}' key in volume structures.", + {"field_alias": field_alias}, + ) def __hash__(self) -> int: return hash(self.name) def __eq__(self, other: object) -> bool: - if type(other) is type(self): - return self.name == other.name + if type(other) is type(self) and hasattr(other, "name"): + return bool(self.name == other.name) return False @@ -275,16 +275,86 @@ def _set_default_filesystem_label(self) -> Self: return self +class MBRStructureItem(StructureItem): + """An item on an MBR-schema volume.""" + + structure_type: MBRPartitionType = Field( + alias="type", + description="The type of the partition.", + examples=[ + "0C", + "83", + ], + ) + """The MBR partition type code. + + For MBR partitions, the value is the partition type byte written as a + hexadecimal code, such as ``0C`` for FAT32 or ``83`` for Linux. + """ + + +class GPTStructureItem(StructureItem): + """An item on a GPT-schema volume.""" + + id: uuid.UUID | None = Field( + default=None, + description="The partition's unique identifier.", + examples=[ + "6F8C47A6-1C2D-4B35-8B1E-9DE3C4E9E3FF", + "E3B0C442-98FC-1FC0-9B42-9AC7E5BD4B35", + ], + ) + """The partition's unique identifier. + + The identifier must be a unique 32-digit hexadecimal number in the GPT UUID format. + """ + + structure_type: GptType = Field( + alias="type", + description="The type of the partition.", + examples=[ + "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", + ], + ) + """The type of the partition. + + For GPT partitions, the value must be the standard 32-digit hexadecimal number + associated with the type. + + This is distinct from the ``structure..id`` key, which is unique among + all partitions, regardless of type. + """ + + partition_number: int | None = Field( + default=None, + description="(Optional) The partition number for this partition.", + ge=1, # GPT partitions are numbered 1-128 + le=128, + ) + """The partition number for this partition. + + If unset, partitions will start at 1 and be read in list order. If set, all + other partitions must also explicitly set their partition number as unique integers. + """ + + class PartitionSchema(str, enum.Enum): """Supported partition schemas.""" GPT = "gpt" """The GUID partition table (GPT) schema.""" + MBR = "mbr" + """The Master Boot Record (MBR) schema.""" + + HYBRID = "mbr,gpt" + """A hybrid MBR/GPT schema, providing both partition tables simultaneously.""" + def _validate_structure_items_partition_numbers( - structures: Collection[StructureItem], -) -> Collection[StructureItem]: + structures: Collection[GPTStructureItem], +) -> Collection[GPTStructureItem]: partition_numbers = {structure.partition_number for structure in structures} # This could be loosened, but it would require us to generate these partition @@ -327,41 +397,62 @@ def _validate_structure_items_partition_numbers( return structures -StructureList = Annotated[ - list[StructureItem], +GPTStructureList = Annotated[ + list[GPTStructureItem], AfterValidator(_validate_structure_items_partition_numbers), ] -class Volume(CraftBaseModel): - """Volume defining properties of the image.""" +MBRStructureList = Annotated[list[MBRStructureItem], Field(min_length=1)] - volume_schema: Literal[PartitionSchema.GPT] = Field( - alias="schema", - description="The partitioning schema of the image.", - examples=["gpt"], - ) - """The partitioning schema of the image. - Imagecraft currently supports GUID partition tables (GPT). - """ +HybridPartitionType = Annotated[ + str, StringConstraints(pattern=HYBRID_PARTITION_TYPE_REGEX) +] - structure: StructureList = Field( - min_length=1, - description="The partitions that comprise the image.", + +class HybridStructureItem(StructureItem): + """An item on a hybrid MBR/GPT-schema volume.""" + + id: uuid.UUID | None = Field( + default=None, + description="The partition's unique identifier.", examples=[ - "[{name: efi, type: C12A7328-F81F-11D2-BA4B-00A0C93EC93B, filesystem: vfat, role: system-boot, filesystem-label: EFI System, size: 256M}, {name: rootfs, type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4, filesystem: ext4, filesystem-label: writable, role: system-data, size: 6G}]" + "6F8C47A6-1C2D-4B35-8B1E-9DE3C4E9E3FF", ], ) - """The partitions that comprise the image. + """The partition's unique identifier in the GPT table. - Each entry in the ``structure`` list represents a disk partition in the final - image. + The identifier must be a unique 32-digit hexadecimal number in the GPT UUID format. + """ + + structure_type: HybridPartitionType = Field( + alias="type", + description="The hybrid MBR/GPT type of the partition.", + examples=["0C,C12A7328-F81F-11D2-BA4B-00A0C93EC93B"], + ) + """The hybrid partition type, expressed as ','. + + The MBR component must be a two-digit hex code and the GPT component must be a + standard GUID. """ - @field_validator("structure", mode="after") + +HybridStructureList = Annotated[list[HybridStructureItem], Field(min_length=1)] + + +StructureList = GPTStructureList | MBRStructureList | HybridStructureList + + +class BaseVolume(CraftBaseModel): + """Base class for volume definitions.""" + + @field_validator("structure", mode="after", check_fields=False) @classmethod - def _validate_structure(cls, value: StructureList) -> StructureList: + def _validate_no_duplicate_filesystem_labels( + cls, value: list[StructureItem] + ) -> list[StructureItem]: + """Raise ValueError if any two structures share a filesystem label.""" fs_labels: list[str] = [str(v.filesystem_label) for v in value] fs_labels_set = set(fs_labels) @@ -374,3 +465,78 @@ def _validate_structure(cls, value: StructureList) -> StructureList: if count > 1 and item != "" ] raise ValueError(f"Duplicate filesystem labels: {dupes}") + + +class GPTVolume(BaseVolume): + """Volume with a GUID Partition Table (GPT) schema.""" + + volume_schema: Literal[PartitionSchema.GPT] = Field( + alias="schema", + description="The partitioning schema of the image.", + examples=["gpt"], + ) + """The partitioning schema of the image.""" + + structure: GPTStructureList = Field( + min_length=1, + description="The partitions that comprise the image.", + examples=[ + "[{name: efi, type: C12A7328-F81F-11D2-BA4B-00A0C93EC93B, filesystem: vfat, role: system-boot, filesystem-label: EFI System, size: 256M}, {name: rootfs, type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4, filesystem: ext4, filesystem-label: writable, role: system-data, size: 6G}]" + ], + ) + """The partitions that comprise the image. + + Each entry in the ``structure`` list represents a disk partition in the final + image. + """ + + +class MBRVolume(BaseVolume): + """Volume with a Master Boot Record (MBR) schema.""" + + volume_schema: Literal[PartitionSchema.MBR] = Field( + alias="schema", + description="The partitioning schema of the image.", + examples=["mbr"], + ) + """The partitioning schema of the image.""" + + structure: MBRStructureList = Field( + description="The partitions that comprise the image.", + examples=[ + "[{name: ubuntu-seed, type: 0C, filesystem: vfat, role: system-boot, size: 1200M}]" + ], + ) + """The partitions that comprise the image. + + Each entry in the ``structure`` list represents a disk partition in the final + image. + """ + + +class HybridVolume(BaseVolume): + """Volume with a hybrid MBR/GPT schema.""" + + volume_schema: Literal[PartitionSchema.HYBRID] = Field( + alias="schema", + description="The partitioning schema of the image.", + examples=["mbr,gpt"], + ) + """The partitioning schema of the image.""" + + structure: HybridStructureList = Field( + description="The partitions that comprise the image.", + examples=[ + "[{name: ubuntu-seed, type: 0C,C12A7328-F81F-11D2-BA4B-00A0C93EC93B, filesystem: vfat, role: system-seed, size: 1200M}]" + ], + ) + """The partitions that comprise the image. + + Each entry in the ``structure`` list represents a disk partition in the final + image. + """ + + +Volume = Annotated[ + GPTVolume | MBRVolume | HybridVolume, Field(discriminator="volume_schema") +] diff --git a/imagecraft/pack/gptutil.py b/imagecraft/pack/gptutil.py index f358905a..2f721ee7 100644 --- a/imagecraft/pack/gptutil.py +++ b/imagecraft/pack/gptutil.py @@ -21,7 +21,7 @@ from craft_cli import CraftError, emit -from imagecraft.models import Role, Volume +from imagecraft.models import GPTVolume, Role from imagecraft.pack import diskutil from imagecraft.subprocesses import run @@ -69,7 +69,7 @@ def _create_gpt_layout( *, imagepath: Path, sector_size: int, - layout: Volume, + layout: GPTVolume, ) -> None: """Partition image. @@ -102,9 +102,12 @@ def _create_gpt_layout( } if structure_item.role == Role.SYSTEM_BOOT.value: partition["bootable"] = None - if structure_item.id: + if hasattr(structure_item, "id") and structure_item.id is not None: partition["uuid"] = str(structure_item.id) - if structure_item.partition_number is not None: + if ( + hasattr(structure_item, "partition_number") + and structure_item.partition_number is not None + ): partition["partition-number"] = str(structure_item.partition_number) partitions.append(partition) start += sectors @@ -145,7 +148,7 @@ def secondary_partition_table_size(sector_size: int) -> int: PARTITION_RESERVED_SIZE: int = NON_MBR_START_OFFSET * SECTOR_SIZE_512 -def image_size(sector_size: int, layout: Volume) -> int: +def image_size(sector_size: int, layout: GPTVolume) -> int: """Determine necessary image size in bytes.""" # For now be conservative and replicate safe behavior of reserving the # first 1MiB of the image for partition table. This must be adapted when @@ -161,7 +164,7 @@ def image_size(sector_size: int, layout: Volume) -> int: def create_empty_gpt_image( imagepath: Path, sector_size: int, - layout: Volume, + layout: GPTVolume, ) -> None: """Create a zeroed image file with a GPT partition table, but no filesystems or data. diff --git a/imagecraft/services/image.py b/imagecraft/services/image.py index 18bf48f7..1c62abc9 100644 --- a/imagecraft/services/image.py +++ b/imagecraft/services/image.py @@ -28,7 +28,7 @@ from craft_cli import CraftError, emit from imagecraft.models import Project -from imagecraft.models.volume import PartitionSchema +from imagecraft.models.volume import GPTVolume, PartitionSchema from imagecraft.pack import gptutil from imagecraft.subprocesses import run @@ -83,7 +83,7 @@ def create_images(self) -> Mapping[str, pathlib.Path]: gptutil.create_empty_gpt_image( imagepath=image_path, sector_size=self._sector_size, - layout=volume, + layout=cast(GPTVolume, volume), ) case _: # Reaching this case is a bug. diff --git a/imagecraft/subprocesses.py b/imagecraft/subprocesses.py index 24eda664..53c9bfee 100644 --- a/imagecraft/subprocesses.py +++ b/imagecraft/subprocesses.py @@ -33,6 +33,7 @@ def run(cmd: str, *args: Any, **kwargs: Any) -> CompletedProcess[str]: "text": True, "check": True, "stdout": PIPE, + "stderr": PIPE, } for key, value in defaults.items(): if key not in kwargs: diff --git a/tests/integration/services/invalid-projects/gpt-unsupported-fields/error.txt b/tests/integration/services/invalid-projects/gpt-unsupported-fields/error.txt new file mode 100644 index 00000000..7c70f125 --- /dev/null +++ b/tests/integration/services/invalid-projects/gpt-unsupported-fields/error.txt @@ -0,0 +1,5 @@ +2 validation errors for VolumeFilesystemsModel +volumes.disk.gpt.structure.0.content + Imagecraft does not support the 'content' key in volume structures. [type=field_not_supported, input_value=[{'source': 'boot-assets/', 'target': '/'}], input_type=list] +volumes.disk.gpt.structure.0.min-size + Imagecraft does not support the 'min-size' key in volume structures. [type=field_not_supported, input_value='1G', input_type=str] diff --git a/tests/integration/services/invalid-projects/gpt-unsupported-fields/imagecraft.yaml b/tests/integration/services/invalid-projects/gpt-unsupported-fields/imagecraft.yaml new file mode 100644 index 00000000..90cac0f0 --- /dev/null +++ b/tests/integration/services/invalid-projects/gpt-unsupported-fields/imagecraft.yaml @@ -0,0 +1,37 @@ +name: simple-project +version: "0.1" +summary: An image that uses unsupported volume structure fields in a GPT schema. +description: | + The `content` and `min-size` keys in the volume schema are not supported by Imagecraft. + Because they are supported by other tools like snapd, we explicitly error out when these + fields are included in the file. +base: bare +build-base: devel + +platforms: + amd64: + arm64: + riscv64: + +filesystems: + default: + - mount: / + device: (volume/disk/rootfs) + +parts: + my-part: + plugin: nil + +volumes: + disk: + schema: gpt + structure: + - name: rootfs + role: system-data + type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4 + filesystem: ext4 + min-size: 1G # Not supported! + size: 6G + content: # This field is not supported! + - source: boot-assets/ + target: / diff --git a/tests/integration/services/invalid-projects/hybrid-unsupported-fields/error.txt b/tests/integration/services/invalid-projects/hybrid-unsupported-fields/error.txt new file mode 100644 index 00000000..5e8e9bc0 --- /dev/null +++ b/tests/integration/services/invalid-projects/hybrid-unsupported-fields/error.txt @@ -0,0 +1,5 @@ +2 validation errors for VolumeFilesystemsModel +volumes.disk.mbr,gpt.structure.0.content + Imagecraft does not support the 'content' key in volume structures. [type=field_not_supported, input_value=[{'source': 'boot-assets/', 'target': '/'}], input_type=list] +volumes.disk.mbr,gpt.structure.0.min-size + Imagecraft does not support the 'min-size' key in volume structures. [type=field_not_supported, input_value='1G', input_type=str] diff --git a/tests/integration/services/invalid-projects/hybrid-unsupported-fields/imagecraft.yaml b/tests/integration/services/invalid-projects/hybrid-unsupported-fields/imagecraft.yaml new file mode 100644 index 00000000..9b9e78da --- /dev/null +++ b/tests/integration/services/invalid-projects/hybrid-unsupported-fields/imagecraft.yaml @@ -0,0 +1,37 @@ +name: simple-project +version: "0.1" +summary: An image that uses unsupported volume structure fields in a hybrid MBR/GPT schema. +description: | + The `content` and `min-size` keys in the volume schema are not supported by Imagecraft. + Because they are supported by other tools like snapd, we explicitly error out when these + fields are included in the file. +base: bare +build-base: devel + +platforms: + amd64: + arm64: + riscv64: + +filesystems: + default: + - mount: / + device: (volume/disk/ubuntu-seed) + +parts: + my-part: + plugin: nil + +volumes: + disk: + schema: mbr,gpt + structure: + - name: ubuntu-seed + role: system-seed + filesystem: vfat + type: 0C,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + min-size: 1G # Not supported! + size: 1200M + content: # This field is not supported! + - source: boot-assets/ + target: / diff --git a/tests/integration/services/invalid-projects/mbr-unsupported-fields/error.txt b/tests/integration/services/invalid-projects/mbr-unsupported-fields/error.txt new file mode 100644 index 00000000..cbf56ce9 --- /dev/null +++ b/tests/integration/services/invalid-projects/mbr-unsupported-fields/error.txt @@ -0,0 +1,5 @@ +2 validation errors for VolumeFilesystemsModel +volumes.disk.mbr.structure.0.content + Imagecraft does not support the 'content' key in volume structures. [type=field_not_supported, input_value=[{'source': '$kernel:dtbs...ssets/', 'target': '/'}], input_type=list] +volumes.disk.mbr.structure.0.min-size + Imagecraft does not support the 'min-size' key in volume structures. [type=field_not_supported, input_value='1M', input_type=str] diff --git a/tests/integration/services/invalid-projects/mbr-unsupported-fields/imagecraft.yaml b/tests/integration/services/invalid-projects/mbr-unsupported-fields/imagecraft.yaml new file mode 100644 index 00000000..18b28368 --- /dev/null +++ b/tests/integration/services/invalid-projects/mbr-unsupported-fields/imagecraft.yaml @@ -0,0 +1,41 @@ +name: simple-project +version: "0.1" +summary: An image that has a `content` key in a volume. +description: | + The `content` key in the volume schema is not supported by Imagecraft. Because it is + supported by other tools like snapd, we explicitly error out when this field is + included in the file. +base: bare +build-base: devel + +platforms: + amd64: + arm64: + riscv64: + +filesystems: + default: + - mount: / + device: (volume/disk/rootfs) + +parts: + my-part: + plugin: nil + +volumes: + disk: + schema: mbr + structure: + - name: ubuntu-seed + role: system-seed + filesystem: vfat + type: 0C + min-size: 1M # Not supported! + size: 1200M + content: # This field is not supported! + # Copied from the pi gadget snap: + # https://github.com/canonical/pi-gadget/blob/24/gadget.yaml + - source: $kernel:dtbs/dtbs/overlays/ + target: /overlays + - source: boot-assets/ + target: / diff --git a/tests/integration/services/test_image.py b/tests/integration/services/test_image.py index 63d0718a..2b3a8d23 100644 --- a/tests/integration/services/test_image.py +++ b/tests/integration/services/test_image.py @@ -17,6 +17,8 @@ from craft_application import ServiceFactory from imagecraft.services.image import ImageService +pytestmark = [pytest.mark.usefixtures("enable_features")] + @pytest.fixture def image_service(default_factory: ServiceFactory, enable_features): diff --git a/tests/integration/services/test_project.py b/tests/integration/services/test_project.py new file mode 100644 index 00000000..8d228092 --- /dev/null +++ b/tests/integration/services/test_project.py @@ -0,0 +1,85 @@ +# This file is part of imagecraft. +# +# Copyright 2026 Canonical Ltd. +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License version 3, as published +# by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . +"""Integration tests for the project service.""" + +import pathlib + +import pydantic +import pytest +from imagecraft.application import APP_METADATA +from imagecraft.services.project import ImagecraftProjectService + +pytestmark = [pytest.mark.usefixtures("enable_features")] + + +@pytest.mark.parametrize( + "project_dir", + [ + pytest.param(path, id=path.name) + for path in sorted((pathlib.Path(__file__).parent / "valid-projects").iterdir()) + ], +) +def test_load_valid_project( + in_project_path: pathlib.Path, + project_dir: pathlib.Path, +): + project_file = in_project_path / "imagecraft.yaml" + project_file.write_text( + (project_dir / "imagecraft.yaml").read_text(), + encoding="utf-8", + ) + + project_service = ImagecraftProjectService( + app=APP_METADATA, + services=None, # ty: ignore[invalid-argument-type] + project_dir=in_project_path, + ) + project_service.configure(platform=None, build_for=None) + + project_service.get() + + +@pytest.mark.parametrize( + "project_dir", + [ + pytest.param(path, id=path.name) + for path in sorted( + (pathlib.Path(__file__).parent / "invalid-projects").iterdir() + ) + ], +) +def test_load_invalid_project( + in_project_path: pathlib.Path, + project_dir: pathlib.Path, +): + project_file = in_project_path / "imagecraft.yaml" + project_file.write_text( + (project_dir / "imagecraft.yaml").read_text(), + encoding="utf-8", + ) + expected_error = (project_dir / "error.txt").read_text(encoding="utf-8").strip() + + project_service = ImagecraftProjectService( + app=APP_METADATA, + services=None, # ty: ignore[invalid-argument-type] + project_dir=in_project_path, + ) + project_service.configure(platform=None, build_for=None) + + with pytest.raises(pydantic.ValidationError) as exc_info: + project_service.get() + + assert str(exc_info.value) == expected_error diff --git a/tests/integration/services/valid-projects/simple-hybrid/imagecraft.yaml b/tests/integration/services/valid-projects/simple-hybrid/imagecraft.yaml new file mode 100644 index 00000000..b69725dc --- /dev/null +++ b/tests/integration/services/valid-projects/simple-hybrid/imagecraft.yaml @@ -0,0 +1,47 @@ +name: simple-project +version: "0.1" +summary: An empty image with a hybrid MBR/GPT partition schema +description: This image has both MBR and GPT partition tables. +base: bare +build-base: devel + +platforms: + amd64: + arm64: + riscv64: + +filesystems: + default: + - mount: / + device: (volume/disk/ubuntu-seed) + +parts: + my-part: + plugin: nil + +volumes: + disk: + # Copied and modified from the Raspberry Pi gadget. + # https://github.com/canonical/pi-gadget/blob/24/gadget.yaml + schema: mbr,gpt + structure: + - name: ubuntu-seed + role: system-seed + filesystem: vfat + type: 0C,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + size: 1200M + - name: ubuntu-boot + role: system-boot + filesystem: vfat + type: 0C,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + size: 750M + - name: ubuntu-save + role: system-save + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 32M + - name: ubuntu-data + role: system-data + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 1500M diff --git a/tests/integration/services/valid-projects/simple-mbr/imagecraft.yaml b/tests/integration/services/valid-projects/simple-mbr/imagecraft.yaml new file mode 100644 index 00000000..9348f569 --- /dev/null +++ b/tests/integration/services/valid-projects/simple-mbr/imagecraft.yaml @@ -0,0 +1,48 @@ +name: simple-project +version: "0.1" +summary: An empty image with MBR partitions +description: This image has MBR partitions! Fancy? +base: bare +build-base: devel + +platforms: + amd64: + arm64: + riscv64: + +filesystems: + default: + - mount: / + device: (volume/disk/ubuntu-seed) + +parts: + my-part: + plugin: nil + +volumes: + disk: + # Copied and modified from the Raspberry Pi gadget: + # https://github.com/canonical/pi-gadget/blob/24/gadget.yaml + schema: mbr + structure: + - name: ubuntu-seed + role: system-seed + filesystem: vfat + type: 0C + size: 1200M + - name: ubuntu-boot + role: system-boot + filesystem: vfat + type: 0C + size: 750M + - name: ubuntu-save + role: system-save + filesystem: ext4 + type: "83" + size: 32M + - name: ubuntu-data + role: system-data + filesystem: ext4 + type: "83" + # XXX: make auto-grow to partition + size: 1500M diff --git a/tests/integration/services/valid-projects/simple/imagecraft.yaml b/tests/integration/services/valid-projects/simple/imagecraft.yaml new file mode 100644 index 00000000..b33660e4 --- /dev/null +++ b/tests/integration/services/valid-projects/simple/imagecraft.yaml @@ -0,0 +1,31 @@ +name: simple-project +version: "0.1" +summary: A test image +description: This image exists purely for testing purposes, yo! +base: bare +build-base: devel + +platforms: + amd64: + arm64: + riscv64: + +filesystems: + default: + - mount: / + device: (volume/disk/rootfs) + +parts: + my-part: + plugin: nil + +volumes: + disk: + schema: gpt + structure: + - name: rootfs + role: system-data + type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4 + filesystem: ext4 + filesystem-label: writable + size: 200 M diff --git a/tests/unit/models/test_project.py b/tests/unit/models/test_project.py index 7cd71d73..5d426772 100644 --- a/tests/unit/models/test_project.py +++ b/tests/unit/models/test_project.py @@ -23,6 +23,12 @@ from craft_application import util from craft_application.errors import CraftValidationError from imagecraft.models import Platform, Project, VolumeFilesystemsModel +from imagecraft.models.project import get_partition_name +from imagecraft.models.volume import ( + GPTStructureItem, + HybridStructureItem, + MBRStructureItem, +) from pydantic import ValidationError IMAGECRAFT_YAML_GENERIC = """ @@ -596,3 +602,49 @@ def test_volumes_fs_partitions_error(yaml_data, error): with pytest.raises(ValueError, match=error): volume_filesystems.get_partitions() + + +_GPT_STRUCTURE = GPTStructureItem.model_validate( + { + "name": "rootfs", + "role": "system-data", + "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + "filesystem": "ext4", + "size": "6G", + } +) + +_MBR_STRUCTURE = MBRStructureItem.model_validate( + { + "name": "ubuntu-seed", + "role": "system-seed", + "type": "0C", + "filesystem": "vfat", + "size": "1200M", + } +) + +_HYBRID_STRUCTURE = HybridStructureItem.model_validate( + { + "name": "ubuntu-boot", + "role": "system-boot", + "type": "0C,C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + "filesystem": "vfat", + "size": "750M", + } +) + + +@pytest.mark.parametrize( + ("volume_name", "structure", "expected"), + [ + pytest.param("disk", _GPT_STRUCTURE, "volume/disk/rootfs", id="gpt"), + pytest.param("disk", _MBR_STRUCTURE, "volume/disk/ubuntu-seed", id="mbr"), + pytest.param("disk", _HYBRID_STRUCTURE, "volume/disk/ubuntu-boot", id="hybrid"), + pytest.param( + "my-vol", _GPT_STRUCTURE, "volume/my-vol/rootfs", id="volume-name" + ), + ], +) +def test_get_partition_name(volume_name, structure, expected): + assert get_partition_name(volume_name, structure) == expected diff --git a/tests/unit/models/test_volume.py b/tests/unit/models/test_volume.py index b94ef49f..0a0f4262 100644 --- a/tests/unit/models/test_volume.py +++ b/tests/unit/models/test_volume.py @@ -18,12 +18,19 @@ import pytest from imagecraft.models import Role, Volume -from imagecraft.models.volume import StructureList +from imagecraft.models.volume import ( + GPTVolume, + HybridVolume, + MBRVolume, + StructureList, +) from pydantic import TypeAdapter, ValidationError def test_volume_valid(): - volume = Volume.model_validate( + volume_adapter = TypeAdapter(Volume) + + volume = volume_adapter.validate_python( { "schema": "gpt", "structure": [ @@ -69,7 +76,7 @@ def test_volume_valid(): ("error_value", "error_class", "volume"), [ ( - "1 validation error for Volume\nschema", + "1 validation error for Volume\n Unable to extract tag", ValidationError, { "structure": [ @@ -83,8 +90,8 @@ def test_volume_valid(): ], }, ), - ( - "1 validation error for Volume\nschema", + pytest.param( + "1 validation error for Volume\n Input tag", ValidationError, { "schema": "", @@ -98,9 +105,10 @@ def test_volume_valid(): } ], }, + id="missing-schema", ), - ( - "1 validation error for Volume\nstructure.0.name\n String should match pattern", + pytest.param( + "1 validation error for Volume\ngpt.structure.0.name\n String should match pattern", ValidationError, { "schema": "gpt", @@ -114,9 +122,10 @@ def test_volume_valid(): } ], }, + id="empty-name", ), ( - "1 validation error for Volume\nstructure.0.name\n String should match pattern", + "1 validation error for Volume\ngpt.structure.0.name\n String should match pattern", ValidationError, { "schema": "gpt", @@ -132,7 +141,7 @@ def test_volume_valid(): }, ), ( - "1 validation error for Volume\nstructure\n List should have at least 1 item after validation, not 0", + "1 validation error for Volume\ngpt.structure\n List should have at least 1 item after validation, not 0", ValidationError, { "schema": "gpt", @@ -140,7 +149,7 @@ def test_volume_valid(): }, ), ( - "1 validation error for Volume\nstructure.0.role\n Input should be 'system-data', 'system-boot' or 'system-seed'", + "1 validation error for Volume\ngpt.structure.0.role\n Input should be '", ValidationError, { "schema": "gpt", @@ -156,7 +165,7 @@ def test_volume_valid(): }, ), pytest.param( - "1 validation error for Volume\nstructure\n Value error, Duplicate filesystem labels: ['test']", + "1 validation error for Volume\ngpt.structure\n Value error, Duplicate filesystem labels: ['test']", ValidationError, { "schema": "gpt", @@ -180,7 +189,7 @@ def test_volume_valid(): id="duplicate-values", ), ( - "1 validation error for Volume\nstructure.0.type", + "1 validation error for Volume\ngpt.structure.0.type", ValidationError, { "schema": "gpt", @@ -196,7 +205,7 @@ def test_volume_valid(): }, ), ( - "1 validation error for Volume\nstructure.0.filesystem", + "1 validation error for Volume\ngpt.structure.0.filesystem", ValidationError, { "schema": "gpt", @@ -212,7 +221,7 @@ def test_volume_valid(): }, ), ( - "1 validation error for Volume\nstructure.0.id\n Input should be a valid UUID", + "1 validation error for Volume\ngpt.structure.0.id\n Input should be a valid UUID", ValidationError, { "schema": "gpt", @@ -229,7 +238,7 @@ def test_volume_valid(): }, ), ( - "1 validation error for Volume\nstructure.0.name\n String should have at most 36 characters", + "1 validation error for Volume\ngpt.structure.0.name\n String should have at most 36 characters", ValidationError, { "schema": "gpt", @@ -245,7 +254,7 @@ def test_volume_valid(): }, ), ( - "1 validation error for Volume\nstructure.0.size\n Field required", + "1 validation error for Volume\ngpt.structure.0.size\n Field required", ValidationError, { "schema": "gpt", @@ -260,7 +269,7 @@ def test_volume_valid(): }, ), ( - "1 validation error for Volume\nstructure.0.size\n Value error, size must be expressed in bytes, optionally with M or G unit.", + "1 validation error for Volume\ngpt.structure.0.size\n Value error, size must be expressed in bytes, optionally with M or G unit.", ValidationError, { "schema": "gpt", @@ -276,7 +285,7 @@ def test_volume_valid(): }, ), ( - "1 validation error for Volume\nstructure.0.size\n Value error, size must be expressed in bytes, optionally with M or G unit.", + "1 validation error for Volume\ngpt.structure.0.size\n Value error, size must be expressed in bytes, optionally with M or G unit.", ValidationError, { "schema": "gpt", @@ -292,7 +301,7 @@ def test_volume_valid(): }, ), ( - "1 validation error for Volume\nstructure\n Value error, Duplicate filesystem labels: ['label1']", + "1 validation error for Volume\ngpt.structure\n Value error, Duplicate filesystem labels: ['label1']", ValidationError, { "schema": "gpt", @@ -317,7 +326,7 @@ def test_volume_valid(): }, ), ( - "1 validation error for Volume\nstructure\n Value error, Duplicate filesystem labels: ['test2']", + "1 validation error for Volume\ngpt.structure\n Value error, Duplicate filesystem labels: ['test2']", ValidationError, { "schema": "gpt", @@ -347,13 +356,11 @@ def test_volume_invalid( error_class, volume, ): - def load_volume(volume, raises): - with pytest.raises(raises) as err: - Volume(**volume) - - return str(err.value) + volume_adapter = TypeAdapter(Volume, config={"title": "Volume"}) + with pytest.raises(error_class) as exc_info: + volume_adapter.validate_python(volume) - assert error_value in load_volume(volume, error_class) + assert error_value in str(exc_info.value) @pytest.mark.parametrize( @@ -460,3 +467,265 @@ def test_structure_list_success(structures: list[dict]): def test_structure_list_errors(structures: list[dict], error_message): with pytest.raises(ValidationError, match=error_message): TypeAdapter(StructureList).validate_python(structures) + + +# --------------------------------------------------------------------------- +# MBRVolume +# --------------------------------------------------------------------------- + +_MBR_SEED = { + "name": "ubuntu-seed", + "role": "system-seed", + "type": "0C", + "filesystem": "vfat", + "size": "1200M", +} +_MBR_DATA = { + "name": "ubuntu-data", + "role": "system-data", + "type": "83", + "filesystem": "ext4", + "size": "1500M", +} + + +@pytest.mark.parametrize( + ("structure_type", "structure"), + [ + ("0C", _MBR_SEED), + ("83", _MBR_DATA), + ], + ids=["fat32", "linux"], +) +def test_mbr_volume_valid(structure_type, structure): + volume_adapter = TypeAdapter(Volume) + volume = volume_adapter.validate_python({"schema": "mbr", "structure": [structure]}) + assert isinstance(volume, MBRVolume) + assert volume.volume_schema == "mbr" + assert volume.structure[0].structure_type == structure_type + + +def test_mbr_volume_invalid_type(): + volume_adapter = TypeAdapter(Volume) + with pytest.raises(ValidationError, match=r"mbr\.structure\.0\.type"): + volume_adapter.validate_python( + { + "schema": "mbr", + "structure": [ + { + "name": "rootfs", + "role": "system-data", + "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + "filesystem": "ext4", + "size": "6G", + } + ], + } + ) + + +def test_mbr_volume_duplicate_filesystem_labels(): + volume_adapter = TypeAdapter(Volume) + with pytest.raises( + ValidationError, + match=re.escape("Value error, Duplicate filesystem labels: ['ubuntu-data']"), + ): + volume_adapter.validate_python( + { + "schema": "mbr", + "structure": [ + {**_MBR_SEED, "filesystem-label": "ubuntu-data"}, + _MBR_DATA, + ], + } + ) + + +# --------------------------------------------------------------------------- +# GPTVolume vs MBRVolume discriminated union +# --------------------------------------------------------------------------- + + +def test_volume_gpt_schema_produces_gpt_volume(): + volume = TypeAdapter(Volume).validate_python( + { + "schema": "gpt", + "structure": [ + { + "name": "rootfs", + "role": "system-data", + "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + "filesystem": "ext4", + "size": "6G", + } + ], + } + ) + assert isinstance(volume, GPTVolume) + + +def test_volume_mbr_schema_produces_mbr_volume(): + volume = TypeAdapter(Volume).validate_python( + {"schema": "mbr", "structure": [_MBR_DATA]} + ) + assert isinstance(volume, MBRVolume) + + +# --------------------------------------------------------------------------- +# content and min-size fields +# --------------------------------------------------------------------------- + +_VALID_GPT_STRUCTURE = { + "name": "rootfs", + "role": "system-data", + "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + "filesystem": "ext4", + "size": "6G", +} +_VALID_MBR_STRUCTURE = _MBR_DATA + + +@pytest.mark.parametrize( + ("schema", "base_structure"), + [ + ("gpt", _VALID_GPT_STRUCTURE), + ("mbr", _VALID_MBR_STRUCTURE), + ], +) +def test_content_null_is_accepted(schema, base_structure): + TypeAdapter(Volume).validate_python( + {"schema": schema, "structure": [{**base_structure, "content": None}]} + ) + + +@pytest.mark.parametrize( + ("schema", "base_structure"), + [ + ("gpt", _VALID_GPT_STRUCTURE), + ("mbr", _VALID_MBR_STRUCTURE), + ], +) +def test_content_non_null_is_rejected(schema, base_structure): + with pytest.raises( + ValidationError, + match="Imagecraft does not support the 'content' key in volume structures.", + ): + TypeAdapter(Volume).validate_python( + { + "schema": schema, + "structure": [ + {**base_structure, "content": [{"source": "boot/", "target": "/"}]} + ], + } + ) + + +@pytest.mark.parametrize( + ("schema", "base_structure"), + [ + ("gpt", _VALID_GPT_STRUCTURE), + ("mbr", _VALID_MBR_STRUCTURE), + ], +) +def test_min_size_null_is_accepted(schema, base_structure): + TypeAdapter(Volume).validate_python( + {"schema": schema, "structure": [{**base_structure, "min-size": None}]} + ) + + +@pytest.mark.parametrize( + ("schema", "base_structure"), + [ + ("gpt", _VALID_GPT_STRUCTURE), + ("mbr", _VALID_MBR_STRUCTURE), + ], +) +def test_min_size_non_null_is_rejected(schema, base_structure): + with pytest.raises( + ValidationError, + match="Imagecraft does not support the 'min-size' key in volume structures.", + ): + TypeAdapter(Volume).validate_python( + {"schema": schema, "structure": [{**base_structure, "min-size": "16M"}]} + ) + + +# --------------------------------------------------------------------------- +# HybridVolume +# --------------------------------------------------------------------------- + +_HYBRID_SEED = { + "name": "ubuntu-seed", + "role": "system-seed", + "type": "0C,C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + "filesystem": "vfat", + "size": "1200M", +} +_HYBRID_DATA = { + "name": "ubuntu-data", + "role": "system-data", + "type": "83,0FC63DAF-8483-4772-8E79-3D69D8477DE4", + "filesystem": "ext4", + "size": "1500M", +} + + +@pytest.mark.parametrize( + ("structure_type", "structure"), + [ + ("0C,C12A7328-F81F-11D2-BA4B-00A0C93EC93B", _HYBRID_SEED), + ("83,0FC63DAF-8483-4772-8E79-3D69D8477DE4", _HYBRID_DATA), + ], + ids=["fat32-efi", "linux-linux-data"], +) +def test_hybrid_volume_valid(structure_type, structure): + volume = TypeAdapter(Volume).validate_python( + {"schema": "mbr,gpt", "structure": [structure]} + ) + assert isinstance(volume, HybridVolume) + assert volume.volume_schema == "mbr,gpt" + assert volume.structure[0].structure_type == structure_type + + +def test_hybrid_volume_schema_produces_hybrid_volume(): + volume = TypeAdapter(Volume).validate_python( + {"schema": "mbr,gpt", "structure": [_HYBRID_DATA]} + ) + assert isinstance(volume, HybridVolume) + + +@pytest.mark.parametrize( + "bad_type", + [ + pytest.param("0C", id="mbr-only"), + pytest.param("0FC63DAF-8483-4772-8E79-3D69D8477DE4", id="gpt-only"), + pytest.param( + "FF,0FC63DAF-8483-4772-8E79-3D69D8477DE4", id="invalid-mbr-component" + ), + pytest.param("0C,NOTAGUUID", id="invalid-gpt-component"), + ], +) +def test_hybrid_volume_invalid_type(bad_type): + with pytest.raises(ValidationError, match="String should match pattern"): + TypeAdapter(Volume).validate_python( + { + "schema": "mbr,gpt", + "structure": [{**_HYBRID_DATA, "type": bad_type}], + } + ) + + +def test_hybrid_volume_duplicate_filesystem_labels(): + with pytest.raises( + ValidationError, + match=re.escape("Value error, Duplicate filesystem labels: ['ubuntu-data']"), + ): + TypeAdapter(Volume).validate_python( + { + "schema": "mbr,gpt", + "structure": [ + {**_HYBRID_SEED, "filesystem-label": "ubuntu-data"}, + _HYBRID_DATA, + ], + } + ) diff --git a/tests/unit/pack/test_gptutil.py b/tests/unit/pack/test_gptutil.py index 1d966937..7640877a 100644 --- a/tests/unit/pack/test_gptutil.py +++ b/tests/unit/pack/test_gptutil.py @@ -16,13 +16,13 @@ import pytest from craft_cli.errors import CraftError -from imagecraft.models import Volume +from imagecraft.models import GPTVolume from imagecraft.pack import diskutil, gptutil @pytest.fixture def volume(): - return Volume.unmarshal( + return GPTVolume.unmarshal( { "schema": "gpt", "structure": [ diff --git a/tests/unit/pack/test_grubutil.py b/tests/unit/pack/test_grubutil.py index d0cf1637..54cc06c7 100644 --- a/tests/unit/pack/test_grubutil.py +++ b/tests/unit/pack/test_grubutil.py @@ -20,6 +20,7 @@ from craft_platforms import DebianArchitecture from imagecraft.errors import ImageError from imagecraft.models import Volume +from imagecraft.models.volume import GPTVolume from imagecraft.pack.chroot import Mount from imagecraft.pack.grubutil import _image_mounts, setup_grub from imagecraft.pack.image import Image @@ -27,7 +28,7 @@ @pytest.fixture def volume(): - return Volume.unmarshal( + return GPTVolume.unmarshal( { "schema": "gpt", "structure": [ @@ -109,7 +110,7 @@ def test_setup_grub(mocker, new_dir, volume, filesystem_mount): ("volume", "arch", "message"), [ ( - Volume.unmarshal( + GPTVolume.unmarshal( { "schema": "gpt", "structure": [ @@ -128,7 +129,7 @@ def test_setup_grub(mocker, new_dir, volume, filesystem_mount): "Skipping GRUB installation because no boot partition was found", ), ( - Volume.unmarshal( + GPTVolume.unmarshal( { "schema": "gpt", "structure": [ @@ -147,7 +148,7 @@ def test_setup_grub(mocker, new_dir, volume, filesystem_mount): "Skipping GRUB installation because no data partition was found", ), ( - Volume.unmarshal( + GPTVolume.unmarshal( { "schema": "gpt", "structure": [ @@ -207,7 +208,7 @@ def test_setup_grub_partitions(mocker, new_dir, volume, arch, emitter, message): [ ( "/dev/loop99", - Volume.unmarshal( + GPTVolume.unmarshal( { "schema": "gpt", "structure": [ @@ -251,7 +252,7 @@ def test_setup_grub_partitions(mocker, new_dir, volume, arch, emitter, message): ), ( "/dev/loop99", - Volume.unmarshal( + GPTVolume.unmarshal( { "schema": "gpt", "structure": [ @@ -303,7 +304,7 @@ def test_image_mounts( [ ( "/dev/loop99", - Volume.unmarshal( + GPTVolume.unmarshal( { "schema": "gpt", "structure": [ diff --git a/tests/unit/pack/test_image.py b/tests/unit/pack/test_image.py index ba441213..c418cc96 100644 --- a/tests/unit/pack/test_image.py +++ b/tests/unit/pack/test_image.py @@ -18,7 +18,7 @@ from unittest.mock import call import pytest -from imagecraft.models import Volume +from imagecraft.models import GPTVolume from imagecraft.pack.image import Image @@ -49,7 +49,7 @@ class TestImage: def test_loopdev(self, mocker, new_dir: Path): mock_run = mocker.patch("imagecraft.pack.image.run", side_effect=run) - volume = Volume.unmarshal( + volume = GPTVolume.unmarshal( { "schema": "gpt", "structure": [ @@ -154,7 +154,7 @@ def test_has_data_partition( new_dir: Path, has_data_partition: bool, # noqa: FBT001 ): - volume = Volume.unmarshal(volume_data) + volume = GPTVolume.unmarshal(volume_data) disk_path = Path(new_dir, "pc.img") disk_path.touch(exist_ok=True) image = Image( @@ -239,7 +239,7 @@ def test_has_boot_partition( new_dir: Path, has_boot_partition: bool, # noqa: FBT001 ): - volume = Volume.unmarshal(volume_data) + volume = GPTVolume.unmarshal(volume_data) disk_path = Path(new_dir, "pc.img") disk_path.touch(exist_ok=True) image = Image( diff --git a/tests/unit/services/test_image.py b/tests/unit/services/test_image.py index 221d84af..4a31b38f 100644 --- a/tests/unit/services/test_image.py +++ b/tests/unit/services/test_image.py @@ -18,7 +18,7 @@ import pytest from craft_application import AppMetadata, ServiceFactory from imagecraft.models import Project, Volume -from imagecraft.models.volume import PartitionSchema, StructureItem +from imagecraft.models.volume import GPTStructureItem, PartitionSchema from imagecraft.services.image import ImageService @@ -51,8 +51,8 @@ def mock_project(): vol = MagicMock(spec=Volume) vol.volume_schema = PartitionSchema.GPT vol.structure = [ - MagicMock(spec=StructureItem, name="efi", partition_number=None), - MagicMock(spec=StructureItem, name="rootfs", partition_number=2), + MagicMock(spec=GPTStructureItem, name="efi", partition_number=None), + MagicMock(spec=GPTStructureItem, name="rootfs", partition_number=2), ] vol.structure[0].name = "efi" vol.structure[1].name = "rootfs"