"""运行: python -m unittest test_check_consistency -v""" import json import subprocess import sys import tempfile import unittest from pathlib import Path import check_consistency as cc import make_samples HERE = Path(__file__).resolve().parent PARAMS = str(HERE / "params.example.json") def para(text, n=1): return {"loc": f"段落 #{n}", "text": text, "kind": "para", "heading": False} def run_text(text, quantities): with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as f: json.dump({"quantities": quantities}, f, ensure_ascii=False) Q, cfg = cc.load_params(f.name) return cc.check([para(text)], Q, cfg) def naive_ok(v, got): """海豚原帖片段的判定方式: str(v) in got 即视为一致(子串匹配)。""" return str(v) in got class SubstringFlaw(unittest.TestCase): COD = {"cod_out": {"value": 5, "unit": "mg/L", "decimals": 1, "aliases": ["出水COD"]}} FLOW = {"flow": {"value": 1500, "unit": "t/d", "decimals": 0, "unit_decimals": {"m³/h": 1}, "aliases": ["小时流量"]}} def test_false_pass_5_in_15(self): doc = "出水COD 15 mg/L" self.assertTrue(naive_ok(5, doc)) # 子串匹配: 误通过 self.assertEqual(len(run_text(doc, self.COD)["inconsistencies"]), 1) # 本脚本: 命中 def test_false_alarm_5_0_vs_5(self): doc = "出水COD 5 mg/L" self.assertFalse(naive_ok("5.0", doc)) # 子串匹配: 误报 self.assertEqual(run_text(doc, self.COD)["inconsistencies"], []) # 本脚本: 一致 def test_false_pass_62_5_in_162_5(self): doc = "小时流量 162.5 m³/h" self.assertTrue(naive_ok(62.5, doc)) # 子串匹配: 误通过 self.assertEqual(len(run_text(doc, self.FLOW)["inconsistencies"]), 1) def test_unit_conversion_is_equal(self): self.assertEqual(run_text("小时流量 62.5 m³/h", self.FLOW)["inconsistencies"], []) self.assertEqual(run_text("小时流量 1500 吨/天", self.FLOW)["inconsistencies"], []) class SafeFormula(unittest.TestCase): def test_rejects_code(self): for expr in ["__import__('os').system('echo hi')", "a.real", "[a]", "a ** 99999", "open('x')"]: with self.assertRaises(cc.ParamsError): cc.safe_eval(expr, lambda n: 1.0) def test_arithmetic_and_cycle(self): self.assertEqual(cc.safe_eval("(a - b) / a * 100", {"a": 400, "b": 50}.get), 87.5) with self.assertRaises(cc.ParamsError): run_text("x", {"a": {"formula": "b + 1", "unit": "t/d"}, "b": {"formula": "a * 2", "unit": "t/d"}}) class Samples(unittest.TestCase): @classmethod def setUpClass(cls): cls.tmp = Path(tempfile.mkdtemp()) make_samples.build(cls.tmp / "clean.docx", PARAMS) make_samples.build(cls.tmp / "drifted.docx", PARAMS, drift=True) def cli(self, name): p = subprocess.run([sys.executable, str(HERE / "check_consistency.py"), PARAMS, str(self.tmp / name), "--json"], capture_output=True, text=True) return p.returncode, json.loads(p.stdout) def test_clean_is_empty(self): code, rep = self.cli("clean.docx") self.assertEqual((code, rep["bare_numbers"], rep["inconsistencies"]), (0, [], [])) def test_drifted_hits_exactly_three(self): code, rep = self.cli("drifted.docx") self.assertEqual(code, 1) self.assertEqual(rep["bare_numbers"], []) got = sorted((i["kind"], i["key"], i["location"]) for i in rep["inconsistencies"]) self.assertEqual(got, [ ("derived", "pac_annual_cost_wan", "表格 2 第 4 行第 3 列"), ("rounding", "power_kwh_per_t", "段落 #8"), ("unit", "treated_flow_tpd", "表格 1 第 3 行第 3 列"), ]) for i in rep["inconsistencies"]: self.assertTrue(i["consistent_elsewhere"], i) # 每处都给出另一处的位置与取值 def test_bare_number_detected(self): Q, cfg = cc.load_params(PARAMS) rep = cc.check([para("反应池有效水深 4.5 m,2026年投运。")], Q, cfg) self.assertEqual([b["written"] for b in rep["bare_numbers"]], ["4.5"]) if __name__ == "__main__": unittest.main()