Skip to content
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
_build
digest

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ test-coverage: install-test
$(POETRY) run pytest -k "not kms" --cov=gardenlinux --cov-report=xml tests/

test-coverage-ci: install-test
$(POETRY) run pytest -k "not kms" --cov=gardenlinux --cov-report=xml --cov-fail-under=50 tests/
$(POETRY) run pytest -k "not kms" --cov=gardenlinux --cov-report=xml --cov-fail-under=85 tests/

test-debug: install-test
$(POETRY) run pytest -k "not kms" -vvv -s
Expand Down
140 changes: 140 additions & 0 deletions tests/apt/test_package_repo_info.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
from types import SimpleNamespace

import gardenlinux.apt.package_repo_info as repoinfo


class FakeAPTRepo:
"""
Fake replacement for apt_repo.APTTRepository.

- stores the contructor args for assertions
- exposes `.packages` and `get_packages_by_name(name)`
"""

def __init__(self, url, dist, components) -> None:
self.url = url
self.dist = dist
self.components = components
# list of objects with .package and .version attributes
self.packages = []

def get_packages_by_name(self, name):
return [p for p in self.packages if p.package == name]


def test_gardenlinuxrepo_init(monkeypatch):
"""
Test if GardenLinuxRepo creates an internal APTRepo
"""
# Arrange
monkeypatch.setattr(repoinfo, "APTRepository", FakeAPTRepo)

# Act
gr = repoinfo.GardenLinuxRepo("dist-123")

# Assert
assert gr.dist == "dist-123"
assert gr.url == "http://packages.gardenlinux.io/gardenlinux"
assert gr.components == ["main"]
# Assert that patch works
assert isinstance(gr.repo, FakeAPTRepo)
# Assert that constructor actually built an internal repo instance
assert gr.repo.url == gr.url
assert gr.repo.dist == gr.dist
assert gr.repo.components == gr.components


def test_get_package_version_by_name(monkeypatch):
# Arrange
monkeypatch.setattr(repoinfo, "APTRepository", FakeAPTRepo)
gr = repoinfo.GardenLinuxRepo("d")
# Fake package objects
gr.repo.packages = [
SimpleNamespace(package="pkg-a", version="1.0"),
SimpleNamespace(package="pkg-b", version="2.0"),
] # type: ignore

# Act
result = gr.get_package_version_by_name("pkg-a")

# Assert
assert result == [("pkg-a", "1.0")]


def test_get_packages_versions_returns_all_pairs(monkeypatch):
# Arrange
monkeypatch.setattr(repoinfo, "APTRepository", FakeAPTRepo)
gr = repoinfo.GardenLinuxRepo("d")
gr.repo.packages = [
SimpleNamespace(package="aa", version="0.1"),
SimpleNamespace(package="bb", version="0.2"),
] # type: ignore

# Act
pv = gr.get_packages_versions()

# Assert
assert pv == [("aa", "0.1"), ("bb", "0.2")]


def test_compare_repo_union_returns_all():
"""
When available_in_both=False, compare_repo returns entries for:
- only names in A
- only names in B
- names in both but with different versions
"""
# Arrange
a = SimpleNamespace(get_packages_versions=lambda: [("a", "1"), ("b", "2")])
b = SimpleNamespace(get_packages_versions=lambda: [("b", "3"), ("c", "4")])

# Act
result = repoinfo.compare_repo(a, b, available_in_both=False) # type: ignore

# Assert
expected = {
("a", "1", None),
("b", "2", "3"),
("c", None, "4"),
}
assert set(result) == expected


def test_compare_repo_intersection_only():
"""
When available_in_both=True, only intersection names are considered;
differences are only returned if versions differ.
"""
# Arrange (both share 'b' with different versions)
a = SimpleNamespace(get_packages_versions=lambda: [("a", "1"), ("b", "2")])
b = SimpleNamespace(get_packages_versions=lambda: [("b", "3"), ("c", "4")])

# Act
result = repoinfo.compare_repo(a, b, available_in_both=True) # type: ignore

# Assert
assert set(result) == {("b", "2", "3")}


def test_compare_same_returns_empty():
"""
When both sets are identical, compare_repo should return an empty set.
"""
# Arrange
a = SimpleNamespace(get_packages_versions=lambda: [("a", "1"), ("b", "2")])
b = SimpleNamespace(get_packages_versions=lambda: [("a", "1"), ("b", "2")])

# Act / Assert
assert repoinfo.compare_repo(a, b, available_in_both=False) == [] # type: ignore


def test_compare_empty_returns_empty():
"""
If both sets are empty, compare_repo should return an empty set.
"""
# Arrange
a = SimpleNamespace(get_packages_versions=lambda: [])
b = SimpleNamespace(get_packages_versions=lambda: [])

# Act / Assert
assert repoinfo.compare_repo(a, b, available_in_both=True) == [] # type: ignore
Empty file added tests/oci/__init__.py
Empty file.
87 changes: 87 additions & 0 deletions tests/oci/test_index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import io
import json
import pytest

from gardenlinux.oci.index import Index


def test_index_init_and_json():
"""Ensure Index init works correctly"""
# Arrange
idx = Index()

# Act
json_bytes = idx.json
decoded = json.loads(json_bytes.decode("utf-8"))

# Assert
assert "schemaVersion" in idx
assert isinstance(json_bytes, bytes)
assert decoded == idx


def test_manifests_as_dict():
"""Verify manifests_as_dict returns correct keys for cname and digest cases."""
# Arrange
idx = Index()
manifest_cname = {"digest": "sha256:abc", "annotations": {"cname": "foo"}}
manifest_no_cname = {"digest": "sha256:def"}
idx["manifests"] = [manifest_cname, manifest_no_cname]

# Act
result = idx.manifests_as_dict

# Assert
assert result["foo"] == manifest_cname
assert result["sha256:def"] == manifest_no_cname


def test_append_manifest_replace():
"""Ensure append_manifest replaces existing manifest with same cname."""
# Arrange
idx = Index()
idx["manifests"] = [
{"annotations": {"cname": "old"}, "digest": "sha256:old"},
{"annotations": {"cname": "other"}, "digest": "sha256:other"},
]
new_manifest = {"annotations": {"cname": "old"}, "digest": "sha256:new"}

# Act
idx.append_manifest(new_manifest)

# Assert
cnames = [manifest["annotations"]["cname"] for manifest in idx["manifests"]]
assert "old" in cnames
assert any(manifest["digest"] == "sha256:new" for manifest in idx["manifests"])


def test_append_manifest_cname_not_found():
"""Test appending new manifest if cname isn't found."""
# Arrange
idx = Index()
idx["manifests"] = [{"annotations": {"cname": "foo"}, "digest": "sha256:foo"}]
new_manifest = {"annotations": {"cname": "bar"}, "digest": "sha256:bar"}

# Act
idx.append_manifest(new_manifest)

# Assert
cnames = [manifest["annotations"]["cname"] for manifest in idx["manifests"]]
assert "bar" in cnames


@pytest.mark.parametrize(
"bad_manifest",
[
"not-a-dict",
{"annotations": {}},
],
)
def test_append_invalid_input_raises(bad_manifest):
"""Test proper error handling for invalid append_manifest input."""
# Arrange
idx = Index()

# Act / Assert
with pytest.raises(RuntimeError):
idx.append_manifest(bad_manifest)
141 changes: 141 additions & 0 deletions tests/oci/test_layer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import builtins
import pytest
from pathlib import Path

import gardenlinux.oci.layer as gl_layer


class DummyLayer:
"""Minimal stub for oras.oci.Layer"""

def __init__(self, blob_path, media_type=None, is_dir=False):
self._init_args = (blob_path, media_type, is_dir)

def to_dict(self):
return {"dummy": True}


@pytest.fixture(autouse=True)
def patch__Layer(monkeypatch):
"""Replace oras.oci.Layer with DummyLayer in Layer's module."""
monkeypatch.setattr(gl_layer, "_Layer", DummyLayer)
yield


def test_dict_property_returns_with_annotations(tmp_path):
"""dict property should merge _Layer.to_dict() with annotations."""
# Arrange
blob = tmp_path / "blob.txt"
blob.write_text("data")

# Act
l = gl_layer.Layer(blob)
result = l.dict

# Assert
assert result["dummy"] is True
assert "annotations" in result
assert result["annotations"]["org.opencontainers.image.title"] == "blob.txt"


def test_getitem_and_delitem_annotations(tmp_path):
"""getitem should return annotations, delitem should clear them."""
# Arrange
blob = tmp_path / "blob.txt"
blob.write_text("data")
l = gl_layer.Layer(blob)

# Act / Assert (__getitem__)
ann = l["annotations"]
assert isinstance(ann, dict)
assert "org.opencontainers.image.title" in ann

# Act / Assert (__delitem__)
l.__delitem__("annotations")
assert l._annotations == {}


def test_getitem_invalid_key_raises(tmp_path):
"""getitem with unsupported key should raise KeyError."""
# Arrange
blob = tmp_path / "blob.txt"
blob.write_text("data")
l = gl_layer.Layer(blob)

# Act / Assert
with pytest.raises(KeyError):
_ = l["invalid"]


def test_setitem_annotations(tmp_path):
"""setitem with supported keys should set annotations"""
# Arrange
blob = tmp_path / "blob.txt"
blob.write_text("data")
l = gl_layer.Layer(blob)

# Act
new_ann = {"x": "y"}
l.__setitem__("annotations", new_ann)

# Assert
assert l._annotations == new_ann


def test_setitem_annotations_invalid_raises(tmp_path):
# Arrange
blob = tmp_path / "blob.txt"
blob.write_text("data")
l = gl_layer.Layer(blob)

# Act / Assert
with pytest.raises(KeyError):
_ = l["invalid"]


def test_len_iter(tmp_path):
# Arrange
blob = tmp_path / "blob.txt"
blob.write_text("data")
l = gl_layer.Layer(blob)

# Act
keys = list(iter(l))

# Assert
assert keys == ["annotations"]
assert len(keys) == 1


def test_gen_metadata_from_file(tmp_path):
# Arrange
blob = tmp_path / "blob.tar"
blob.write_text("data")
l = gl_layer.Layer(blob)

# Act
arch = "amd64"
metadata = gl_layer.Layer.generate_metadata_from_file_name(blob, arch)

# Assert
assert metadata["file_name"] == "blob.tar"
assert "media_type" in metadata
assert metadata["annotations"]["io.gardenlinux.image.layer.architecture"] == arch


def test_lookup_media_type_for_file_name(tmp_path):
# Arrange
blob = tmp_path / "blob.tar"
blob.write_text("data")

# Act
media_type = gl_layer.Layer.lookup_media_type_for_file_name(blob)
from gardenlinux.constants import GL_MEDIA_TYPE_LOOKUP

assert media_type == GL_MEDIA_TYPE_LOOKUP["tar"]


def test_lookup_media_type_for_file_name_invalid_raises(tmp_path):
# Arrange / Act / Assert
with pytest.raises(ValueError):
gl_layer.Layer.lookup_media_type_for_file_name(tmp_path / "unknown.xyz")
Loading