问题 - 索引中该目录记为小写 `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 条。
203 lines
6.3 KiB
Python
203 lines
6.3 KiB
Python
# coding=utf-8
|
||
"""init_db.py 数据库初始化脚本单元测试.
|
||
|
||
重点覆盖:
|
||
- init_tushare_data:tushare_data 库创建 + _table_meta 建表(b501e35 新增)
|
||
- init_tushare_data 被 main() 正确调用并纳入 steps
|
||
- execute_sql:成功/异常路径
|
||
- check_database:连接成功/失败
|
||
"""
|
||
|
||
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 init_db
|
||
|
||
|
||
class _FakeCursor:
|
||
"""模拟 cursor,记录执行的 SQL."""
|
||
|
||
def __init__(self):
|
||
self.executed = []
|
||
|
||
def execute(self, sql, params=None):
|
||
self.executed.append(sql)
|
||
|
||
def close(self):
|
||
pass
|
||
|
||
|
||
class _FakeConn:
|
||
"""模拟 pymysql 连接,记录 select_db 调用."""
|
||
|
||
def __init__(self):
|
||
self._cursor = _FakeCursor()
|
||
self.selected_dbs = []
|
||
self.committed = False
|
||
|
||
def cursor(self):
|
||
# execute_sql 用 with conn.cursor() as cur 模式
|
||
return _ContextCursor(self._cursor)
|
||
|
||
def select_db(self, db):
|
||
self.selected_dbs.append(db)
|
||
|
||
def commit(self):
|
||
self.committed = True
|
||
|
||
def close(self):
|
||
pass
|
||
|
||
|
||
class _ContextCursor:
|
||
"""with conn.cursor() as cur 的上下文管理器包装."""
|
||
|
||
def __init__(self, cursor):
|
||
self._cursor = cursor
|
||
|
||
def __enter__(self):
|
||
return self._cursor
|
||
|
||
def __exit__(self, *a):
|
||
return False
|
||
|
||
|
||
# ── init_tushare_data ─────────────────────────────────────
|
||
|
||
|
||
class TestInitTushareData:
|
||
"""init_tushare_data 测试(b501e35 新增)."""
|
||
|
||
def test_creates_database_and_table(self):
|
||
"""验证创建 tushare_data 库和 _table_meta 表."""
|
||
conn = _FakeConn()
|
||
steps = init_db.init_tushare_data(conn)
|
||
|
||
assert len(steps) == 2
|
||
assert "tushare_data" in steps[0]
|
||
assert "_table_meta" in steps[1]
|
||
|
||
# 验证 SQL 执行内容
|
||
all_sql = " ".join(conn._cursor.executed)
|
||
assert "CREATE DATABASE IF NOT EXISTS" in all_sql
|
||
assert "tushare_data" in all_sql
|
||
assert "CREATE TABLE IF NOT EXISTS" in all_sql
|
||
assert "_table_meta" in all_sql
|
||
|
||
def test_selects_tushare_database(self):
|
||
"""验证 select_db 被调用且参数为 tushare_data."""
|
||
conn = _FakeConn()
|
||
init_db.init_tushare_data(conn)
|
||
|
||
assert init_db.MYSQL_DATABASE_TUSHARE in conn.selected_dbs
|
||
|
||
def test_table_meta_has_correct_columns(self):
|
||
"""验证 _table_meta 表结构包含必要列."""
|
||
conn = _FakeConn()
|
||
init_db.init_tushare_data(conn)
|
||
|
||
table_sql = conn._cursor.executed[1]
|
||
for col in ["table_name", "last_update", "row_count", "status"]:
|
||
assert col in table_sql
|
||
|
||
def test_table_meta_primary_key(self):
|
||
"""验证 _table_meta 以 table_name 为主键."""
|
||
conn = _FakeConn()
|
||
init_db.init_tushare_data(conn)
|
||
|
||
table_sql = conn._cursor.executed[1]
|
||
assert "PRIMARY KEY" in table_sql
|
||
assert "table_name" in table_sql
|
||
|
||
|
||
# ── execute_sql ───────────────────────────────────────────
|
||
|
||
|
||
class TestExecuteSql:
|
||
"""execute_sql 成功/异常路径测试."""
|
||
|
||
def test_success_returns_true(self):
|
||
conn = _FakeConn()
|
||
assert init_db.execute_sql(conn, "CREATE DATABASE x") is True
|
||
assert conn.committed is True
|
||
|
||
def test_exception_returns_false(self, capsys):
|
||
"""执行失败时返回 False 并输出错误到 stderr."""
|
||
class FailConn(_FakeConn):
|
||
def cursor(self):
|
||
raise RuntimeError("boom")
|
||
|
||
conn = FailConn()
|
||
result = init_db.execute_sql(conn, "SELECT 1")
|
||
assert result is False
|
||
captured = capsys.readouterr()
|
||
assert "SQL ERROR" in captured.err
|
||
|
||
|
||
# ── check_database ────────────────────────────────────────
|
||
|
||
|
||
class TestCheckDatabase:
|
||
"""check_database 连接检查测试."""
|
||
|
||
def test_success_returns_true(self):
|
||
fake_conn = MagicMock()
|
||
with patch.object(init_db, "connect", return_value=fake_conn):
|
||
assert init_db.check_database("h", 3306, "u", "p", "db") is True
|
||
fake_conn.close.assert_called_once()
|
||
|
||
def test_failure_returns_false(self, capsys):
|
||
with patch.object(init_db, "connect", side_effect=RuntimeError("refused")):
|
||
assert init_db.check_database("h", 3306, "u", "p", "db") is False
|
||
captured = capsys.readouterr()
|
||
assert "CHECK FAILED" in captured.err
|
||
|
||
|
||
# ── main 集成 ─────────────────────────────────────────────
|
||
|
||
|
||
class TestMainIntegration:
|
||
"""main() 调用三个 init_* 函数的集成测试."""
|
||
|
||
def test_main_includes_tushare_steps(self, capsys):
|
||
"""main() 结果中包含 tushare_data 初始化步骤."""
|
||
fake_conn = _FakeConn()
|
||
with patch.object(init_db, "connect", return_value=fake_conn):
|
||
init_db.main()
|
||
|
||
result = json.loads(capsys.readouterr().out)
|
||
assert result["success"] is True
|
||
all_steps = " ".join(result["steps"])
|
||
assert "tushare_data" in all_steps
|
||
assert "_table_meta" in all_steps
|
||
|
||
def test_main_calls_all_three_init_functions(self, capsys):
|
||
"""main() 调用 news_flash + akshare_data + tushare_data 三个初始化."""
|
||
fake_conn = _FakeConn()
|
||
with patch.object(init_db, "connect", return_value=fake_conn):
|
||
init_db.main()
|
||
|
||
result = json.loads(capsys.readouterr().out)
|
||
all_steps = " ".join(result["steps"])
|
||
assert "news_flash" in all_steps
|
||
assert "akshare_data" in all_steps
|
||
assert "tushare_data" in all_steps
|
||
|
||
def test_main_connection_error(self, capsys):
|
||
"""main() 连接失败时 success=False 且输出错误."""
|
||
with patch.object(init_db, "connect", side_effect=RuntimeError("no mysql")):
|
||
init_db.main()
|
||
|
||
result = json.loads(capsys.readouterr().out)
|
||
assert result["success"] is False
|
||
assert "no mysql" in result["error"]
|