"""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_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("> 样本太少")