The drop-sync refactor is now contractually enforced:
* create_storage({"mode": "sync"|"async"|<any>}) raises StorageConfigError
-- the regression we most want to catch is someone "restoring" the
sync shape by re-introducing mode= handling.
* create_storage() preserves caller's dict, returns an AsyncStorageBackend
subclass, and wraps constructor TypeError into a useful StorageConfigError.
* build_storage_config emits no mode key on either local or s3 branch.
* register_backend() refuses to silently overwrite a different class on
the same name (collision guard), and accepts same-class re-registration.
* registry surfaces the built-in local + s3 classes after import; the
conflict-test cleanup pattern avoids leaking global state across tests.
Adds [tool.pytest.ini_options] (asyncio_mode=auto, testpaths=tests) so
uv run --package common pytest common/tests works from the workspace root.
17 new tests, all green. Backend suite still 136 passed.
134 lines
5.0 KiB
Python
134 lines
5.0 KiB
Python
"""create_storage 工厂测试:覆盖 mode 字段严格拒绝 + 正常路径。"""
|
|
|
|
from common.storage.base import AsyncStorageBackend
|
|
from common.storage.exceptions import StorageConfigError
|
|
from common.storage.factory import (
|
|
PURPOSE_BUCKETS,
|
|
USAGE_TYPE_TO_PURPOSE,
|
|
build_storage_config,
|
|
create_storage,
|
|
)
|
|
|
|
|
|
# ── mode 字段严格拒绝 ──────────────────────────────────────────────
|
|
|
|
|
|
def test_create_storage_rejects_mode_sync():
|
|
"""显式 mode='sync' 现在必须拒绝 —— 同步抽象已砍掉。"""
|
|
import pytest
|
|
|
|
with pytest.raises(StorageConfigError) as exc_info:
|
|
create_storage({"type": "local", "base_dir": "/tmp", "mode": "sync"})
|
|
msg = str(exc_info.value)
|
|
assert "不再接受 'mode' 字段" in msg
|
|
|
|
|
|
def test_create_storage_rejects_mode_async():
|
|
"""显式 mode='async' 也必须拒绝 —— 只有 async 一条路,不需要再声明。"""
|
|
import pytest
|
|
|
|
with pytest.raises(StorageConfigError) as exc_info:
|
|
create_storage({"type": "s3", "bucket": "x", "mode": "async"})
|
|
msg = str(exc_info.value)
|
|
assert "不再接受 'mode' 字段" in msg
|
|
|
|
|
|
def test_create_storage_rejects_any_mode_value():
|
|
"""任何 mode 字段(包含未来可能新增的合法值)都拒绝 —— 简化语义。"""
|
|
import pytest
|
|
|
|
for value in ("async", "sync", "dual", ""):
|
|
with pytest.raises(StorageConfigError):
|
|
create_storage({"type": "local", "base_dir": "/tmp", "mode": value})
|
|
|
|
|
|
# ── 正常路径 ────────────────────────────────────────────────────────
|
|
|
|
|
|
def test_create_storage_returns_async_subclass():
|
|
s = create_storage({"type": "local", "base_dir": "/tmp/cc-factory-test"})
|
|
assert isinstance(s, AsyncStorageBackend)
|
|
# 确认是真 AsyncStorageBackend,不是同步兼容形态
|
|
assert type(s).__name__ == "LocalStorageBackend"
|
|
|
|
|
|
def test_create_storage_missing_type_raises():
|
|
import pytest
|
|
|
|
with pytest.raises(StorageConfigError) as exc_info:
|
|
create_storage({"base_dir": "/tmp"})
|
|
assert "缺少 'type' 字段" in str(exc_info.value)
|
|
|
|
|
|
def test_create_storage_unknown_type_raises():
|
|
import pytest
|
|
|
|
with pytest.raises(StorageConfigError) as exc_info:
|
|
create_storage({"type": "nonexistent"})
|
|
assert "未知的存储后端" in str(exc_info.value)
|
|
|
|
|
|
def test_create_storage_does_not_mutate_input_dict():
|
|
"""调用方传入的字典不能被改写。"""
|
|
cfg = {"type": "local", "base_dir": "/tmp/cc-no-mutate"}
|
|
cfg_id = id(cfg)
|
|
snapshot = dict(cfg)
|
|
create_storage(cfg)
|
|
assert dict(cfg) == snapshot, "factory should not mutate caller's dict"
|
|
assert id(cfg) == cfg_id
|
|
|
|
|
|
def test_create_storage_kwargs_mismatch_raises_wrapped_error():
|
|
import pytest
|
|
|
|
with pytest.raises(StorageConfigError) as exc_info:
|
|
create_storage({"type": "local", "base_dir": 12345}) # base_dir 必须是 str
|
|
msg = str(exc_info.value)
|
|
assert "参数不匹配" in msg
|
|
|
|
|
|
# ── build_storage_config 不再产生 mode 字段 ─────────────────────────
|
|
|
|
|
|
def test_build_storage_config_local_has_no_mode(monkeypatch):
|
|
"""local 分支的输出 dict 不能含 'mode'。"""
|
|
from common import config as common_config
|
|
|
|
monkeypatch.setattr(common_config.settings, "storage_backend", "local", raising=False)
|
|
monkeypatch.setattr(common_config.settings, "local_storage_base_dir", "/tmp", raising=False)
|
|
|
|
cfg = build_storage_config("workspace")
|
|
assert "mode" not in cfg, f"local cfg must not have 'mode', got: {cfg}"
|
|
assert cfg["type"] == "local"
|
|
assert "base_dir" in cfg
|
|
|
|
|
|
def test_build_storage_config_s3_has_no_mode(monkeypatch):
|
|
"""s3 分支的输出 dict 也不能含 'mode'。"""
|
|
from common import config as common_config
|
|
|
|
monkeypatch.setattr(common_config.settings, "storage_backend", "s3", raising=False)
|
|
monkeypatch.setattr(common_config.settings, "s3_workspace_bucket", "wb", raising=False)
|
|
monkeypatch.setattr(common_config.settings, "s3_endpoint", "http://s3", raising=False)
|
|
monkeypatch.setattr(common_config.settings, "s3_access_key", "ak", raising=False)
|
|
monkeypatch.setattr(common_config.settings, "s3_secret_key", "sk", raising=False)
|
|
|
|
cfg = build_storage_config("workspace")
|
|
assert "mode" not in cfg, f"s3 cfg must not have 'mode', got: {cfg}"
|
|
assert cfg["type"] == "s3"
|
|
|
|
|
|
def test_build_storage_config_unknown_bucket_raises():
|
|
import pytest
|
|
|
|
from common.storage.exceptions import StorageConfigError
|
|
with pytest.raises(StorageConfigError):
|
|
build_storage_config("not-a-real-bucket")
|
|
|
|
|
|
def test_purpose_buckets_constant_complete():
|
|
"""PURPOSE_BUCKETS 必须覆盖 USAGE_TYPE_TO_PURPOSE 中所有 purpose。"""
|
|
purposes = set(USAGE_TYPE_TO_PURPOSE.values())
|
|
assert purposes.issubset(set(PURPOSE_BUCKETS)), (
|
|
f"missing buckets for purposes: {purposes - set(PURPOSE_BUCKETS)}"
|
|
) |