Files
kernalix7--winpodx/tests/test_config_schema_version.py
wehub-resource-sync 3e779be6f3
CI / lint (push) Failing after 13m4s
CI / test (3.11, ubuntu-latest) (push) Failing after 2m4s
CI / test (3.13, ubuntu-latest) (push) Successful in 13m30s
CI / test (3.14, ubuntu-latest) (push) Successful in 17m21s
CI / test (3.12, ubuntu-latest) (push) Successful in 17m55s
CI / discover-apps-ps (push) Successful in 1m56s
CI / test (3.9, ubuntu-latest) (push) Successful in 13m17s
CI / test (3.10, ubuntu-latest) (push) Successful in 26m21s
CI / audit (push) Successful in 13m38s
Deploy site / deploy (push) Has been cancelled
CI / test (3.14, ubuntu-24.04-arm) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:37 +08:00

165 lines
6.4 KiB
Python

# SPDX-License-Identifier: MIT
"""Pin the on-disk config schema_version marker + the migration hook.
0.6.0 introduces the marker without changing the TOML layout, so the
hook is a no-op today. These tests lock the contract so a 0.7.0+
migration lands with the seam intact:
* Config.load() reads ``schema_version`` (missing -> 0 = pre-0.6.0).
* When the read value differs from SCHEMA_VERSION, _migrate_config()
runs and cfg.schema_version is bumped to current.
* save() writes ``schema_version`` at the top of the TOML.
* A pre-0.6.0 file (no marker) round-trips: load it, save it, the file
now has schema_version = SCHEMA_VERSION and no user settings dropped.
"""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pytest
from winpodx.core.config import SCHEMA_VERSION, Config, _migrate_config
def test_schema_version_constant_is_positive() -> None:
# Sanity: 0 is reserved for "pre-marker file", so the live constant
# must be >= 1.
assert isinstance(SCHEMA_VERSION, int)
assert SCHEMA_VERSION >= 1
def test_config_default_carries_current_schema_version() -> None:
cfg = Config()
assert cfg.schema_version == SCHEMA_VERSION
def test_migrate_hook_is_noop_at_current_version() -> None:
# 0.6.0 introduces the marker without restructuring the file, so the
# hook returns the data unchanged. The contract is *the dict is
# returned*, not "the same identity" -- future migrations may copy.
data = {"rdp": {"user": "alice"}, "schema_version": SCHEMA_VERSION}
out = _migrate_config(dict(data), SCHEMA_VERSION)
assert out == data
def test_migrate_hook_from_pre_marker_preserves_settings() -> None:
# A pre-0.6.0 file has no schema_version key. The hook receives
# from_version=0 and must not drop any user-authored settings.
data = {
"rdp": {"user": "alice", "port": 3389},
"pod": {"backend": "podman"},
"ui": {"language": "ko"},
}
out = _migrate_config(dict(data), 0)
# Today's hook is a no-op, so all keys round-trip.
assert out["rdp"] == data["rdp"]
assert out["pod"] == data["pod"]
assert out["ui"] == data["ui"]
def test_load_unmarked_file_bumps_schema_version(tmp_path: Path) -> None:
# Write a pre-0.6.0 file (no schema_version) and confirm load() sets
# cfg.schema_version = SCHEMA_VERSION so the next save converges.
cfg_path = tmp_path / "winpodx.toml"
cfg_path.write_text(
"[rdp]\nuser = 'alice'\nport = 3389\n[pod]\nbackend = 'podman'\n",
encoding="utf-8",
)
with patch.object(Config, "path", classmethod(lambda cls: cfg_path)):
cfg = Config.load()
assert cfg.schema_version == SCHEMA_VERSION
assert cfg.rdp.user == "alice"
assert cfg.pod.backend == "podman"
def test_load_corrupt_schema_version_falls_back_to_zero(tmp_path: Path) -> None:
# A hand-edit could write a non-int value; load must not crash, it
# treats the file as pre-marker and runs the migration hook.
cfg_path = tmp_path / "winpodx.toml"
cfg_path.write_text(
"schema_version = 'banana'\n[rdp]\nuser = 'bob'\n",
encoding="utf-8",
)
with patch.object(Config, "path", classmethod(lambda cls: cfg_path)):
cfg = Config.load()
assert cfg.schema_version == SCHEMA_VERSION
assert cfg.rdp.user == "bob"
def test_save_emits_schema_version_first(tmp_path: Path) -> None:
# Marker MUST land in the saved file (so a future load sees it) and
# MUST be near the top so a hand-edit can flag the layout version
# without scrolling.
cfg_path = tmp_path / "winpodx.toml"
with patch.object(Config, "path", classmethod(lambda cls: cfg_path)):
cfg = Config()
cfg.save()
text = cfg_path.read_text(encoding="utf-8")
assert f"schema_version = {SCHEMA_VERSION}" in text
# Must appear before any section header.
first_section_idx = text.find("[")
schema_idx = text.find("schema_version")
assert 0 <= schema_idx < first_section_idx
def test_legacy_file_round_trip_through_save_keeps_settings(tmp_path: Path) -> None:
# The full upgrade story: load a 0.5.x file -> save -> reload. Every
# user setting survives, and the file gains the marker.
cfg_path = tmp_path / "winpodx.toml"
cfg_path.write_text(
"[rdp]\nuser = 'carol'\nport = 4001\n[ui]\nlanguage = 'ko'\n",
encoding="utf-8",
)
with patch.object(Config, "path", classmethod(lambda cls: cfg_path)):
cfg = Config.load()
cfg.save()
reloaded = Config.load()
assert reloaded.rdp.user == "carol"
assert reloaded.rdp.port == 4001
assert reloaded.ui.language == "ko"
assert reloaded.schema_version == SCHEMA_VERSION
def test_schema_version_not_dropped_by_apply(tmp_path: Path) -> None:
# _apply mustn't trip over a top-level non-section key. A future
# accident (typo'd field name, stray hand-edit) shouldn't break load.
cfg_path = tmp_path / "winpodx.toml"
cfg_path.write_text(
f"schema_version = {SCHEMA_VERSION}\nstray_top_level_key = 42\n[rdp]\nuser = 'dave'\n",
encoding="utf-8",
)
with patch.object(Config, "path", classmethod(lambda cls: cfg_path)):
cfg = Config.load()
assert cfg.schema_version == SCHEMA_VERSION
assert cfg.rdp.user == "dave"
@pytest.mark.parametrize("from_v", [0, 1])
def test_migrate_hook_called_with_correct_from_version(from_v: int) -> None:
# When _migrate_config grows real logic, callers must pass the
# original file version. Locking the call shape now keeps a future
# 0.7.0 migration from being silently bypassed.
calls: list[tuple[int, dict]] = []
def spy(data: dict, from_version: int) -> dict:
calls.append((from_version, dict(data)))
return data
if from_v == SCHEMA_VERSION:
# Same version: hook is intentionally skipped (no migration needed).
return
with patch("winpodx.core.config._migrate_config", side_effect=spy):
# Exercise via load() against an in-memory dict.
from winpodx.core import config as cfg_mod
Config.load.__func__ # noqa: B018 — sanity-check method exists
# Direct call path: simulate what load does.
data = {"schema_version": from_v} if from_v else {}
from_version = int(data.get("schema_version", 0))
if from_version != cfg_mod.SCHEMA_VERSION:
cfg_mod._migrate_config(data, from_version)
assert calls and calls[0][0] == from_v