Skip to content

Commit

Permalink
chore: refactor "features"
Browse files Browse the repository at this point in the history
Instead of having a "loose" boolean in Application, formalize the
concept a bit by adding an AppFeatures object to be kept in the
AppMetadata (as part of the app's "static" info).

This complicates testing a bit because the features have to be enabled
at AppMetadata-creation-time and can't be set "a posteriori" anymore,
but the end result isn't too onerous and better reflects what the
applications will do anyway.
  • Loading branch information
tigarmo committed Oct 17, 2023
1 parent ef5620d commit c9bef6a
Show file tree
Hide file tree
Showing 5 changed files with 34 additions and 9 deletions.
3 changes: 2 additions & 1 deletion craft_application/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# with this program. If not, see <http://www.gnu.org/licenses/>.
"""Framework for *craft applications."""

from craft_application.application import Application, AppMetadata
from craft_application.application import Application, AppFeatures, AppMetadata
from craft_application import models
from craft_application.services import (
BaseService,
Expand All @@ -38,6 +38,7 @@
__all__ = [
"__version__",
"Application",
"AppFeatures",
"AppMetadata",
"models",
"BaseService",
Expand Down
15 changes: 10 additions & 5 deletions craft_application/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ def parsed_args(self) -> argparse.Namespace:
return self._parsed_command_args or argparse.Namespace()


@dataclass(frozen=True)
class AppFeatures:
"""Specific features that can be enabled/disabled per-application."""

build_secrets: bool = False
"""Support for build-time secrets."""


@final
@dataclass(frozen=True)
class AppMetadata:
Expand All @@ -63,6 +71,7 @@ class AppMetadata:
version: str = field(init=False)
source_ignore_patterns: list[str] = field(default_factory=lambda: [])
managed_instance_project_path = pathlib.PurePosixPath("/root/project")
features: AppFeatures = AppFeatures()

ProjectClass: type[models.Project] = models.Project

Expand Down Expand Up @@ -94,10 +103,6 @@ class Application:
:ivar services: A ServiceFactory for this application
"""

# Feature flag for build-time secrets. Applications that want to use the feature
# must set this to True.
enable_build_secrets = False

def __init__(
self,
app: AppMetadata,
Expand Down Expand Up @@ -384,7 +389,7 @@ def _transform_project_yaml(self, yaml_data: dict[str, Any]) -> dict[str, Any]:
self._expand_environment(yaml_data)

# Handle build secrets.
if self.enable_build_secrets:
if self.app.features.build_secrets:
self._render_secrets(yaml_data)

# Perform extra, application-specific transformations.
Expand Down
21 changes: 20 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,33 @@ def get_build_plan(self) -> list[models.BuildInfo]:


@pytest.fixture()
def app_metadata() -> craft_application.AppMetadata:
def features(request) -> dict[str, bool]:
"""Fixture that controls the enabled features.
To use it, mark the test with the features that should be enabled. For example:
@pytest.mark.enable_features("build_secrets")
def test_with_build_secrets(...)
"""
features = {}

for feature_marker in request.node.iter_markers("enable_features"):
for feature_name in feature_marker.args:
features[feature_name] = True

return features


@pytest.fixture()
def app_metadata(features) -> craft_application.AppMetadata:
with pytest.MonkeyPatch.context() as m:
m.setattr(metadata, "version", lambda _: "3.14159")
return craft_application.AppMetadata(
"testcraft",
"A fake app for testing craft-application",
ProjectClass=MyProject,
source_ignore_patterns=["*.snap", "*.charm", "*.starcraft"],
features=craft_application.AppFeatures(**features),
)


Expand Down
2 changes: 1 addition & 1 deletion tests/integration/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ def test_global_environment(
assert variables["project_version"] == "1.2.3"


@pytest.mark.enable_features("build_secrets")
def test_build_secrets_destructive(create_app, monkeypatch, tmp_path, capsys):
"""Test the use of build secrets in destructive mode."""
monkeypatch.setenv("CRAFT_DEBUG", "1")
Expand All @@ -269,7 +270,6 @@ def test_build_secrets_destructive(create_app, monkeypatch, tmp_path, capsys):
monkeypatch.setattr("sys.argv", ["testcraft", "prime", "-v", "--destructive-mode"])

app = create_app()
app.enable_build_secrets = True

# Set the environment variables that the project needs
monkeypatch.setenv("HOST_SOURCE_FOLDER", "secret-source")
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,13 +540,13 @@ def build_secrets_project(monkeypatch, tmp_path):


@pytest.mark.usefixtures("build_secrets_project")
@pytest.mark.enable_features("build_secrets")
def test_application_build_secrets(app_metadata, fake_services, monkeypatch, mocker):
monkeypatch.setenv("SECRET_VAR_1", "source-folder")
monkeypatch.setenv("SECRET_VAR_2", "secret-value")
spied_set_secrets = mocker.spy(craft_cli.emit, "set_secrets")

app = application.Application(app_metadata, fake_services)
app.enable_build_secrets = True
project = app.project

mypart = project.parts["mypart"]
Expand Down

0 comments on commit c9bef6a

Please sign in to comment.