Files
homeagent-sdk/tools/gengskill/tests/test_modules.py

347 lines
12 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.

#!/usr/bin/env python3
"""
Geng Skill 单元测试
====================
验证各检测模块的正确性、边界条件处理和风险评分一致性。
运行方式:
cd geng-skill
python3 -m pytest tests/test_modules.py -v
或直接运行:
python3 tests/test_modules.py
"""
import sys
import os
import json
import random
import math
# 添加 scripts 目录到路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts'))
from last_digit_test import last_digit_test, extract_last_digit
from benford_test import benford_test, get_first_digit
from grim_test import grim_test_single, grim_test_batch
from fixed_relation_test import fixed_relation_test
from decimal_consistency_test import decimal_consistency_test
# ============================================================
# Test: Last Digit Test
# ============================================================
class TestLastDigitTest:
"""末位数字检测模块测试"""
def test_uniform_data_low_risk(self):
"""均匀分布的末位数字应返回低风险"""
random.seed(42)
# 生成末位数字均匀的数据
values = [f"{random.uniform(1, 100):.2f}" for _ in range(100)]
result = last_digit_test(values)
assert result['status'] == 'completed'
assert result['risk_level'] == 'low'
assert result['risk_score'] < 40
def test_concentrated_digits_high_risk(self):
"""集中在某个数字的末位应返回高风险"""
# 80% 的末位是 "5"
values = ['1.25', '2.35', '3.45', '4.55', '5.65',
'6.75', '7.85', '8.95', '9.15', '10.25',
'11.35', '12.45', '13.55', '14.65', '15.75',
'16.85', '17.95', '18.05', '19.15', '20.55']
result = last_digit_test(values)
assert result['status'] == 'completed'
assert result['most_frequent_digit'] == 5
def test_insufficient_data(self):
"""数据量不足应返回 insufficient_data"""
values = ['1.23', '4.56', '7.89']
result = last_digit_test(values)
assert result['status'] == 'insufficient_data'
def test_extract_last_digit(self):
"""末位数字提取正确性"""
assert extract_last_digit('3.14') == 4
assert extract_last_digit('100') == 1
assert extract_last_digit('-5.67') == 7
# ============================================================
# Test: Benford's Law Test
# ============================================================
class TestBenfordTest:
"""本福特定律检测模块测试"""
def test_benford_compliant_data(self):
"""符合本福特定律的数据应返回低风险"""
# 生成符合本福特定律的数据
random.seed(42)
values = []
for _ in range(200):
# 对数均匀分布产生的数据符合本福特定律
val = 10 ** (random.uniform(0, 4))
values.append(f"{val:.2f}")
result = benford_test(values)
assert result['status'] == 'completed'
# 对数均匀数据应近似符合
assert result['conformity'] in ('close', 'acceptable', 'marginal')
def test_uniform_first_digit_high_risk(self):
"""首位数字均匀分布应偏离本福特定律"""
# 人为造假数据:首位数字接近均匀
values = []
for d in range(1, 10):
for _ in range(20):
values.append(f"{d}{random.randint(10,99)}")
result = benford_test(values)
assert result['status'] == 'completed'
# 均匀分布会显著偏离本福特定律
assert result['p_value'] < 0.05
def test_insufficient_data(self):
"""数据量不足"""
values = ['123', '456']
result = benford_test(values)
assert result['status'] == 'insufficient_data'
def test_first_digit_extraction(self):
"""首位数字提取"""
assert get_first_digit('314.15') == 3
assert get_first_digit('0.0052') == 5
assert get_first_digit('9999') == 9
# ============================================================
# Test: GRIM Test
# ============================================================
class TestGrimTest:
"""GRIM 测试模块测试"""
def test_consistent_mean(self):
"""合法均值应通过"""
# n=20, 整数数据, mean=3.40 → sum=68 (整数) ✓
result = grim_test_single('3.40', 20, decimals=2)
assert result['consistent'] == True
def test_inconsistent_mean(self):
"""非法均值应失败"""
# n=20, 整数数据, mean=3.47 → sum=69.4 (非整数) ✗
result = grim_test_single('3.47', 20, decimals=2)
assert result['consistent'] == False
def test_consistent_mean_n25(self):
"""n=25 的合法均值"""
# n=25, mean=3.48 → sum=87 (整数) ✓
result = grim_test_single('3.48', 25, decimals=2)
assert result['consistent'] == True
def test_batch_mode(self):
"""批量 GRIM 测试"""
items = [
{'mean': '3.40', 'n': 20, 'decimals': 2, 'label': 'Item A'},
{'mean': '3.47', 'n': 20, 'decimals': 2, 'label': 'Item B'},
{'mean': '4.00', 'n': 10, 'decimals': 2, 'label': 'Item C'},
]
result = grim_test_batch(items)
assert result['status'] == 'completed'
assert result['total_items'] == 3
assert result['inconsistent_items'] == 1 # 3.47/20 不一致
def test_range_check(self):
"""量表范围检查"""
# 均值超出量表范围
result = grim_test_single('6.50', 20, decimals=2, scale_min=1, scale_max=5)
assert result.get('consistent') == False or result.get('status') == 'range_error'
# ============================================================
# Test: Fixed Relation Test
# ============================================================
class TestFixedRelationTest:
"""固定关系检测模块测试"""
def test_exact_ratio_detected(self):
"""精确固定比值应被检测到"""
col1 = [1.23, 2.34, 3.45, 4.56, 5.67, 6.78, 7.89]
col2 = [2.46, 4.68, 6.90, 9.12, 11.34, 13.56, 15.78] # ×2
result = fixed_relation_test(col1, col2)
assert result['status'] == 'completed'
assert result['risk_level'] == 'high'
assert result['risk_score'] >= 85
assert result['detections']['fixed_ratio']['is_exact'] == True
def test_exact_difference_detected(self):
"""精确固定差值应被检测到"""
col1 = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0]
col2 = [4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0] # +3
result = fixed_relation_test(col1, col2)
assert result['detections']['fixed_difference']['is_exact'] == True
def test_independent_data_low_risk(self):
"""独立随机数据应返回低风险"""
random.seed(42)
col1 = [random.uniform(1, 10) for _ in range(30)]
col2 = [random.uniform(1, 10) for _ in range(30)]
result = fixed_relation_test(col1, col2)
assert result['risk_level'] == 'low'
assert result['risk_score'] < 30
def test_insufficient_data(self):
"""数据不足"""
result = fixed_relation_test([1.0, 2.0], [3.0, 4.0])
assert result['status'] == 'insufficient_data'
def test_unequal_lengths(self):
"""长度不一致应报错"""
result = fixed_relation_test([1, 2, 3], [4, 5])
assert result['status'] == 'error'
# ============================================================
# Test: Decimal Consistency Test
# ============================================================
class TestDecimalConsistencyTest:
"""小数位一致性检测模块测试"""
def test_diverse_decimals_low_risk(self):
"""多样化小数模式应低风险"""
random.seed(42)
values = [f"{random.uniform(1, 100):.{random.randint(1,4)}f}" for _ in range(50)]
result = decimal_consistency_test(values)
assert result['status'] == 'completed'
assert result['risk_level'] in ('low', 'medium')
def test_repeated_decimals_high_risk(self):
"""高度重复的小数模式应高风险"""
# 所有值小数部分都是 .34
values = [f"{i}.34" for i in range(1, 31)]
result = decimal_consistency_test(values)
assert result['status'] == 'completed'
assert result['risk_score'] >= 25 # 至少中风险
def test_insufficient_data(self):
"""数据不足"""
values = ['1.23', '4.56']
result = decimal_consistency_test(values)
assert result['status'] == 'insufficient_data'
# ============================================================
# Integration Test
# ============================================================
class TestIntegration:
"""集成测试:模拟完整检测流程"""
def test_fake_data_high_risk(self):
"""已知造假数据应返回高风险"""
# 模拟耿同学发现的典型造假:固定比例关系
control = [2.34, 3.12, 1.87, 4.56, 2.98, 3.45, 1.23, 5.67, 2.01, 3.89]
treatment = [x * 2 for x in control] # 精确 ×2
result = fixed_relation_test(control, treatment, 'Control', 'Treatment')
assert result['risk_score'] >= 85
assert 'fixed_ratio' in result['detections']
assert result['detections']['fixed_ratio']['mean_ratio'] == 2.0
def test_real_data_low_risk(self):
"""正常实验数据应返回低风险"""
random.seed(123)
# 模拟真实实验:基础值 + 效应 + 随机噪声
control = [random.gauss(5, 1.5) for _ in range(20)]
treatment = [x * random.gauss(2, 0.3) for x in control] # ×2 但有变异
result = fixed_relation_test(control, treatment, 'Control', 'Treatment')
# 由于有随机噪声,不应该报告"精确"固定关系
assert result['detections']['fixed_ratio']['is_exact'] == False
def test_output_schema_compliance(self):
"""输出格式应符合标准 schema"""
values = [f"{random.uniform(1, 100):.2f}" for _ in range(50)]
result = last_digit_test(values)
# 必须包含的标准字段
required_fields = ['test_name', 'status', 'risk_level', 'risk_score', 'interpretation']
for field in required_fields:
assert field in result, f"缺少必需字段: {field}"
# 风险评分范围
assert 0 <= result['risk_score'] <= 100
# 风险等级合法值
assert result['risk_level'] in ('low', 'medium', 'medium-high', 'high')
# ============================================================
# Run tests
# ============================================================
def run_all_tests():
"""简易测试运行器(不依赖 pytest"""
import traceback
test_classes = [
TestLastDigitTest,
TestBenfordTest,
TestGrimTest,
TestFixedRelationTest,
TestDecimalConsistencyTest,
TestIntegration,
]
total = 0
passed = 0
failed = 0
errors = []
print("=" * 70)
print(" 🧪 Geng Skill 单元测试")
print("=" * 70)
print()
for test_class in test_classes:
class_name = test_class.__name__
print(f"{class_name}")
instance = test_class()
methods = [m for m in dir(instance) if m.startswith('test_')]
for method_name in methods:
total += 1
try:
getattr(instance, method_name)()
passed += 1
print(f"{method_name}")
except AssertionError as e:
failed += 1
errors.append((class_name, method_name, str(e)))
print(f"{method_name}: {e}")
except Exception as e:
failed += 1
errors.append((class_name, method_name, traceback.format_exc()))
print(f" 💥 {method_name}: {type(e).__name__}: {e}")
print()
print("=" * 70)
print(f" 结果: {passed} 通过 / {failed} 失败 / {total} 总计")
print("=" * 70)
if errors:
print("\n❌ 失败详情:")
for cls, method, err in errors:
print(f" {cls}.{method}: {err[:200]}")
return failed == 0
if __name__ == '__main__':
success = run_all_tests()
sys.exit(0 if success else 1)