Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(application): attrs & tests for build planner #4588

Merged
merged 8 commits into from
Feb 20, 2024
Merged
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
43 changes: 33 additions & 10 deletions snapcraft/application.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2023 Canonical Ltd.
# Copyright 2023-2024 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
Expand All @@ -27,6 +27,7 @@
import craft_application.commands as craft_app_commands
import craft_application.models
import craft_cli
import pydantic
from craft_application import Application, AppMetadata, util
from craft_application.models import BuildInfo
from craft_cli import emit
Expand All @@ -36,36 +37,58 @@
from snapcraft import cli, errors, models, services
from snapcraft.commands import unimplemented
from snapcraft.models import Architecture
from snapcraft.models.project import validate_architectures
from snapcraft.providers import SNAPCRAFT_BASE_TO_PROVIDER_BASE
from snapcraft.utils import get_effective_base, get_host_architecture


class SnapcraftBuildPlanner(craft_application.models.BuildPlanner):
"""A project model that creates build plans."""

architectures: List[str | Architecture] = pydantic.Field(
default_factory=lambda: [get_host_architecture()]
)
base: str | None = None
build_base: str | None = None
name: str | None = None
project_type: str | None = None

@pydantic.validator("architectures", always=True)
def _validate_architecture_data( # pylint: disable=no-self-argument
cls, architectures: list[str | Architecture]
) -> list[Architecture]:
"""Validate architecture data.

Most importantly, converts architecture strings into Architecture objects.
"""
return validate_architectures(architectures)

def get_build_plan(self) -> List[BuildInfo]:
"""Get the build plan for this project."""
build_plan: List[BuildInfo] = []

architectures = cast(List[Architecture], getattr(self, "architectures", []))

for arch in architectures:
for arch in self.architectures:
# build_for will be a single element list
build_for = cast(list, arch.build_for)[0]
if isinstance(arch, Architecture):
build_on = arch.build_on
build_for = cast(list[str], arch.build_for)[0]
else:
build_on = arch
build_for = arch

Check warning on line 77 in snapcraft/application.py

View check run for this annotation

Codecov / codecov/patch

snapcraft/application.py#L76-L77

Added lines #L76 - L77 were not covered by tests

# TODO: figure out when to filter `all`
if build_for == "all":
build_for = get_host_architecture()

# build on will be a list of archs
for build_on in arch.build_on:
for build_on in cast(list[str], build_on):
base = SNAPCRAFT_BASE_TO_PROVIDER_BASE[
str(
get_effective_base(
base=getattr(self, "base", None),
build_base=getattr(self, "build_base", None),
name=getattr(self, "name", None),
project_type=getattr(self, "type", None),
base=self.base,
build_base=self.build_base,
name=self.name,
project_type=self.project_type,
)
)
]
Expand Down
6 changes: 3 additions & 3 deletions snapcraft/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def _validate_command_chain(command_chains: Optional[List[str]]) -> Optional[Lis
return command_chains


def _validate_architectures(architectures):
def validate_architectures(architectures):
"""Expand and validate architecture data.

Validation includes:
Expand Down Expand Up @@ -603,7 +603,7 @@ def _validate_epoch(cls, epoch):
@classmethod
def _validate_architecture_data(cls, architectures):
"""Validate architecture data."""
return _validate_architectures(architectures)
return validate_architectures(architectures)

@pydantic.validator("provenance")
@classmethod
Expand Down Expand Up @@ -794,7 +794,7 @@ class ArchitectureProject(models.CraftBaseModel, extra=pydantic.Extra.ignore):
@classmethod
def _validate_architecture_data(cls, architectures):
"""Validate architecture data."""
return _validate_architectures(architectures)
return validate_architectures(architectures)

@classmethod
def unmarshal(cls, data: Dict[str, Any]) -> "ArchitectureProject":
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/services/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@

def test_pack(package_service, default_factory, mocker):
mock_pack_snap = mocker.patch.object(pack, "pack_snap")
mock_linters_run = mocker.patch.object(linters, "run_linters")
mock_linters_report = mocker.patch.object(linters, "report")
mocker.patch.object(linters, "run_linters")
mocker.patch.object(linters, "report")

package_service.pack(prime_dir=Path("prime"), dest=Path())

Expand Down
145 changes: 145 additions & 0 deletions tests/unit/test_application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright 2024 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 warranty of
# MERCHANTABILITY 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 <http://www.gnu.org/licenses/>.
"""Unit tests for application classes."""
from typing import cast

import pytest
from craft_providers import bases

from snapcraft import application
from snapcraft.models.project import Architecture


@pytest.fixture(
params=[
["amd64"],
["riscv64"],
["amd64", "riscv64", "s390x"],
[Architecture(build_on="amd64", build_for="riscv64")],
]
)
def architectures(request):
lengau marked this conversation as resolved.
Show resolved Hide resolved
return request.param


@pytest.mark.parametrize(
("base", "expected_base"),
[
("core20", bases.BaseName("ubuntu", "20.04")),
("core22", bases.BaseName("ubuntu", "22.04")),
("core24", bases.BaseName("ubuntu", "24.04")),
],
)
def test_build_planner_success_default_architecture(base, expected_base):
data = {
"base": base,
}

planner = application.SnapcraftBuildPlanner.parse_obj(data)

actual = planner.get_build_plan()

archs = cast(list[Architecture], planner.architectures)
for build_info in actual:
assert build_info.base == expected_base
assert [build_info.build_for] in [a.build_for for a in archs]
assert [build_info.build_on] in [a.build_on for a in archs]
assert build_info.platform == f"{expected_base.name}@{expected_base.version}"


@pytest.mark.parametrize(
("base", "expected_base"),
[
("core20", bases.BaseName("ubuntu", "20.04")),
("core22", bases.BaseName("ubuntu", "22.04")),
("core24", bases.BaseName("ubuntu", "24.04")),
],
)
def test_build_planner_success_base_only(architectures, base, expected_base):
data = {
"architectures": architectures,
"base": base,
}

planner = application.SnapcraftBuildPlanner.parse_obj(data)

actual = planner.get_build_plan()

archs = cast(list[Architecture], planner.architectures)
for build_info in actual:
assert build_info.base == expected_base
assert [build_info.build_for] in [a.build_for for a in archs]
assert [build_info.build_on] in [a.build_on for a in archs]
assert build_info.platform == f"{expected_base.name}@{expected_base.version}"


@pytest.mark.parametrize("base", ["core20", "core22", "core24"])
@pytest.mark.parametrize(
("build_base", "expected_base"),
[
("core20", bases.BaseName("ubuntu", "20.04")),
("core22", bases.BaseName("ubuntu", "22.04")),
("core24", bases.BaseName("ubuntu", "24.04")),
],
)
def test_build_planner_success_build_base(
architectures, base, build_base, expected_base
):
data = {
"architectures": architectures,
"base": base,
"build-base": build_base,
}

planner = application.SnapcraftBuildPlanner.parse_obj(data)

actual = planner.get_build_plan()

archs = cast(list[Architecture], planner.architectures)
for build_info in actual:
assert build_info.base == expected_base
assert [build_info.build_for] in [a.build_for for a in archs]
assert [build_info.build_on] in [a.build_on for a in archs]
assert build_info.platform == f"{expected_base.name}@{expected_base.version}"


@pytest.mark.parametrize("base", ["core20", "core22", "core24"])
@pytest.mark.parametrize(
("build_base", "expected_base"),
[
("core20", bases.BaseName("ubuntu", "20.04")),
("core22", bases.BaseName("ubuntu", "22.04")),
("core24", bases.BaseName("ubuntu", "24.04")),
],
)
def test_build_planner_success_architecture_all(base, build_base, expected_base):
data = {
"architectures": [{"build-on": ["amd64"], "build-for": "all"}],
"base": base,
"build-base": build_base,
}

planner = application.SnapcraftBuildPlanner.parse_obj(data)

actual = planner.get_build_plan()

architectures = cast(list[Architecture], planner.architectures)
for build_info in actual:
assert build_info.base == expected_base
assert [build_info.build_on] in [a.build_on for a in architectures]
assert build_info.platform == f"{expected_base.name}@{expected_base.version}"

assert "all" not in [a.build_on for a in architectures]
Loading