Skip to content

Commit 568cfa2

Browse files
authored
[Tests/CI] Push Test Coverage over 85% and adjust CI goal (#164)
* Add tests for apt/package_repo_info.py * Add tests for s3/__main__.py * Make: Bump coverage failure threshhold to 85 * Add digest file to .gitignore * Properly test oci/index.py * Create new oci test directory * Move existing oci tests * Create dedicated test file for oci/index.py * Bring s3_artifacts test coverage back up * Add tests for oci/layer.py
1 parent 39f7529 commit 568cfa2

File tree

9 files changed

+529
-4
lines changed

9 files changed

+529
-4
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
_build
2+
digest
23

34
# Byte-compiled / optimized / DLL files
45
__pycache__/

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ test-coverage: install-test
5050
$(POETRY) run pytest -k "not kms" --cov=gardenlinux --cov-report=xml tests/
5151

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

5555
test-debug: install-test
5656
$(POETRY) run pytest -k "not kms" -vvv -s
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
from types import SimpleNamespace
2+
3+
import gardenlinux.apt.package_repo_info as repoinfo
4+
5+
6+
class FakeAPTRepo:
7+
"""
8+
Fake replacement for apt_repo.APTTRepository.
9+
10+
- stores the contructor args for assertions
11+
- exposes `.packages` and `get_packages_by_name(name)`
12+
"""
13+
14+
def __init__(self, url, dist, components) -> None:
15+
self.url = url
16+
self.dist = dist
17+
self.components = components
18+
# list of objects with .package and .version attributes
19+
self.packages = []
20+
21+
def get_packages_by_name(self, name):
22+
return [p for p in self.packages if p.package == name]
23+
24+
25+
def test_gardenlinuxrepo_init(monkeypatch):
26+
"""
27+
Test if GardenLinuxRepo creates an internal APTRepo
28+
"""
29+
# Arrange
30+
monkeypatch.setattr(repoinfo, "APTRepository", FakeAPTRepo)
31+
32+
# Act
33+
gr = repoinfo.GardenLinuxRepo("dist-123")
34+
35+
# Assert
36+
assert gr.dist == "dist-123"
37+
assert gr.url == "http://packages.gardenlinux.io/gardenlinux"
38+
assert gr.components == ["main"]
39+
# Assert that patch works
40+
assert isinstance(gr.repo, FakeAPTRepo)
41+
# Assert that constructor actually built an internal repo instance
42+
assert gr.repo.url == gr.url
43+
assert gr.repo.dist == gr.dist
44+
assert gr.repo.components == gr.components
45+
46+
47+
def test_get_package_version_by_name(monkeypatch):
48+
# Arrange
49+
monkeypatch.setattr(repoinfo, "APTRepository", FakeAPTRepo)
50+
gr = repoinfo.GardenLinuxRepo("d")
51+
# Fake package objects
52+
gr.repo.packages = [
53+
SimpleNamespace(package="pkg-a", version="1.0"),
54+
SimpleNamespace(package="pkg-b", version="2.0"),
55+
] # type: ignore
56+
57+
# Act
58+
result = gr.get_package_version_by_name("pkg-a")
59+
60+
# Assert
61+
assert result == [("pkg-a", "1.0")]
62+
63+
64+
def test_get_packages_versions_returns_all_pairs(monkeypatch):
65+
# Arrange
66+
monkeypatch.setattr(repoinfo, "APTRepository", FakeAPTRepo)
67+
gr = repoinfo.GardenLinuxRepo("d")
68+
gr.repo.packages = [
69+
SimpleNamespace(package="aa", version="0.1"),
70+
SimpleNamespace(package="bb", version="0.2"),
71+
] # type: ignore
72+
73+
# Act
74+
pv = gr.get_packages_versions()
75+
76+
# Assert
77+
assert pv == [("aa", "0.1"), ("bb", "0.2")]
78+
79+
80+
def test_compare_repo_union_returns_all():
81+
"""
82+
When available_in_both=False, compare_repo returns entries for:
83+
- only names in A
84+
- only names in B
85+
- names in both but with different versions
86+
"""
87+
# Arrange
88+
a = SimpleNamespace(get_packages_versions=lambda: [("a", "1"), ("b", "2")])
89+
b = SimpleNamespace(get_packages_versions=lambda: [("b", "3"), ("c", "4")])
90+
91+
# Act
92+
result = repoinfo.compare_repo(a, b, available_in_both=False) # type: ignore
93+
94+
# Assert
95+
expected = {
96+
("a", "1", None),
97+
("b", "2", "3"),
98+
("c", None, "4"),
99+
}
100+
assert set(result) == expected
101+
102+
103+
def test_compare_repo_intersection_only():
104+
"""
105+
When available_in_both=True, only intersection names are considered;
106+
differences are only returned if versions differ.
107+
"""
108+
# Arrange (both share 'b' with different versions)
109+
a = SimpleNamespace(get_packages_versions=lambda: [("a", "1"), ("b", "2")])
110+
b = SimpleNamespace(get_packages_versions=lambda: [("b", "3"), ("c", "4")])
111+
112+
# Act
113+
result = repoinfo.compare_repo(a, b, available_in_both=True) # type: ignore
114+
115+
# Assert
116+
assert set(result) == {("b", "2", "3")}
117+
118+
119+
def test_compare_same_returns_empty():
120+
"""
121+
When both sets are identical, compare_repo should return an empty set.
122+
"""
123+
# Arrange
124+
a = SimpleNamespace(get_packages_versions=lambda: [("a", "1"), ("b", "2")])
125+
b = SimpleNamespace(get_packages_versions=lambda: [("a", "1"), ("b", "2")])
126+
127+
# Act / Assert
128+
assert repoinfo.compare_repo(a, b, available_in_both=False) == [] # type: ignore
129+
130+
131+
def test_compare_empty_returns_empty():
132+
"""
133+
If both sets are empty, compare_repo should return an empty set.
134+
"""
135+
# Arrange
136+
a = SimpleNamespace(get_packages_versions=lambda: [])
137+
b = SimpleNamespace(get_packages_versions=lambda: [])
138+
139+
# Act / Assert
140+
assert repoinfo.compare_repo(a, b, available_in_both=True) == [] # type: ignore

tests/oci/__init__.py

Whitespace-only changes.

tests/oci/test_index.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import io
2+
import json
3+
import pytest
4+
5+
from gardenlinux.oci.index import Index
6+
7+
8+
def test_index_init_and_json():
9+
"""Ensure Index init works correctly"""
10+
# Arrange
11+
idx = Index()
12+
13+
# Act
14+
json_bytes = idx.json
15+
decoded = json.loads(json_bytes.decode("utf-8"))
16+
17+
# Assert
18+
assert "schemaVersion" in idx
19+
assert isinstance(json_bytes, bytes)
20+
assert decoded == idx
21+
22+
23+
def test_manifests_as_dict():
24+
"""Verify manifests_as_dict returns correct keys for cname and digest cases."""
25+
# Arrange
26+
idx = Index()
27+
manifest_cname = {"digest": "sha256:abc", "annotations": {"cname": "foo"}}
28+
manifest_no_cname = {"digest": "sha256:def"}
29+
idx["manifests"] = [manifest_cname, manifest_no_cname]
30+
31+
# Act
32+
result = idx.manifests_as_dict
33+
34+
# Assert
35+
assert result["foo"] == manifest_cname
36+
assert result["sha256:def"] == manifest_no_cname
37+
38+
39+
def test_append_manifest_replace():
40+
"""Ensure append_manifest replaces existing manifest with same cname."""
41+
# Arrange
42+
idx = Index()
43+
idx["manifests"] = [
44+
{"annotations": {"cname": "old"}, "digest": "sha256:old"},
45+
{"annotations": {"cname": "other"}, "digest": "sha256:other"},
46+
]
47+
new_manifest = {"annotations": {"cname": "old"}, "digest": "sha256:new"}
48+
49+
# Act
50+
idx.append_manifest(new_manifest)
51+
52+
# Assert
53+
cnames = [manifest["annotations"]["cname"] for manifest in idx["manifests"]]
54+
assert "old" in cnames
55+
assert any(manifest["digest"] == "sha256:new" for manifest in idx["manifests"])
56+
57+
58+
def test_append_manifest_cname_not_found():
59+
"""Test appending new manifest if cname isn't found."""
60+
# Arrange
61+
idx = Index()
62+
idx["manifests"] = [{"annotations": {"cname": "foo"}, "digest": "sha256:foo"}]
63+
new_manifest = {"annotations": {"cname": "bar"}, "digest": "sha256:bar"}
64+
65+
# Act
66+
idx.append_manifest(new_manifest)
67+
68+
# Assert
69+
cnames = [manifest["annotations"]["cname"] for manifest in idx["manifests"]]
70+
assert "bar" in cnames
71+
72+
73+
@pytest.mark.parametrize(
74+
"bad_manifest",
75+
[
76+
"not-a-dict",
77+
{"annotations": {}},
78+
],
79+
)
80+
def test_append_invalid_input_raises(bad_manifest):
81+
"""Test proper error handling for invalid append_manifest input."""
82+
# Arrange
83+
idx = Index()
84+
85+
# Act / Assert
86+
with pytest.raises(RuntimeError):
87+
idx.append_manifest(bad_manifest)

tests/oci/test_layer.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import builtins
2+
import pytest
3+
from pathlib import Path
4+
5+
import gardenlinux.oci.layer as gl_layer
6+
7+
8+
class DummyLayer:
9+
"""Minimal stub for oras.oci.Layer"""
10+
11+
def __init__(self, blob_path, media_type=None, is_dir=False):
12+
self._init_args = (blob_path, media_type, is_dir)
13+
14+
def to_dict(self):
15+
return {"dummy": True}
16+
17+
18+
@pytest.fixture(autouse=True)
19+
def patch__Layer(monkeypatch):
20+
"""Replace oras.oci.Layer with DummyLayer in Layer's module."""
21+
monkeypatch.setattr(gl_layer, "_Layer", DummyLayer)
22+
yield
23+
24+
25+
def test_dict_property_returns_with_annotations(tmp_path):
26+
"""dict property should merge _Layer.to_dict() with annotations."""
27+
# Arrange
28+
blob = tmp_path / "blob.txt"
29+
blob.write_text("data")
30+
31+
# Act
32+
l = gl_layer.Layer(blob)
33+
result = l.dict
34+
35+
# Assert
36+
assert result["dummy"] is True
37+
assert "annotations" in result
38+
assert result["annotations"]["org.opencontainers.image.title"] == "blob.txt"
39+
40+
41+
def test_getitem_and_delitem_annotations(tmp_path):
42+
"""getitem should return annotations, delitem should clear them."""
43+
# Arrange
44+
blob = tmp_path / "blob.txt"
45+
blob.write_text("data")
46+
l = gl_layer.Layer(blob)
47+
48+
# Act / Assert (__getitem__)
49+
ann = l["annotations"]
50+
assert isinstance(ann, dict)
51+
assert "org.opencontainers.image.title" in ann
52+
53+
# Act / Assert (__delitem__)
54+
l.__delitem__("annotations")
55+
assert l._annotations == {}
56+
57+
58+
def test_getitem_invalid_key_raises(tmp_path):
59+
"""getitem with unsupported key should raise KeyError."""
60+
# Arrange
61+
blob = tmp_path / "blob.txt"
62+
blob.write_text("data")
63+
l = gl_layer.Layer(blob)
64+
65+
# Act / Assert
66+
with pytest.raises(KeyError):
67+
_ = l["invalid"]
68+
69+
70+
def test_setitem_annotations(tmp_path):
71+
"""setitem with supported keys should set annotations"""
72+
# Arrange
73+
blob = tmp_path / "blob.txt"
74+
blob.write_text("data")
75+
l = gl_layer.Layer(blob)
76+
77+
# Act
78+
new_ann = {"x": "y"}
79+
l.__setitem__("annotations", new_ann)
80+
81+
# Assert
82+
assert l._annotations == new_ann
83+
84+
85+
def test_setitem_annotations_invalid_raises(tmp_path):
86+
# Arrange
87+
blob = tmp_path / "blob.txt"
88+
blob.write_text("data")
89+
l = gl_layer.Layer(blob)
90+
91+
# Act / Assert
92+
with pytest.raises(KeyError):
93+
_ = l["invalid"]
94+
95+
96+
def test_len_iter(tmp_path):
97+
# Arrange
98+
blob = tmp_path / "blob.txt"
99+
blob.write_text("data")
100+
l = gl_layer.Layer(blob)
101+
102+
# Act
103+
keys = list(iter(l))
104+
105+
# Assert
106+
assert keys == ["annotations"]
107+
assert len(keys) == 1
108+
109+
110+
def test_gen_metadata_from_file(tmp_path):
111+
# Arrange
112+
blob = tmp_path / "blob.tar"
113+
blob.write_text("data")
114+
l = gl_layer.Layer(blob)
115+
116+
# Act
117+
arch = "amd64"
118+
metadata = gl_layer.Layer.generate_metadata_from_file_name(blob, arch)
119+
120+
# Assert
121+
assert metadata["file_name"] == "blob.tar"
122+
assert "media_type" in metadata
123+
assert metadata["annotations"]["io.gardenlinux.image.layer.architecture"] == arch
124+
125+
126+
def test_lookup_media_type_for_file_name(tmp_path):
127+
# Arrange
128+
blob = tmp_path / "blob.tar"
129+
blob.write_text("data")
130+
131+
# Act
132+
media_type = gl_layer.Layer.lookup_media_type_for_file_name(blob)
133+
from gardenlinux.constants import GL_MEDIA_TYPE_LOOKUP
134+
135+
assert media_type == GL_MEDIA_TYPE_LOOKUP["tar"]
136+
137+
138+
def test_lookup_media_type_for_file_name_invalid_raises(tmp_path):
139+
# Arrange / Act / Assert
140+
with pytest.raises(ValueError):
141+
gl_layer.Layer.lookup_media_type_for_file_name(tmp_path / "unknown.xyz")

0 commit comments

Comments
 (0)