a8ea132924
medium effort code-review 暴露 8 个 finding,按 P0/P1/P2 优先级修复: P0 (数据正确性 / 跑得起来): #1 replace_all_node_categories 改 scope-aware delete 之前无条件 delete(NodeCategory) + 插入 runtime filter 后的子集, 导致 --type2 0 跑把其它 6 个 category 误删。改为只 delete 本批 category_key 集合,其它 type2 不动。 #2 replace_all_stock_node_map 对称化 之前空 rows 时 early-return 留下 stale,导致「categories 新 但 mappings 旧」不一致。改为 scope-aware delete 同 #1。 #3 stock_node systemd --workers 20 → 8 20 workers × 默认 10 RPS = 200 RPS,撞穿 mairui 钻石档 100 RPS 上限,触发风控。改为 8 × 10 = 80 RPS,留 20% buffer。 P1 (静默错): #4 is_a_share_code 接 hermes 格式 之前硬性要求 len(c)==6,iter_stock_codes 改返 hermes 时 5 个 task (moneyflow/share_snapshot/kline_5min/tick_trade/kline_daily) 会静默过滤成空 list。剥前缀再判断,兼容 'SH600519'。 #5 mairui tz 契约钉在 source 层 tz_localize 改为「已带 tz 就保留,没有再标 Asia/Shanghai」, 避免 mairui 改格式时抛 TypeError。task_tick_trade 删掉 strftime fallback + 冗长注释(契约已在 source 层 docstring)。 #6 CLI inspect.signature 过滤 kwargs --type2 / --backfill 加在共享 p_sync parser 上,任何 task 通过 **kwargs 静默吞掉。改为 dispatcher 按 task._run 签名过滤, 不支持的参数打 warning。 P2 (维护): #7 task_stock_node --type2 输入校验 之前空字符串 / 「concept」 / 99 都 silently collapse 成空 set, 走「无匹配叶子节点」warning 分支(被 daily_check 当正常)。 CLI 不再 silent-drop,任务层加 strict 校验,typo 返 status=error。 #8 code6_to_exchange 删 3 个死分支 4 个 code6_to_* wrapper 都先 _strip_hermes,3 个 hermes if 分支 走不到。删除后 function 简化,所有 caller 行为不变。 附带: - tests/test_smoke.py: 11 → 13 (含 mairui_ma_daily + stock_node) - tests/test_schema_models.py: 21 → 22 (含 kline_stock_ma_daily + node_categories/nodes/stock_node_map) - bin/market_sync_stock_node_run.sh 注释补充 RPS 计算 验证: - pytest tests/ 11 passed - is_a_share_code 8 个 hermes 边界 case 全过 - cli sync kline_daily --type2 2,3 → warning 已打印 - cli sync stock_node --type2 concept → status=error - cli sync stock_node --type2 99 → status=error - cli sync stock_node --type2 0 → 仅 0:0 行被刷新,其它 6 类不动 - fetch_tick_trade('600519') 返回 tz=Asia/Shanghai +08:00
108 lines
3.8 KiB
Python
108 lines
3.8 KiB
Python
"""冒烟测试:不依赖 MySQL/网络,只验证 import + 基本逻辑。
|
||
|
||
跑:pytest tests/test_smoke.py -v
|
||
"""
|
||
import pytest
|
||
|
||
|
||
def test_import_config():
|
||
from app.core.config import settings
|
||
assert settings is not None
|
||
assert hasattr(settings, "pg_host")
|
||
|
||
|
||
def test_import_data_source_base():
|
||
from app.core.datasource.base import DataSource, FetchResult, registry
|
||
assert DataSource is not None
|
||
assert FetchResult is not None
|
||
assert registry is not None
|
||
# registry 应该是 dict-like
|
||
assert hasattr(registry, "register")
|
||
assert hasattr(registry, "get")
|
||
|
||
|
||
def test_import_sync_base():
|
||
from app.core.sync.base import SyncTask, _is_market_closed, _effective_sync_end
|
||
assert SyncTask is not None
|
||
assert isinstance(_is_market_closed(), bool)
|
||
assert isinstance(_effective_sync_end(), str)
|
||
# 同步任务基类不允许空 dataset_id
|
||
with pytest.raises(ValueError):
|
||
SyncTask()
|
||
|
||
|
||
def test_tasks_registry():
|
||
from app.tasks import TASKS, get_task
|
||
assert isinstance(TASKS, dict)
|
||
# 注:每次新增 task 都要更新这里的数字
|
||
# 当前 13 个:stock_basic, kline_daily, kline_index, kline_5min, tick_trade,
|
||
# moneyflow, industry_sector, sector_features, share_snapshot, market_regime,
|
||
# longhubang, mairui_ma_daily, stock_node
|
||
assert len(TASKS) == 13
|
||
# get_task 应该返回实例
|
||
task = get_task("kline_daily")
|
||
assert task.dataset_id == "kline_daily"
|
||
|
||
|
||
def test_sync_definitions():
|
||
from app.core.sync.registry import SYNC_DEFINITIONS
|
||
assert len(SYNC_DEFINITIONS) == 13
|
||
# 所有 dataset_id 应唯一
|
||
ids = [d["dataset_id"] for d in SYNC_DEFINITIONS]
|
||
assert len(set(ids)) == 13
|
||
# tick_trade 应在其中且 sort_order=45
|
||
tt = next(d for d in SYNC_DEFINITIONS if d["dataset_id"] == "tick_trade")
|
||
assert tt["sort_order"] == 45
|
||
assert "stock_basic" in tt["dependency_ids"]
|
||
# longhubang 应在其中且 sort_order=85
|
||
lh = next(d for d in SYNC_DEFINITIONS if d["dataset_id"] == "longhubang")
|
||
assert lh["sort_order"] == 85
|
||
assert "stock_basic" in lh["dependency_ids"]
|
||
# stock_node 应在其中且 sort_order=21
|
||
sn = next(d for d in SYNC_DEFINITIONS if d["dataset_id"] == "stock_node")
|
||
assert sn["sort_order"] == 21
|
||
assert "stock_basic" in sn["dependency_ids"]
|
||
|
||
|
||
def test_build_default_registry():
|
||
"""不连接网络,只看是否能完成注册(部分源 may not available 不影响注册)。"""
|
||
from app.core.datasource.base import registry
|
||
from app.core.datasource.registry import build_default_registry
|
||
build_default_registry()
|
||
keys = [s.key for s in registry.all()]
|
||
assert "datasource_xinlang" in keys
|
||
assert "datasource_baostock" in keys
|
||
assert "datasource_mairui" in keys
|
||
# akshare:2026-07-01 决策更新——龙虎榜无替代源,烟测通过,可注册。
|
||
# 见 [[no-akshare]] 记忆:仅"无替代"+"akshare 烟测 OK"才允许用。
|
||
assert "datasource_akshare_lhb" in keys
|
||
assert "datasource_xueqiu" in keys
|
||
|
||
|
||
def test_data_source_abstract():
|
||
"""DataSource 抽象方法未实现时不应能被直接实例化。"""
|
||
from app.core.datasource.base import DataSource
|
||
with pytest.raises(TypeError):
|
||
DataSource() # 抽象方法未实现
|
||
|
||
|
||
def test_api_health_route():
|
||
"""FastAPI 加载 + /api/health 路由可访问。"""
|
||
from fastapi.testclient import TestClient
|
||
from app.api.main import app
|
||
|
||
client = TestClient(app)
|
||
resp = client.get("/api/health")
|
||
assert resp.status_code == 200
|
||
body = resp.json()
|
||
assert body.get("status") == "ok"
|
||
|
||
|
||
def test_effective_sync_end_format():
|
||
from app.core.sync.base import _effective_sync_end
|
||
s = _effective_sync_end()
|
||
# YYYY-MM-DD 共 10 字符
|
||
assert len(s) == 10
|
||
assert s[4] == "-"
|
||
assert s[7] == "-"
|