diff --git a/productmd/composeinfo.py b/productmd/composeinfo.py index bf853cc..fe31793 100644 --- a/productmd/composeinfo.py +++ b/productmd/composeinfo.py @@ -28,6 +28,7 @@ """ import re +import warnings import productmd.common from productmd.common import Header, RELEASE_VERSION_RE @@ -742,33 +743,44 @@ def __init__(self, variant): self._variant = variant self.parent = None - # paths: product certificate + # paths: product certificate (not in _fields; handled separately, not Location-aware) self.identity = {} + # Binary + self.os_tree = {} + self.packages = {} + self.repository = {} + self.isos = {} + self.images = {} + self.jigdos = {} + # Source + self.source_tree = {} + self.source_packages = {} + self.source_repository = {} + self.source_isos = {} + self.source_jigdos = {} + # Debug + self.debug_tree = {} + self.debug_packages = {} + self.debug_repository = {} + self._fields = [ - # binary "os_tree", "packages", "repository", "isos", "images", "jigdos", - # source "source_tree", "source_packages", "source_repository", "source_isos", "source_jigdos", - # debug "debug_tree", "debug_packages", "debug_repository", - # debug isos and jigdos are not supported ] - for name in self._fields: - setattr(self, name, {}) - # Parallel storage for Location objects (v2.0 round-trip fidelity) # Structure: {field_name: {arch: Location}} self._locations = {} @@ -787,70 +799,114 @@ def deserialize(self, data, file_version=None): def _deserialize_v1(self, data): """Deserialize from v1.x format (path strings).""" - paths = data - for arch in sorted(self._variant.arches): - for name in self._fields: - value = paths.get(name, {}).get(arch, None) + for name in self._fields: + field_data = data.get(name, {}) + for arch, value in sorted(field_data.items()): if value: field = getattr(self, name) field[arch] = value def _deserialize_v2(self, data): """Deserialize from v2.0 format (Location objects).""" - paths = data - for arch in sorted(self._variant.arches): - for name in self._fields: - value = paths.get(name, {}).get(arch, None) + for name in self._fields: + field_data = data.get(name, {}) + for arch, value in sorted(field_data.items()): if value: - if isinstance(value, dict) and "url" in value: - # v2.0 Location object - loc = Location.from_dict(value) - field = getattr(self, name) - field[arch] = loc.local_path - self._locations.setdefault(name, {})[arch] = loc - else: - # Fallback: plain string (shouldn't happen in v2.0) - field = getattr(self, name) - field[arch] = value + if not isinstance(value, dict): + raise TypeError("v2.0 path '%s[%s]' must be a Location dict, got %s" % (name, arch, type(value).__name__)) + loc = Location.from_dict(value) + field = getattr(self, name) + field[arch] = loc.local_path + self._locations.setdefault(name, {})[arch] = loc + + def set_location(self, field_name, arch, location): + """ + Set a Location for a path field, updating both the path string + and the internal Location storage. + + :param field_name: Path field name (e.g., ``"repository"``, ``"os_tree"``) + :type field_name: str + :param arch: Architecture key (e.g., ``"x86_64"``, ``"src"``) + :type arch: str + :param location: Location object + :type location: :class:`productmd.location.Location` + :raises ValueError: if ``field_name`` is not a known path field + :raises TypeError: if ``location`` is not a Location instance + """ + if field_name not in self._fields: + raise ValueError("Unknown path field: %s" % field_name) + if not isinstance(location, Location): + raise TypeError("Expected Location, got %s" % type(location).__name__) + field = getattr(self, field_name) + field[arch] = location.local_path + self._locations.setdefault(field_name, {})[arch] = location + + def get_location(self, field_name, arch): + """ + Get the Location object for a path field, or None. + + :param field_name: Path field name (e.g., ``"repository"``, ``"os_tree"``) + :type field_name: str + :param arch: Architecture key (e.g., ``"x86_64"``, ``"src"``) + :type arch: str + :return: Location object or None + :rtype: :class:`productmd.location.Location` or None + """ + return self._locations.get(field_name, {}).get(arch) def serialize(self, data, output_version=None): self.validate() + self._warn_unknown_arches() if output_version is not None and output_version >= VERSION_2_0: self._serialize_v2(data) else: self._serialize_v1(data) + def _warn_unknown_arches(self): + """Emit a warning if any path field has arch keys not in variant.arches.""" + for name in self._fields: + field = getattr(self, name) + for arch in field: + if arch not in self._variant.arches: + warnings.warn( + "Variant '%s': path '%s' has arch '%s' " + "which is not in variant.arches %s" % (self._variant.uid, name, arch, sorted(self._variant.arches)), + stacklevel=4, + ) + def _serialize_v1(self, data): """Serialize in v1.x format (path strings).""" paths = data - for arch in sorted(self._variant.arches): - for name in self._fields: - field = getattr(self, name) - value = field.get(arch, None) + for name in self._fields: + field = getattr(self, name) + for arch in sorted(field.keys()): + value = field[arch] if value: - paths.setdefault(name, {})[arch] = value + if isinstance(value, Location): + paths.setdefault(name, {})[arch] = value.local_path + else: + paths.setdefault(name, {})[arch] = value def _serialize_v2(self, data): """Serialize in v2.0 format (Location objects).""" paths = data - for arch in sorted(self._variant.arches): - for name in self._fields: - field = getattr(self, name) - value = field.get(arch, None) - if value: - # Use stored Location if available (round-trip) - loc = self._locations.get(name, {}).get(arch, None) - if loc is not None: - paths.setdefault(name, {})[arch] = loc.serialize() - else: - # Synthesize Location from path string - loc = Location( - url=value, - size=None, - checksum=None, - local_path=value, - ) - paths.setdefault(name, {})[arch] = loc.serialize() + for name in self._fields: + field = getattr(self, name) + for arch in sorted(field.keys()): + value = field[arch] + if not value: + continue + # Check _locations first (round-trip from deserialization) + loc = self._locations.get(name, {}).get(arch) + if loc is not None: + paths.setdefault(name, {})[arch] = loc.serialize() + elif isinstance(value, Location): + # User assigned a Location object directly + paths.setdefault(name, {})[arch] = value.serialize() + else: + raise ValueError( + "Cannot serialize '%s[%s]' as v2.0: no Location set. Use set_location() or upgrade_to_v2()." % (name, arch) + ) class Variant(VariantBase): diff --git a/productmd/convert.py b/productmd/convert.py index 5457bc5..969e3d8 100644 --- a/productmd/convert.py +++ b/productmd/convert.py @@ -224,10 +224,10 @@ def _iter_variant_paths(variant: object) -> Iterator[LocationEntry]: for field_name in paths._fields: field = getattr(paths, field_name) for arch, path in field.items(): - loc = paths._locations.get(field_name, {}).get(arch) + loc = paths.get_location(field_name, arch) def _setter(loc, _paths=paths, _field=field_name, _arch=arch): - _paths._locations.setdefault(_field, {})[_arch] = loc + _paths.set_location(_field, _arch, loc) yield LocationEntry( MetadataType.VARIANT_PATH, @@ -252,21 +252,28 @@ def _iter_composeinfo(composeinfo: object) -> Iterator[LocationEntry]: def _copy_metadata(obj: object) -> object: """Create a deep copy of a metadata object via serialize/deserialize. - Serializes using the object's current output_version to avoid - triggering side effects (e.g. lazy Location synthesis on v1.x data). + Always serializes as v1.2 to avoid triggering v2.0 strict Location + requirements — the copy is a transient intermediate, not a format + conversion. The caller (upgrade_to_v2 / downgrade_to_v1) will set + Locations and output_version on the copy afterwards. + + .. note:: + + Because the copy round-trips through v1.2, any v2.0-only data + (e.g. ``contents`` / ``FileEntry`` on OCI Locations) is not + preserved. This is intentional — ``upgrade_to_v2`` is designed + for v1.x → v2.0 conversion, not for re-processing existing + v2.0 metadata. :param obj: Metadata object to copy :return: New metadata object with identical data """ data = {} - # Use the object's current output version to avoid side effects. - # For v1.x data, this prevents the v2.0 code path from running - # (which would lazily create Location objects on the originals). - current_version = getattr(obj, "output_version", None) - if current_version is not None: - obj.serialize(data, force_version=current_version) - else: - obj.serialize(data) + # Always serialize as v1.2 for the copy round-trip. v2.0 serialization + # requires every path to have a Location, which may not be true on the + # source object. The v1.2 path preserves all data without that + # requirement. + obj.serialize(data, force_version=VERSION_1_2) new_obj = type(obj)() new_obj.deserialize(data) return new_obj diff --git a/tests/test_composeinfo_v2.py b/tests/test_composeinfo_v2.py index 42336c3..179a126 100644 --- a/tests/test_composeinfo_v2.py +++ b/tests/test_composeinfo_v2.py @@ -23,7 +23,7 @@ def _create_composeinfo(): return ci -def _add_server_variant(ci): +def _add_server_variant(ci, with_locations=False): """Add a Server variant with paths to the ComposeInfo.""" variant = Variant(ci) variant.id = "Server" @@ -41,6 +41,17 @@ def _add_server_variant(ci): "aarch64": "Server/aarch64/os/Packages", } + if with_locations: + for arch in ["x86_64", "aarch64"]: + for field, path in [("os_tree", "Server/%s/os" % arch), ("packages", "Server/%s/os/Packages" % arch)]: + loc = Location( + url="https://cdn.example.com/%s" % path, + size=2847, + checksum="sha256:" + "a" * 64, + local_path=path, + ) + variant.paths.set_location(field, arch, loc) + ci.variants.add(variant) return ci @@ -76,7 +87,7 @@ class TestVariantPathsSerialization: def test_serialize_has_paths(self, version): """Test that both formats include variant paths.""" ci = _create_composeinfo() - _add_server_variant(ci) + _add_server_variant(ci, with_locations=(version >= VERSION_2_0)) data = {} ci.serialize(data, force_version=version) @@ -107,7 +118,7 @@ def test_serialize_v12_paths_are_strings(self): def test_serialize_v20_paths_are_locations(self): """Test v2.0 serialization produces Location objects.""" ci = _create_composeinfo() - _add_server_variant(ci) + _add_server_variant(ci, with_locations=True) data = {} ci.serialize(data, force_version=VERSION_2_0) @@ -120,28 +131,37 @@ def test_serialize_v20_paths_are_locations(self): assert "local_path" in os_tree assert os_tree["local_path"] == "Server/x86_64/os" + def test_serialize_v20_without_location_raises(self): + """Test v2.0 serialization raises ValueError when no Location is set.""" + ci = _create_composeinfo() + _add_server_variant(ci) # no locations + + data = {} + with pytest.raises(ValueError, match="no Location set"): + ci.serialize(data, force_version=VERSION_2_0) + def test_serialize_v20_with_explicit_location(self): """Test v2.0 serialization with explicitly set Location objects.""" ci = _create_composeinfo() - _add_server_variant(ci) + _add_server_variant(ci, with_locations=True) - # Attach Location objects to the variant paths + # Override one specific Location to verify it takes effect server = ci.variants["Server"] loc = Location( - url="https://cdn.example.com/Server/x86_64/os/", - size=2847, - checksum="sha256:" + "a" * 64, + url="https://custom-cdn.example.com/Server/x86_64/os/", + size=9999, + checksum="sha256:" + "b" * 64, local_path="Server/x86_64/os", ) - server.paths._locations.setdefault("os_tree", {})["x86_64"] = loc + server.paths.set_location("os_tree", "x86_64", loc) data = {} ci.serialize(data, force_version=VERSION_2_0) os_tree = data["payload"]["variants"]["Server"]["paths"]["os_tree"]["x86_64"] - assert os_tree["url"] == "https://cdn.example.com/Server/x86_64/os/" - assert os_tree["size"] == 2847 - assert os_tree["checksum"] == "sha256:" + "a" * 64 + assert os_tree["url"] == "https://custom-cdn.example.com/Server/x86_64/os/" + assert os_tree["size"] == 9999 + assert os_tree["checksum"] == "sha256:" + "b" * 64 assert os_tree["local_path"] == "Server/x86_64/os" def test_deserialize_v12_format(self): @@ -184,7 +204,7 @@ def test_deserialize_v12_format(self): assert server.paths.os_tree == {"x86_64": "Server/x86_64/os"} assert server.paths.packages == {"x86_64": "Server/x86_64/os/Packages"} # v1.x data should not have locations - assert server.paths._locations == {} + assert server.paths.get_location("os_tree", "x86_64") is None def test_deserialize_v20_format(self): """Test deserialization from v2.0 format.""" @@ -234,9 +254,8 @@ def test_deserialize_v20_format(self): assert server.paths.os_tree == {"x86_64": "Server/x86_64/os"} # Location preserved for round-trip - assert "os_tree" in server.paths._locations - assert "x86_64" in server.paths._locations["os_tree"] - loc = server.paths._locations["os_tree"]["x86_64"] + loc = server.paths.get_location("os_tree", "x86_64") + assert loc is not None assert isinstance(loc, Location) assert loc.url == "https://cdn.example.com/Server/x86_64/os/" assert loc.size == 2847 @@ -251,7 +270,7 @@ def test_deserialize_v20_format(self): def test_header_version_matches_output(self, version, header_version): """Test that the serialized header version matches force_version.""" ci = _create_composeinfo() - _add_server_variant(ci) + _add_server_variant(ci, with_locations=(version >= VERSION_2_0)) data = {} ci.serialize(data, force_version=version) @@ -283,17 +302,7 @@ def test_v12_roundtrip(self): def test_v20_roundtrip(self): """Test v2.0 format round-trip preserves data including locations.""" ci = _create_composeinfo() - _add_server_variant(ci) - - # Attach Location to os_tree - server = ci.variants["Server"] - loc = Location( - url="https://cdn.example.com/Server/x86_64/os/", - size=2847, - checksum="sha256:" + "a" * 64, - local_path="Server/x86_64/os", - ) - server.paths._locations.setdefault("os_tree", {})["x86_64"] = loc + _add_server_variant(ci, with_locations=True) # Serialize as v2.0 data = {} @@ -310,24 +319,14 @@ def test_v20_roundtrip(self): assert server2.paths.os_tree["x86_64"] == "Server/x86_64/os" # Verify Location round-trip - loc2 = server2.paths._locations["os_tree"]["x86_64"] - assert loc2.url == "https://cdn.example.com/Server/x86_64/os/" + loc2 = server2.paths.get_location("os_tree", "x86_64") + assert loc2.url == "https://cdn.example.com/Server/x86_64/os" assert loc2.size == 2847 def test_v20_roundtrip_identity(self): """Test v2.0 serialize-deserialize-serialize produces identical output.""" ci = _create_composeinfo() - _add_server_variant(ci) - - server = ci.variants["Server"] - for arch in ["x86_64", "aarch64"]: - loc = Location( - url=f"https://cdn.example.com/Server/{arch}/os/", - size=2847, - checksum="sha256:" + "a" * 64, - local_path=f"Server/{arch}/os", - ) - server.paths._locations.setdefault("os_tree", {})[arch] = loc + _add_server_variant(ci, with_locations=True) # First serialize data1 = {} @@ -413,3 +412,332 @@ def test_no_paths_variant(self): data = {} ci.serialize(data, force_version=version) assert "Server" in data["payload"]["variants"] + + +class TestVariantPathsSetGetLocation: + """Tests for the set_location/get_location public API.""" + + def test_set_location_populates_both_storages(self): + """set_location writes to the path field dict and internal location storage.""" + ci = _create_composeinfo() + _add_server_variant(ci) + server = ci.variants["Server"] + + loc = Location( + url="https://cdn.example.com/Server/x86_64/os/", + size=2847, + checksum="sha256:" + "a" * 64, + local_path="Server/x86_64/os", + ) + server.paths.set_location("os_tree", "x86_64", loc) + + assert server.paths.os_tree["x86_64"] == "Server/x86_64/os" + assert server.paths.get_location("os_tree", "x86_64") is loc + + def test_set_location_overwrites_existing(self): + """set_location replaces a previously set Location.""" + ci = _create_composeinfo() + _add_server_variant(ci) + server = ci.variants["Server"] + + loc1 = Location(url="https://old.example.com/os/", local_path="Server/x86_64/os") + loc2 = Location(url="https://new.example.com/os/", local_path="Server/x86_64/os") + + server.paths.set_location("os_tree", "x86_64", loc1) + server.paths.set_location("os_tree", "x86_64", loc2) + + assert server.paths.get_location("os_tree", "x86_64") is loc2 + + def test_set_location_rejects_invalid_field(self): + """set_location raises ValueError for unknown field names.""" + ci = _create_composeinfo() + _add_server_variant(ci) + server = ci.variants["Server"] + + loc = Location(url="https://example.com/", local_path="x") + with pytest.raises(ValueError, match="Unknown path field"): + server.paths.set_location("nonexistent_field", "x86_64", loc) + + def test_set_location_rejects_non_location(self): + """set_location raises TypeError for non-Location values.""" + ci = _create_composeinfo() + _add_server_variant(ci) + server = ci.variants["Server"] + + with pytest.raises(TypeError, match="Expected Location"): + server.paths.set_location("os_tree", "x86_64", "not a Location") + + def test_get_location_returns_none_for_missing(self): + """get_location returns None when no Location is set.""" + ci = _create_composeinfo() + _add_server_variant(ci) + server = ci.variants["Server"] + + assert server.paths.get_location("os_tree", "x86_64") is None + assert server.paths.get_location("os_tree", "nonexistent_arch") is None + assert server.paths.get_location("isos", "x86_64") is None + + def test_set_location_with_src_arch(self): + """set_location works with the 'src' pseudo-arch.""" + ci = _create_composeinfo() + _add_server_variant(ci) + server = ci.variants["Server"] + + loc = Location( + url="https://cdn.example.com/Server/source/os/", + local_path="Server/source/os", + ) + server.paths.set_location("source_repository", "src", loc) + + assert server.paths.source_repository["src"] == "Server/source/os" + assert server.paths.get_location("source_repository", "src") is loc + + +class TestVariantPathsSrcArch: + """Tests for source_repository with 'src' pseudo-arch (issue #229).""" + + def test_src_arch_v12_roundtrip(self): + """source_repository['src'] survives v1.2 serialize/deserialize.""" + ci = _create_composeinfo() + + variant = Variant(ci) + variant.id = "BaseOS" + variant.uid = "BaseOS" + variant.name = "BaseOS" + variant.type = "variant" + variant.arches = set(["x86_64"]) + variant.paths.repository = {"x86_64": "BaseOS/x86_64/os"} + variant.paths.source_repository = {"src": "BaseOS/source/os"} + ci.variants.add(variant) + + data = {} + ci.serialize(data, force_version=VERSION_1_2) + + paths = data["payload"]["variants"]["BaseOS"]["paths"] + assert "source_repository" in paths + assert paths["source_repository"]["src"] == "BaseOS/source/os" + + ci2 = ComposeInfo() + ci2.deserialize(data) + baseos = ci2.variants["BaseOS"] + assert baseos.paths.source_repository == {"src": "BaseOS/source/os"} + + def test_src_arch_v20_roundtrip(self): + """source_repository['src'] survives v2.0 serialize/deserialize.""" + ci = _create_composeinfo() + + variant = Variant(ci) + variant.id = "BaseOS" + variant.uid = "BaseOS" + variant.name = "BaseOS" + variant.type = "variant" + variant.arches = set(["x86_64"]) + ci.variants.add(variant) + + repo_loc = Location( + url="https://cdn.example.com/BaseOS/x86_64/os/", + local_path="BaseOS/x86_64/os", + ) + variant.paths.set_location("repository", "x86_64", repo_loc) + + src_loc = Location( + url="https://cdn.example.com/BaseOS/source/os/", + local_path="BaseOS/source/os", + ) + variant.paths.set_location("source_repository", "src", src_loc) + + data = {} + ci.serialize(data, force_version=VERSION_2_0) + + paths = data["payload"]["variants"]["BaseOS"]["paths"] + assert "source_repository" in paths + assert paths["source_repository"]["src"]["url"] == "https://cdn.example.com/BaseOS/source/os/" + + ci2 = ComposeInfo() + ci2.deserialize(data) + baseos = ci2.variants["BaseOS"] + assert baseos.paths.source_repository["src"] == "BaseOS/source/os" + loc2 = baseos.paths.get_location("source_repository", "src") + assert loc2.url == "https://cdn.example.com/BaseOS/source/os/" + + def test_src_arch_deserialized_from_v12_json(self): + """source_repository['src'] is read from v1.2 JSON even when 'src' is not in arches.""" + data = { + "header": {"type": "productmd.composeinfo", "version": "1.2"}, + "payload": { + "compose": { + "id": "Test-1.0-20260204.0", + "date": "20260204", + "type": "production", + "respin": 0, + }, + "release": { + "name": "Test", + "short": "Test", + "version": "1.0", + "type": "ga", + }, + "variants": { + "BaseOS": { + "id": "BaseOS", + "uid": "BaseOS", + "name": "BaseOS", + "type": "variant", + "arches": ["x86_64"], + "paths": { + "repository": {"x86_64": "BaseOS/x86_64/os"}, + "source_repository": {"src": "BaseOS/source/os"}, + }, + } + }, + }, + } + + ci = ComposeInfo() + ci.deserialize(data) + baseos = ci.variants["BaseOS"] + assert baseos.paths.source_repository == {"src": "BaseOS/source/os"} + + +class TestVariantPathsStrictV2: + """Tests for strict v2.0 serialization (no synthesis fallback).""" + + def test_deserialize_v20_rejects_plain_string(self): + """v2.0 deserialization raises on plain string path data.""" + data = { + "header": {"type": "productmd.composeinfo", "version": "2.0"}, + "payload": { + "compose": { + "id": "Test-1.0-20260204.0", + "date": "20260204", + "type": "production", + "respin": 0, + }, + "release": { + "name": "Test", + "short": "Test", + "version": "1.0", + "type": "ga", + }, + "variants": { + "Server": { + "id": "Server", + "uid": "Server", + "name": "Server", + "type": "variant", + "arches": ["x86_64"], + "paths": { + "os_tree": { + "x86_64": "Server/x86_64/os", + }, + }, + } + }, + }, + } + + ci = ComposeInfo() + with pytest.raises(TypeError, match="must be a Location dict"): + ci.deserialize(data) + + def test_serialize_v20_rejects_missing_location(self): + """v2.0 serialization raises when path has no Location set.""" + ci = _create_composeinfo() + _add_server_variant(ci) + + with pytest.raises(ValueError, match="no Location set"): + ci.serialize({}, force_version=VERSION_2_0) + + def test_serialize_v20_with_direct_location_in_field(self): + """v2.0 serialization works when Location is assigned directly to field dict.""" + ci = _create_composeinfo() + + variant = Variant(ci) + variant.id = "Server" + variant.uid = "Server" + variant.name = "Fedora Server" + variant.type = "variant" + variant.arches = set(["x86_64"]) + loc = Location( + url="https://cdn.example.com/Server/x86_64/os/", + size=2847, + checksum="sha256:" + "a" * 64, + local_path="Server/x86_64/os", + ) + variant.paths.os_tree["x86_64"] = loc + ci.variants.add(variant) + + data = {} + ci.serialize(data, force_version=VERSION_2_0) + + os_tree = data["payload"]["variants"]["Server"]["paths"]["os_tree"]["x86_64"] + assert os_tree["url"] == "https://cdn.example.com/Server/x86_64/os/" + assert os_tree["size"] == 2847 + + def test_serialize_v12_extracts_local_path_from_location(self): + """v1.2 serialization extracts local_path from Location objects in field dicts.""" + ci = _create_composeinfo() + + variant = Variant(ci) + variant.id = "Server" + variant.uid = "Server" + variant.name = "Fedora Server" + variant.type = "variant" + variant.arches = set(["x86_64"]) + loc = Location( + url="https://cdn.example.com/Server/x86_64/os/", + local_path="Server/x86_64/os", + ) + variant.paths.os_tree["x86_64"] = loc + ci.variants.add(variant) + + data = {} + ci.serialize(data, force_version=VERSION_1_2) + + os_tree = data["payload"]["variants"]["Server"]["paths"]["os_tree"]["x86_64"] + assert isinstance(os_tree, str) + assert os_tree == "Server/x86_64/os" + + +class TestVariantPathsArchWarning: + """Tests for warning when path arch keys don't match variant.arches.""" + + def test_warns_on_unknown_arch(self): + """Serialize emits a warning when a path field has an arch not in variant.arches.""" + ci = _create_composeinfo() + + variant = Variant(ci) + variant.id = "Server" + variant.uid = "Server" + variant.name = "Server" + variant.type = "variant" + variant.arches = set(["x86_64"]) + variant.paths.source_repository = {"src": "Server/source/os"} + ci.variants.add(variant) + + import warnings + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + data = {} + ci.serialize(data, force_version=VERSION_1_2) + + arch_warnings = [x for x in w if "not in variant.arches" in str(x.message)] + assert len(arch_warnings) == 1 + assert "source_repository" in str(arch_warnings[0].message) + assert "'src'" in str(arch_warnings[0].message) + + def test_no_warning_for_matching_arches(self): + """Serialize does not warn when all arch keys match variant.arches.""" + ci = _create_composeinfo() + _add_server_variant(ci) + + import warnings + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + data = {} + ci.serialize(data, force_version=VERSION_1_2) + + arch_warnings = [x for x in w if "not in variant.arches" in str(x.message)] + assert len(arch_warnings) == 0 diff --git a/tests/test_convert.py b/tests/test_convert.py index 82beb7a..6a88a12 100644 --- a/tests/test_convert.py +++ b/tests/test_convert.py @@ -321,7 +321,7 @@ def test_set_location_callback_composeinfo(self): break server = ci.variants["Server"] - assert server.paths._locations["os_tree"]["x86_64"] is loc + assert server.paths.get_location("os_tree", "x86_64") is loc # ---------------------------------------------------------------------------