问题 - 索引中该目录记为小写 `scripts/`,工作区实为 `Scripts/`。因 core.ignorecase=true, 本地 `git diff -- Scripts/bin/x.py` 会**静默返回空**(看似无改动), 且在大小写敏感环境(Linux CI / Docker 挂载)会检出 `scripts/`, 与全仓所有 `Scripts/...` 引用不匹配。 - 该目录此前 10 个脚本(check_python_version.py、perf_snapshot.py、 install_collector_launchd.py、mysql_toggle.sh 等)从未入库, 而 CI 第 31 行正调用 `Scripts/bin/check_python_version.py --strict` —— 全新检出必然失败。 改动 - 索引路径统一为 `Scripts/`(工作区文件未移动,仅索引改名)。 - 补齐上述 10 个未跟踪脚本及其测试。 影响 - 修正前索引 35 条(全为小写路径),修正后 45 条。
185 lines
6.4 KiB
Python
185 lines
6.4 KiB
Python
# coding=utf-8
|
|
"""check_db.py 数据库状态检测单元测试.
|
|
|
|
重点覆盖:
|
|
- tushare_data 数据库存在性检测(b501e35 新增逻辑)
|
|
- 三库(news_flash / akshare_data / tushare_data)表枚举
|
|
- ready 标志判定逻辑
|
|
- MySQL 连接失败时的错误降级
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
# 将 Scripts/bin 加入 sys.path
|
|
_bin_dir = os.path.join(os.path.dirname(__file__), "..")
|
|
if _bin_dir not in sys.path:
|
|
sys.path.insert(0, _bin_dir)
|
|
|
|
import check_db
|
|
|
|
|
|
class _FakeCursor:
|
|
"""模拟 pymysql cursor,按 SQL 关键词返回预设结果."""
|
|
|
|
def __init__(self, databases=None, tables_map=None):
|
|
self._databases = databases or []
|
|
self._tables_map = tables_map or {}
|
|
self._current_db = None
|
|
|
|
def execute(self, sql, params=None):
|
|
sql_upper = sql.upper().strip()
|
|
if sql_upper.startswith("SHOW DATABASES"):
|
|
self._fetchall_result = [(d,) for d in self._databases]
|
|
elif sql_upper.startswith("SHOW TABLES"):
|
|
self._fetchall_result = [
|
|
(t,) for t in self._tables_map.get(self._current_db, [])
|
|
]
|
|
|
|
def fetchall(self):
|
|
return getattr(self, "_fetchall_result", [])
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
|
|
class _FakeConn:
|
|
"""模拟 pymysql 连接,记录 select_db 调用."""
|
|
|
|
def __init__(self, databases=None, tables_map=None):
|
|
self._cursor = _FakeCursor(databases, tables_map)
|
|
self.selected_dbs = []
|
|
|
|
def cursor(self):
|
|
return self._cursor
|
|
|
|
def select_db(self, db):
|
|
self.selected_dbs.append(db)
|
|
self._cursor._current_db = db
|
|
|
|
def close(self):
|
|
pass
|
|
|
|
|
|
class TestCheckDbTushare:
|
|
"""tushare_data 数据库检测测试(b501e35 新增)."""
|
|
|
|
def test_tushare_exists_with_tables(self, capsys):
|
|
"""tushare_data 库存在且有表时,tushare_data_exists=True 且表被枚举."""
|
|
fake_conn = _FakeConn(
|
|
databases=["news_flash", "akshare_data", "tushare_data"],
|
|
tables_map={
|
|
"news_flash": ["news_sources", "news_flash", "push_log", "fetch_log"],
|
|
"akshare_data": ["stock_daily"],
|
|
"tushare_data": ["stock_daily", "stock_weekly"],
|
|
},
|
|
)
|
|
with patch.object(check_db, "connect", return_value=fake_conn):
|
|
check_db.main()
|
|
|
|
result = json.loads(capsys.readouterr().out)
|
|
assert result["tushare_data_exists"] is True
|
|
assert "stock_daily" in result["tushare_data_tables"]
|
|
assert "stock_weekly" in result["tushare_data_tables"]
|
|
|
|
def test_tushare_not_exists(self, capsys):
|
|
"""tushare_data 库不存在时,tushare_data_exists=False 且表列表为空."""
|
|
fake_conn = _FakeConn(
|
|
databases=["news_flash", "akshare_data"],
|
|
tables_map={
|
|
"news_flash": ["news_sources", "news_flash", "push_log", "fetch_log"],
|
|
"akshare_data": ["stock_daily"],
|
|
},
|
|
)
|
|
with patch.object(check_db, "connect", return_value=fake_conn):
|
|
check_db.main()
|
|
|
|
result = json.loads(capsys.readouterr().out)
|
|
assert result["tushare_data_exists"] is False
|
|
assert result["tushare_data_tables"] == []
|
|
|
|
def test_all_three_databases_checked(self, capsys):
|
|
"""三库同时存在时均被检查且 select_db 被调用三次."""
|
|
fake_conn = _FakeConn(
|
|
databases=["news_flash", "akshare_data", "tushare_data"],
|
|
tables_map={
|
|
"news_flash": ["news_sources", "news_flash", "push_log", "fetch_log"],
|
|
"akshare_data": ["stock_daily"],
|
|
"tushare_data": ["_table_meta"],
|
|
},
|
|
)
|
|
with patch.object(check_db, "connect", return_value=fake_conn):
|
|
check_db.main()
|
|
|
|
assert "news_flash" in fake_conn.selected_dbs
|
|
assert "akshare_data" in fake_conn.selected_dbs
|
|
assert "tushare_data" in fake_conn.selected_dbs
|
|
|
|
|
|
class TestCheckDbReady:
|
|
"""ready 标志判定逻辑测试."""
|
|
|
|
def test_ready_when_news_flash_has_all_required_tables(self, capsys):
|
|
"""news_flash 含全部必需表时 ready=True."""
|
|
fake_conn = _FakeConn(
|
|
databases=["news_flash", "akshare_data", "tushare_data"],
|
|
tables_map={
|
|
"news_flash": ["news_sources", "news_flash", "push_log", "fetch_log"],
|
|
"akshare_data": [],
|
|
"tushare_data": [],
|
|
},
|
|
)
|
|
with patch.object(check_db, "connect", return_value=fake_conn):
|
|
check_db.main()
|
|
|
|
result = json.loads(capsys.readouterr().out)
|
|
assert result["ready"] is True
|
|
|
|
def test_not_ready_when_missing_required_table(self, capsys):
|
|
"""news_flash 缺少必需表时 ready=False."""
|
|
fake_conn = _FakeConn(
|
|
databases=["news_flash", "akshare_data", "tushare_data"],
|
|
tables_map={
|
|
"news_flash": ["news_sources", "news_flash"], # 缺 push_log, fetch_log
|
|
"akshare_data": [],
|
|
"tushare_data": [],
|
|
},
|
|
)
|
|
with patch.object(check_db, "connect", return_value=fake_conn):
|
|
check_db.main()
|
|
|
|
result = json.loads(capsys.readouterr().out)
|
|
assert result["ready"] is False
|
|
|
|
|
|
class TestCheckDbConnectionError:
|
|
"""连接失败时的错误降级测试."""
|
|
|
|
def test_connection_error_sets_error(self, capsys):
|
|
"""MySQL 不可达时 error 被设置,ready=False."""
|
|
with patch.object(check_db, "connect", side_effect=RuntimeError("conn refused")):
|
|
check_db.main()
|
|
|
|
result = json.loads(capsys.readouterr().out)
|
|
assert result["mysql_reachable"] is False
|
|
assert result["ready"] is False
|
|
assert "conn refused" in result["error"]
|
|
|
|
def test_result_has_all_keys_even_on_error(self, capsys):
|
|
"""连接失败时结果仍包含所有预期字段(含 tushare 字段)."""
|
|
with patch.object(check_db, "connect", side_effect=RuntimeError("timeout")):
|
|
check_db.main()
|
|
|
|
result = json.loads(capsys.readouterr().out)
|
|
expected_keys = {
|
|
"mysql_reachable", "news_flash_exists", "news_flash_tables",
|
|
"akshare_data_exists", "akshare_data_tables",
|
|
"tushare_data_exists", "tushare_data_tables",
|
|
"ready", "error",
|
|
}
|
|
assert expected_keys.issubset(set(result.keys()))
|