初始化奇妙小屏幕控制器项目
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.migrate_state_root import (
|
||||
STAGING_NAME,
|
||||
StateRootMigrationError,
|
||||
build_manifest,
|
||||
migrate_state_root,
|
||||
)
|
||||
|
||||
|
||||
def seed_legacy_state(root):
|
||||
(root / "templates" / "records").mkdir(parents=True)
|
||||
(root / "templates" / "thumbnails").mkdir(parents=True)
|
||||
(root / "future-user-data").mkdir()
|
||||
(root / "runtime").mkdir()
|
||||
(root / ".portrait.tmp").mkdir()
|
||||
(root / "config.json").write_text(
|
||||
json.dumps({"brightness": 37, "voltage_calibration_factor": 1.01}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(root / "templates" / "records" / "template.json").write_text("{}", encoding="utf-8")
|
||||
(root / "templates" / "thumbnails" / "template.png").write_bytes(b"png")
|
||||
(root / "future-user-data" / "keep.bin").write_bytes(b"keep")
|
||||
(root / "last_frame.png").write_bytes(b"runtime")
|
||||
(root / "startup_indicator.json").write_text("{}", encoding="utf-8")
|
||||
(root / "runtime" / "last_frame.png").write_bytes(b"new-runtime")
|
||||
(root / ".portrait.tmp" / "keep.bin").write_bytes(b"keep")
|
||||
(root / "config.json.tmp").write_text("partial", encoding="utf-8")
|
||||
(root / "templates" / "records" / ".record.json.0123456789abcdef0123456789abcdef.tmp").write_text(
|
||||
"partial",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def test_migration_preserves_persistent_unknown_files_and_keeps_source(tmp_path):
|
||||
source = tmp_path / "opt" / "data"
|
||||
target = tmp_path / "var" / "lib" / "matrix-screen-controller"
|
||||
seed_legacy_state(source)
|
||||
|
||||
result = migrate_state_root(source, target)
|
||||
|
||||
assert result.status == "migrated"
|
||||
assert source.is_dir()
|
||||
assert (source / "last_frame.png").exists()
|
||||
assert (target / "config.json").exists()
|
||||
assert (target / "templates" / "records" / "template.json").exists()
|
||||
assert (target / "future-user-data" / "keep.bin").read_bytes() == b"keep"
|
||||
assert not (target / "last_frame.png").exists()
|
||||
assert not (target / "startup_indicator.json").exists()
|
||||
assert not (target / "runtime").exists()
|
||||
assert (target / ".portrait.tmp" / "keep.bin").read_bytes() == b"keep"
|
||||
assert not (target / "config.json.tmp").exists()
|
||||
assert not (
|
||||
target
|
||||
/ "templates"
|
||||
/ "records"
|
||||
/ ".record.json.0123456789abcdef0123456789abcdef.tmp"
|
||||
).exists()
|
||||
assert build_manifest(source) == build_manifest(target)
|
||||
|
||||
|
||||
def test_migration_is_idempotent_when_target_matches(tmp_path):
|
||||
source = tmp_path / "legacy"
|
||||
target = tmp_path / "state"
|
||||
seed_legacy_state(source)
|
||||
migrate_state_root(source, target)
|
||||
|
||||
second = migrate_state_root(source, target)
|
||||
|
||||
assert second.status == "already-prepared"
|
||||
assert not (target.parent / STAGING_NAME).exists()
|
||||
|
||||
|
||||
def test_migration_refuses_different_nonempty_target_without_modifying_it(tmp_path):
|
||||
source = tmp_path / "legacy"
|
||||
target = tmp_path / "state"
|
||||
seed_legacy_state(source)
|
||||
target.mkdir()
|
||||
existing = target / "config.json"
|
||||
existing.write_text('{"brightness": 99}', encoding="utf-8")
|
||||
before = existing.read_bytes()
|
||||
|
||||
with pytest.raises(StateRootMigrationError, match="refusing to merge"):
|
||||
migrate_state_root(source, target)
|
||||
|
||||
assert existing.read_bytes() == before
|
||||
assert not (target.parent / STAGING_NAME).exists()
|
||||
|
||||
|
||||
def test_missing_source_keeps_current_target_untouched(tmp_path):
|
||||
source = tmp_path / "missing"
|
||||
target = tmp_path / "state"
|
||||
target.mkdir()
|
||||
config = target / "config.json"
|
||||
config.write_text('{"schema_version": 1}', encoding="utf-8")
|
||||
|
||||
result = migrate_state_root(source, target)
|
||||
|
||||
assert result.status == "target-only"
|
||||
assert config.read_text(encoding="utf-8") == '{"schema_version": 1}'
|
||||
|
||||
|
||||
def test_migration_refuses_overlapping_state_roots(tmp_path):
|
||||
source = tmp_path / "legacy"
|
||||
seed_legacy_state(source)
|
||||
|
||||
with pytest.raises(StateRootMigrationError, match="must not contain"):
|
||||
migrate_state_root(source, source / "new-state")
|
||||
|
||||
|
||||
def test_missing_source_and_empty_target_fail_closed(tmp_path):
|
||||
source = tmp_path / "missing"
|
||||
target = tmp_path / "empty"
|
||||
target.mkdir()
|
||||
|
||||
with pytest.raises(StateRootMigrationError, match="no persistent files"):
|
||||
migrate_state_root(source, target)
|
||||
|
||||
|
||||
def test_empty_legacy_source_fails_closed(tmp_path):
|
||||
source = tmp_path / "empty-source"
|
||||
source.mkdir()
|
||||
|
||||
with pytest.raises(StateRootMigrationError, match="source contains no persistent files"):
|
||||
migrate_state_root(source, tmp_path / "target")
|
||||
|
||||
|
||||
def test_state_root_symlink_is_rejected(tmp_path):
|
||||
real_source = tmp_path / "real-source"
|
||||
seed_legacy_state(real_source)
|
||||
source_link = tmp_path / "source-link"
|
||||
try:
|
||||
source_link.symlink_to(real_source, target_is_directory=True)
|
||||
except OSError:
|
||||
pytest.skip("directory symlinks are unavailable on this host")
|
||||
|
||||
with pytest.raises(StateRootMigrationError, match="must not be symlinks"):
|
||||
migrate_state_root(source_link, tmp_path / "target")
|
||||
Reference in New Issue
Block a user