Files
MCM-ICM_Toolkit/tests/test_core.py
T
guzhujushi c93810ffb1 fix(core): 补齐 get_rng 类型校验,set_seed 覆盖 numpy 旧全局
get_rng 的"共享单例生成器"设计由用户本人提出,本提交只在其上修 bug。

- random_state 判断从 isinstance(x, int) 放宽到 (int, np.integer):
  np.int64 不是 int 的子类,之前会静默退回全局流、种子被无声忽略
- 非预期类型改成抛 TypeError(含 bool / float / str / list),不再静默降级
- get_rng 签名补上 np.integer 与 Generator,与 docstring 的承诺一致
- set_seed 同时调用 np.random.seed:sklearn / statsmodels 走的是 numpy
  旧全局状态,不覆盖它们"提交能重跑"就有洞(带 noqa: NPY002 与理由)
- 测试:TestRandom 由 4 条增到 7 条(np.integer、非法类型、共享生成器推进)
- 新增 docs/dev/personal-pitfalls.md:个人易错点清单,只追加、不通读,
  首批记录本轮 4 个点;docs/README.md 补上 05 与该文件的索引
- STATUS.md:L0 进度说明更新,决策 24 改写(共享单例)、新增决策 25
  (src/ 实现默认由用户本人写)
2026-10-01 19:38:59 +08:00

143 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""core 包测试。
TODO(L0):TestValidate 实现后去掉 skip 标记(Result 与 Random 已完成)。
"""
from __future__ import annotations
import numpy as np
import pytest
class TestRandom:
def test_get_rng_is_reproducible(self):
from mcm_toolkit.core import get_rng
assert get_rng(42).random() == get_rng(42).random()
def test_get_rng_passes_through_generator(self):
from mcm_toolkit.core import get_rng
g = np.random.default_rng(0)
assert get_rng(g) is g
def test_get_rng_accepts_numpy_integer(self):
"""np.int64 不是 int 的子类,必须当成整数处理,不能静默退化成全局流。"""
from mcm_toolkit.core import get_rng, set_seed
set_seed(1)
first = get_rng(np.int64(42))
second = get_rng(np.int64(42))
assert first is not second, "传 np.integer 应当新建独立生成器"
assert first.random() == np.random.default_rng(42).random()
def test_get_rng_rejects_bad_types(self):
"""类型不对就报错。静默当成"没传种子",换来的只会是不可复现的结果。"""
from mcm_toolkit.core import get_rng
for bad in ["42", 42.0, True, [42]]:
with pytest.raises(TypeError):
get_rng(bad)
def test_get_rng_without_seed_shares_and_advances(self):
from mcm_toolkit.core import get_rng, set_seed
set_seed(3)
assert get_rng() is get_rng(), "无参调用应返回同一个共享生成器"
assert get_rng().random() != get_rng().random(), "共享生成器必须被推进"
def test_get_rng_default_follows_global_seed(self):
"""set_seed 之后,不传 random_state 也必须复现——这是"论文能重跑"的底线。"""
from mcm_toolkit.core import get_rng, set_seed
set_seed(7)
first = get_rng().random()
set_seed(7)
second = get_rng().random()
assert first == second
def test_set_seed_also_covers_third_party_global_state(self):
"""sklearn / statsmodels 走的是 np.random 旧全局,set_seed 要连它一起覆盖。"""
from mcm_toolkit.core import set_seed
set_seed(7)
first = np.random.random() # noqa: NPY002 - 这里就是要验证旧全局被设上了
set_seed(7)
second = np.random.random() # noqa: NPY002
assert first == second
@pytest.mark.skip(reason="未实现:见 docs/03-roadmap.md L0")
class TestValidate:
def test_check_same_length_raises_with_detail(self):
from mcm_toolkit.core import check_same_length
with pytest.raises(ValueError, match=r"2.*3|长度"):
check_same_length([1, 2], [1, 2, 3])
def test_check_array_rejects_nan(self):
from mcm_toolkit.core import check_array
with pytest.raises(ValueError):
check_array([1.0, np.nan, 2.0], finite=True)
def test_check_probability_bounds(self):
from mcm_toolkit.core import check_probability
assert check_probability(0.5) == 0.5
with pytest.raises(ValueError):
check_probability(1.5)
def test_check_square_matrix(self):
from mcm_toolkit.core import check_square_matrix
check_square_matrix([[1, 2], [3, 4]])
with pytest.raises(ValueError):
check_square_matrix([[1, 2, 3], [4, 5, 6]])
def test_result_repr_is_readable():
from mcm_toolkit.core import Result
text = repr(Result("demo", params={"a": 1}, metrics={"r2": 0.9}))
# 只断言关键信息在场,不断言完整字符串——否则以后调格式会让测试集体变红
assert "demo" in text and "r2" in text
def test_result_repr_stays_short_and_safe():
"""默认值、失败结果、十万级数组:repr 都不能崩,也不能刷屏。"""
from mcm_toolkit.core import Result
assert "empty" in repr(Result("empty"))
failed = repr(Result("boom", success=False, message="求解未收敛"))
assert "success=False" in failed
assert "求解未收敛" in failed
noisy = repr(Result("mc", data=np.arange(100_000)))
assert "ndarray(100000)" in noisy
assert len(noisy) < 200, "data 不应被整个打印出来"
def test_result_summary_is_markdown_table():
from mcm_toolkit.core import Result
result = Result("topsis", params={"n": 3}, metrics={"best": 0.83})
text = result.summary()
lines = text.splitlines()
assert lines[0] == "### topsis"
assert lines[2] == "| 项 | 值 |"
assert "| success | True |" in lines
assert "| params.n | 3 |" in lines
assert "| metrics.best | 0.83 |" in lines
assert result.summary() == text, "summary 必须无副作用、可重复调用"
def test_result_summary_appends_message_on_failure():
from mcm_toolkit.core import Result
text = Result("gm11", success=False, message="样本太少").summary()
assert "| success | False |" in text
assert text.rstrip().endswith("> 样本太少")