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

refactor: subclass remote-build errors from CraftError #482

Merged
merged 2 commits into from
Sep 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
60 changes: 19 additions & 41 deletions craft_application/remote/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,63 +14,41 @@

"""Remote build errors."""

from dataclasses import dataclass
import craft_cli.errors

from craft_application.util import humanize_list

@dataclass(repr=True)
class RemoteBuildError(Exception):
"""Unexpected remote build error.

:param brief: Brief description of error.
:param details: Detailed information.
"""

brief: str
details: str | None = None

def __str__(self) -> str:
"""Return the string representation of the error."""
components = [self.brief]

if self.details:
components.append(self.details)

return "\n".join(components)
class RemoteBuildError(craft_cli.errors.CraftError):
"""Error for remote builds."""


class RemoteBuildGitError(RemoteBuildError):
"""Git repository cannot be prepared correctly."""
"""Git repository cannot be prepared correctly.

def __init__(self, message: str) -> None:
self.message = message
brief = "Git operation failed"
details = message
:param message: Git error message.
"""

super().__init__(brief=brief, details=details)
def __init__(self, message: str) -> None:
message = f"Git operation failed with: {message}"
super().__init__(message=message)


class UnsupportedArchitectureError(RemoteBuildError):
"""Unsupported architecture error."""
"""Unsupported architecture error.

:param architectures: List of unsupported architectures.
"""

def __init__(self, architectures: list[str]) -> None:
brief = "Architecture not supported by the remote builder."
details = (
message = (
"The following architectures are not supported by the remote builder: "
f"{architectures}.\nPlease remove them from the "
"architecture list and try again."
f"{humanize_list(architectures, 'and')}."
)
resolution = "Remove them from the architecture list and try again."

super().__init__(brief=brief, details=details)
super().__init__(message=message, resolution=resolution)


class RemoteBuildInvalidGitRepoError(RemoteBuildError):
Copy link
Contributor

Choose a reason for hiding this comment

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

This may be out of scope for this PR, but should RemoteBuildInvalidGitRepoError be a subclass of RemoteBuildGitError?

Copy link
Contributor

Choose a reason for hiding this comment

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

No, because RemoteBuildGitError is for errors that happened with git, whereas this is saying "this isn't git-able"

"""The Git repository is invalid for remote build.

:param brief: Brief description of error.
:param details: Detailed information.
"""

def __init__(self, details: str) -> None:
brief = "The Git repository is invalid for remote build."

super().__init__(brief=brief, details=details)
"""The Git repository is invalid for remote build."""
6 changes: 4 additions & 2 deletions craft_application/remote/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ def check_git_repo_for_remote_build(path: Path) -> None:

if git_type == GitType.INVALID:
raise RemoteBuildInvalidGitRepoError(
f"Could not find a git repository in {str(path)!r}"
message=f"Could not find a git repository in {str(path)!r}",
resolution="Initialize a git repository in the project directory",
)

if git_type == GitType.SHALLOW:
raise RemoteBuildInvalidGitRepoError(
"Remote build for shallow cloned git repos are no longer supported"
message="Remote builds for shallow cloned git repos are not supported",
resolution="Make a non-shallow clone of the repository",
)
11 changes: 11 additions & 0 deletions docs/reference/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@
Changelog
*********

4.2.4 (2024-Sep-19)
mr-cal marked this conversation as resolved.
Show resolved Hide resolved
-------------------

Remote build
============

- Remote build errors are now a subclass of ``CraftError``.

For a complete list of commits, check out the `4.2.4`_ release on GitHub.

4.2.3 (2024-Sep-18)
-------------------

Expand Down Expand Up @@ -278,3 +288,4 @@ For a complete list of commits, check out the `2.7.0`_ release on GitHub.
.. _4.2.1: https://github.com/canonical/craft-application/releases/tag/4.2.1
.. _4.2.2: https://github.com/canonical/craft-application/releases/tag/4.2.2
.. _4.2.3: https://github.com/canonical/craft-application/releases/tag/4.2.3
.. _4.2.4: https://github.com/canonical/craft-application/releases/tag/4.2.4
2 changes: 1 addition & 1 deletion tests/unit/git/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,6 +894,6 @@ def test_check_git_repo_for_remote_build_shallow(empty_working_directory):

with pytest.raises(
RemoteBuildInvalidGitRepoError,
match="Remote build for shallow cloned git repos are no longer supported",
match="Remote builds for shallow cloned git repos are not supported",
):
check_git_repo_for_remote_build(git_shallow_path)
26 changes: 11 additions & 15 deletions tests/unit/remote/test_errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,21 @@
from craft_application.remote import errors


def test_git_error():
"""Test RemoteBuildGitError."""
error = errors.RemoteBuildGitError(message="failed to push some refs to 'unknown'")

assert (
str(error) == "Git operation failed with: failed to push some refs to 'unknown'"
)


def test_unsupported_architecture_error():
"""Test UnsupportedArchitectureError."""
error = errors.UnsupportedArchitectureError(architectures=["amd64", "arm64"])

assert str(error) == (
"Architecture not supported by the remote builder.\nThe following "
"architectures are not supported by the remote builder: ['amd64', 'arm64'].\n"
"Please remove them from the architecture list and try again."
)
assert repr(error) == (
"UnsupportedArchitectureError(brief='Architecture not supported by the remote "
"builder.', details=\"The following architectures are not supported by the "
"remote builder: ['amd64', 'arm64'].\\nPlease remove them from the "
'architecture list and try again.")'
)

assert error.brief == "Architecture not supported by the remote builder."
assert error.details == (
"The following architectures are not supported by the remote builder: "
"['amd64', 'arm64'].\nPlease remove them from the architecture list and "
"try again."
"'amd64' and 'arm64'."
)
assert error.resolution == "Remove them from the architecture list and try again."
Comment on lines +18 to +35
Copy link
Contributor

Choose a reason for hiding this comment

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

Also perhaps out of scope, but I'd argue that these tests aren't really all that useful. Feels like checking the code coverage box, but doesn't really test any logic or flow, just something that will need updating when the string changes in the source.

Copy link
Contributor

Choose a reason for hiding this comment

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

The logic tested here is that we have our formatting strings correct. It prevents regressions where one may change f"my format {string}" to `"my format {string}".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, we do and do not test this kind of stuff throughout our projects, so I'm not sure what the team stance is.

FWIW, this won't increase code coverage unless there are no unit tests that raise the errors.

Given how many UX regressions we've experienced, I slightly lean towards doing tests like this to ensure the custom formatting in these errors will look as expected.

If someone on the team is interested in a writeup of our team's unit testing practices, that would be very welcome!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Commented before seeing Alex's response, which is better than my answer :)

57 changes: 57 additions & 0 deletions tests/unit/remote/test_git.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright 2024 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Remote-build git tests."""

import pytest
from craft_application.git import GitType
from craft_application.remote import errors, git


@pytest.fixture
def mock_get_git_repo_type(mocker):
return mocker.patch("craft_application.remote.git.get_git_repo_type")


def test_git_normal(tmp_path, mock_get_git_repo_type):
"""No-op for a normal git repo."""
mock_get_git_repo_type.return_value = GitType.NORMAL

assert git.check_git_repo_for_remote_build(tmp_path) is None


def test_git_invalid_error(tmp_path, mock_get_git_repo_type):
"""Raise an error for invalid git repos."""
mock_get_git_repo_type.return_value = GitType.INVALID

with pytest.raises(errors.RemoteBuildInvalidGitRepoError) as err:
git.check_git_repo_for_remote_build(tmp_path)

assert str(err.value) == f"Could not find a git repository in {str(tmp_path)!r}"
assert (
err.value.resolution == "Initialize a git repository in the project directory"
)


def test_git_shallow_clone_error(tmp_path, mock_get_git_repo_type):
"""Raise an error for shallowly cloned repos."""
mock_get_git_repo_type.return_value = GitType.SHALLOW

with pytest.raises(errors.RemoteBuildInvalidGitRepoError) as err:
git.check_git_repo_for_remote_build(tmp_path)

assert (
str(err.value) == "Remote builds for shallow cloned git repos are not supported"
)
assert err.value.resolution == "Make a non-shallow clone of the repository"
3 changes: 2 additions & 1 deletion tests/unit/remote/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
validate_architectures,
)
from craft_application.remote.utils import _SUPPORTED_ARCHS
from craft_application.util import humanize_list

###############################
# validate architecture tests #
Expand Down Expand Up @@ -57,7 +58,7 @@ def test_validate_architectures_error(archs, expected_archs):

assert (
"The following architectures are not supported by the remote builder: "
f"{expected_archs}"
f"{humanize_list(expected_archs, 'and')}"
) in str(raised.value)


Expand Down
Loading