更新了部分测试
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
## 设计原则
|
||||
- 不绕过 schema/状态机校验;配置错误必须显式失败。
|
||||
- 单元测试聚焦纯逻辑,集成测试覆盖流水线与边界行为。
|
||||
- 优先保留行为测试,避免只验证转发器、setter 或仅供测试存在的空壳包装层。
|
||||
- 所有测试均从仓库根路径解析配置,避免目录迁移导致路径脆弱。
|
||||
- 保留高价值回归测试,移除仅重复路径或无断言价值的历史脚本型测试(脚本保留在 `scripts/`,不纳入 pytest)。
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ async def pipeline_bundle():
|
||||
local_ds = InMemoryDataSource(poll_max_retries=2, poll_interval=0)
|
||||
remote_ds = InMemoryDataSource(poll_max_retries=2, poll_interval=0)
|
||||
|
||||
local_ds.register_handler(
|
||||
local_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_project",
|
||||
node_class=TestProjectNode,
|
||||
@@ -120,7 +120,7 @@ async def pipeline_bundle():
|
||||
load_nodes=local_project_nodes,
|
||||
)
|
||||
)
|
||||
local_ds.register_handler(
|
||||
local_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_contract",
|
||||
node_class=TestContractNode,
|
||||
@@ -129,7 +129,7 @@ async def pipeline_bundle():
|
||||
)
|
||||
)
|
||||
|
||||
remote_ds.register_handler(
|
||||
remote_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_project",
|
||||
node_class=TestProjectNode,
|
||||
@@ -143,7 +143,7 @@ async def pipeline_bundle():
|
||||
update_mode_by_code={"UPD-1": "success"},
|
||||
)
|
||||
)
|
||||
remote_ds.register_handler(
|
||||
remote_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_contract",
|
||||
node_class=TestContractNode,
|
||||
@@ -453,7 +453,7 @@ async def test_pipeline_create_then_update_completes_in_single_run_with_reload()
|
||||
local_ds = InMemoryDataSource(poll_max_retries=2, poll_interval=0)
|
||||
remote_ds = InMemoryDataSource(poll_max_retries=2, poll_interval=0)
|
||||
|
||||
local_ds.register_handler(
|
||||
local_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_project",
|
||||
node_class=TestProjectNode,
|
||||
@@ -464,7 +464,7 @@ async def test_pipeline_create_then_update_completes_in_single_run_with_reload()
|
||||
)
|
||||
)
|
||||
remote_handler = _MutableProjectRemoteHandler()
|
||||
remote_ds.register_handler(remote_handler)
|
||||
remote_ds.add_handler(remote_handler)
|
||||
|
||||
project_strategy = TestProjectStrategy("test_project", local_collection, remote_collection, binding_manager)
|
||||
project_strategy.update_config(
|
||||
|
||||
@@ -73,7 +73,7 @@ async def test_full_pipeline_multinode_mock_persisted_db_snapshot() -> None:
|
||||
local_ds = InMemoryDataSource(poll_max_retries=2, poll_interval=0)
|
||||
remote_ds = InMemoryDataSource(poll_max_retries=2, poll_interval=0)
|
||||
|
||||
local_ds.register_handler(
|
||||
local_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_project",
|
||||
node_class=TestProjectNode,
|
||||
@@ -81,7 +81,7 @@ async def test_full_pipeline_multinode_mock_persisted_db_snapshot() -> None:
|
||||
load_nodes=local_project_nodes,
|
||||
)
|
||||
)
|
||||
local_ds.register_handler(
|
||||
local_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_contract",
|
||||
node_class=TestContractNode,
|
||||
@@ -90,7 +90,7 @@ async def test_full_pipeline_multinode_mock_persisted_db_snapshot() -> None:
|
||||
)
|
||||
)
|
||||
|
||||
remote_ds.register_handler(
|
||||
remote_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_project",
|
||||
node_class=TestProjectNode,
|
||||
@@ -104,7 +104,7 @@ async def test_full_pipeline_multinode_mock_persisted_db_snapshot() -> None:
|
||||
update_mode_by_code={"UPD-1": "success"},
|
||||
)
|
||||
)
|
||||
remote_ds.register_handler(
|
||||
remote_ds.add_handler(
|
||||
InMemoryNodeHandler(
|
||||
node_type="test_contract",
|
||||
node_class=TestContractNode,
|
||||
|
||||
@@ -97,8 +97,8 @@ class _DS(BaseDataSource):
|
||||
@pytest.mark.asyncio
|
||||
async def test_load_all_continue_when_one_handler_fails(capsys) -> None:
|
||||
ds = _DS()
|
||||
ds.register_handler(_FailHandler())
|
||||
ds.register_handler(_OkHandler())
|
||||
ds.add_handler(_FailHandler())
|
||||
ds.add_handler(_OkHandler())
|
||||
ds.set_collection(DataCollection("local"))
|
||||
|
||||
await ds.load_all(order=["fail", "ok"])
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pydantic import BaseModel
|
||||
|
||||
from sync_state_machine.common.registry import DomainRegistry
|
||||
@@ -17,7 +18,7 @@ from sync_state_machine.config import (
|
||||
from sync_state_machine.datasource import BaseDataSource
|
||||
from sync_state_machine.datasource.handler import BaseNodeHandler
|
||||
from sync_state_machine.datasource.jsonl import BaseJsonlHandler, JsonlDataSource
|
||||
from sync_state_machine.pipeline.factory import _register_handlers
|
||||
from sync_state_machine.pipeline.factory import create_pipeline_from_config
|
||||
from sync_state_machine.sync_system.strategy import DefaultSyncStrategy
|
||||
|
||||
|
||||
@@ -79,7 +80,8 @@ def _register_test_db_datasource_type() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_register_handlers_uses_db_handler_for_db_datasource(tmp_path: Path) -> None:
|
||||
@pytest.mark.asyncio
|
||||
async def test_register_handlers_uses_db_handler_for_db_datasource(tmp_path: Path) -> None:
|
||||
_register_test_db_datasource_type()
|
||||
|
||||
if not DomainRegistry.is_registered("test_db_handler"):
|
||||
@@ -104,10 +106,9 @@ def test_register_handlers_uses_db_handler_for_db_datasource(tmp_path: Path) ->
|
||||
logging=LoggingConfig(initialize=False),
|
||||
)
|
||||
|
||||
local_ds = BaseDataSource()
|
||||
remote_ds = JsonlDataSource(tmp_path)
|
||||
|
||||
_register_handlers(config, local_ds, remote_ds, ["test_db_handler"])
|
||||
|
||||
assert isinstance(local_ds.get_handler("test_db_handler"), TestDbHandler)
|
||||
assert isinstance(remote_ds.get_handler("test_db_handler"), TestJsonlHandler)
|
||||
pipeline = await create_pipeline_from_config(config)
|
||||
try:
|
||||
assert isinstance(pipeline.local_datasource.get_handler("test_db_handler"), TestDbHandler)
|
||||
assert isinstance(pipeline.remote_datasource.get_handler("test_db_handler"), TestJsonlHandler)
|
||||
finally:
|
||||
await pipeline.close()
|
||||
@@ -1,86 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from sync_state_machine.common.sync_node import SyncNode
|
||||
from sync_state_machine.datasource.api.handler import BaseApiHandler
|
||||
from sync_state_machine.datasource.handler import BaseNodeHandler, CompatibleNodeHandler
|
||||
from sync_state_machine.datasource.task_result import TaskResult
|
||||
|
||||
|
||||
class SharedConfigSchema(BaseModel):
|
||||
__test__ = False
|
||||
id: str
|
||||
|
||||
|
||||
class SharedConfigNode(SyncNode[SharedConfigSchema]):
|
||||
__test__ = False
|
||||
node_type = "shared_config_demo"
|
||||
schema = SharedConfigSchema
|
||||
|
||||
|
||||
class SharedConfigBaseHandler(BaseNodeHandler[SharedConfigSchema]):
|
||||
__test__ = False
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__("shared_config_demo", SharedConfigNode, SharedConfigSchema)
|
||||
|
||||
async def load(self):
|
||||
return []
|
||||
|
||||
async def create_all(self, nodes):
|
||||
return []
|
||||
|
||||
async def update_all(self, nodes):
|
||||
return []
|
||||
|
||||
def extract_id(self, data: dict[str, Any]) -> str:
|
||||
return str(data.get("id", ""))
|
||||
|
||||
|
||||
class SharedConfigApiHandler(BaseApiHandler[SharedConfigSchema]):
|
||||
__test__ = False
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._node_type = "shared_api_demo"
|
||||
self._node_class = SharedConfigNode
|
||||
self._schema = SharedConfigSchema
|
||||
|
||||
async def load(self):
|
||||
return []
|
||||
|
||||
async def create_all(self, nodes):
|
||||
return [TaskResult.success(node_id=node.node_id) for node in nodes]
|
||||
|
||||
async def update_all(self, nodes):
|
||||
return [TaskResult.success(node_id=node.node_id) for node in nodes]
|
||||
|
||||
async def delete_all(self, nodes):
|
||||
return [TaskResult.success(node_id=node.node_id) for node in nodes]
|
||||
|
||||
|
||||
def test_base_handler_supports_shared_handler_config() -> None:
|
||||
handler = SharedConfigBaseHandler()
|
||||
|
||||
handler.set_handler_config({"mode": "local", "batch_size": 10})
|
||||
|
||||
assert handler.handler_config == {"mode": "local", "batch_size": 10}
|
||||
|
||||
|
||||
def test_compatible_handler_forwards_shared_handler_config() -> None:
|
||||
handler = CompatibleNodeHandler(SharedConfigBaseHandler())
|
||||
|
||||
handler.set_handler_config({"mode": "remote"})
|
||||
|
||||
assert handler.handler_config == {"mode": "remote"}
|
||||
|
||||
|
||||
def test_api_handler_supports_shared_handler_config() -> None:
|
||||
handler = SharedConfigApiHandler()
|
||||
|
||||
handler.set_handler_config({"filter_project_id": ["p1", "p2"]})
|
||||
|
||||
assert handler.handler_config == {"filter_project_id": ["p1", "p2"]}
|
||||
@@ -35,14 +35,18 @@ class _OkStrategy(BaseSyncStrategy):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.bound = False
|
||||
self.create_calls = 0
|
||||
self.update_calls = 0
|
||||
|
||||
async def bind(self, **kwargs):
|
||||
self.bound = True
|
||||
|
||||
async def create(self):
|
||||
self.create_calls += 1
|
||||
return []
|
||||
|
||||
async def update(self):
|
||||
self.update_calls += 1
|
||||
return []
|
||||
|
||||
|
||||
@@ -87,6 +91,52 @@ async def test_phase_sync_by_node_type_continues_after_strategy_error(tmp_path:
|
||||
await pipeline.phase_sync_by_node_type()
|
||||
|
||||
assert ok_strategy.bound is True
|
||||
assert ok_strategy.create_calls == 0
|
||||
assert ok_strategy.update_calls == 0
|
||||
assert pipeline.stats["failed"] >= 1
|
||||
|
||||
await pipeline.close()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_phase_sync_by_node_type_skip_sync_keeps_bind_but_skips_writes(tmp_path: Path) -> None:
|
||||
db_path = tmp_path / "state.db"
|
||||
persistence = PersistenceBackend(str(db_path))
|
||||
await persistence.initialize()
|
||||
|
||||
local_collection = DataCollection("local", persistence=persistence, auto_persist=False)
|
||||
remote_collection = DataCollection("remote", persistence=persistence, auto_persist=False)
|
||||
binding_manager = BindingManager(persistence, auto_persist=False)
|
||||
|
||||
strategy = _OkStrategy("ok", local_collection, remote_collection, binding_manager)
|
||||
strategy.skip_sync = True
|
||||
|
||||
pipeline = FullSyncPipeline(
|
||||
local_datasource=_DS(),
|
||||
remote_datasource=_DS(),
|
||||
strategies=[strategy],
|
||||
persistence=persistence,
|
||||
local_collection=local_collection,
|
||||
remote_collection=remote_collection,
|
||||
binding_manager=binding_manager,
|
||||
node_types=["ok"],
|
||||
load_order=["ok"],
|
||||
sync_order=["ok"],
|
||||
enable_persistence=False,
|
||||
)
|
||||
|
||||
load_calls: list[tuple[str, str]] = []
|
||||
|
||||
async def _record_load(node_type: str, reason: str = "") -> None:
|
||||
load_calls.append((node_type, reason))
|
||||
|
||||
pipeline._load_node_type = _record_load # type: ignore[method-assign]
|
||||
|
||||
await pipeline.phase_sync_by_node_type()
|
||||
|
||||
assert load_calls == [("ok", "pre-strategy refresh")]
|
||||
assert strategy.bound is True
|
||||
assert strategy.create_calls == 0
|
||||
assert strategy.update_calls == 0
|
||||
|
||||
await pipeline.close()
|
||||
|
||||
@@ -45,7 +45,7 @@ async def test_jsonl_project_load_applies_target_project_filter(tmp_path):
|
||||
datasource = JsonlDataSource(tmp_path)
|
||||
handler = BaseJsonlHandler(datasource, "project", _ProjectNode, _ProjectSchema)
|
||||
handler.set_handler_config({"data_id_filter": ["p1"]})
|
||||
datasource.register_handler(handler)
|
||||
datasource.add_handler(handler)
|
||||
datasource.set_collection(DataCollection("local"))
|
||||
|
||||
await datasource.load_all(order=["project"])
|
||||
@@ -70,7 +70,7 @@ async def test_jsonl_non_project_load_filters_rows_by_project_id(tmp_path):
|
||||
datasource = JsonlDataSource(tmp_path)
|
||||
handler = BaseJsonlHandler(datasource, "contract", _BizNode, _BizSchema)
|
||||
handler.set_handler_config({"data_id_filter": ["p1"]})
|
||||
datasource.register_handler(handler)
|
||||
datasource.add_handler(handler)
|
||||
datasource.set_collection(DataCollection("local"))
|
||||
|
||||
await datasource.load_all(order=["contract"])
|
||||
@@ -104,8 +104,8 @@ async def test_jsonl_non_project_load_inherits_project_filter_from_loaded_projec
|
||||
project_handler = BaseJsonlHandler(datasource, "project", _ProjectNode, _ProjectSchema)
|
||||
project_handler.set_handler_config({"data_id_filter": ["p1"]})
|
||||
contract_handler = BaseJsonlHandler(datasource, "contract", _BizNode, _BizSchema)
|
||||
datasource.register_handler(project_handler)
|
||||
datasource.register_handler(contract_handler)
|
||||
datasource.add_handler(project_handler)
|
||||
datasource.add_handler(contract_handler)
|
||||
datasource.set_collection(DataCollection("local"))
|
||||
|
||||
await datasource.load_all(order=["project", "contract"])
|
||||
|
||||
@@ -27,7 +27,7 @@ async def test_units_jsonl_handler_loads_units_filename(tmp_path):
|
||||
)
|
||||
|
||||
datasource = JsonlDataSource(tmp_path)
|
||||
datasource.register_handler(UnitsJsonlHandler(datasource))
|
||||
datasource.add_handler(UnitsJsonlHandler(datasource))
|
||||
datasource.set_collection(DataCollection("local"))
|
||||
|
||||
await datasource.load_all(order=["units"])
|
||||
|
||||
Reference in New Issue
Block a user