@furongjun1999/dsh-memory 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +588 -572
- package/codebuddy/CODEBUDDY.md +10 -10
- package/data/policy.json +27 -0
- package/docs/discipline/harnesses.yaml +31 -0
- package/docs/discipline/templates/full.md.tmpl +1 -1
- package/docs/discipline/templates/rules.mdc.tmpl +1 -1
- package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +418 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +441 -0
- package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +398 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +498 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +669 -0
- package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +683 -0
- package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +205 -0
- package/docs/eval//346/200/247/350/203/275/344/270/223/351/241/271_/345/206/267/346/237/245/350/257/242/344/270/216/345/206/205/345/255/230_v1.0.md +218 -0
- package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +10 -0
- package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +646 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v20.md +283 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v21.md +224 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v22.md +223 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v23.md +293 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v24.md +303 -0
- package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v25.md +230 -0
- package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +20 -0
- package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
- package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +12 -0
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +21 -9
- package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +7 -2
- package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +39 -3
- package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +220 -0
- package/docs/plans//347/234/237/346/272/220/347/264/242/345/274/225_/351/200/232/347/224/250/346/234/272/345/210/266_v0.3.md +227 -0
- package/docs/plans//350/234/202/345/267/242/346/250/241/345/236/213/345/257/206/351/222/245/351/205/215/347/275/256/351/235/242_v1.0.md +385 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +460 -460
- package/lib/bridge.js +24 -2
- package/lib/hooks.d.ts +31 -2
- package/lib/hooks.js +219 -16
- package/lib/init.js +16 -1
- package/lib/lib/prompt_safety.d.ts +52 -1
- package/lib/lib/prompt_safety.js +76 -1
- package/md_cg/audit.py +652 -379
- package/md_cg/bench6_arms.py +1 -1
- package/md_cg/bench_p0.py +1 -1
- package/md_cg/branches.py +2 -2
- package/md_cg/ccgc.py +1071 -1006
- package/md_cg/chain.py +1 -1
- package/md_cg/consistency.py +97 -9
- package/md_cg/consolidate.py +34 -9
- package/md_cg/crypto.py +59 -26
- package/md_cg/docindex.py +16 -1
- package/md_cg/evolution.py +17 -1
- package/md_cg/export.py +1 -1
- package/md_cg/forgetting.py +309 -13
- package/md_cg/fsutil.py +454 -8
- package/md_cg/identity.py +3 -3
- package/md_cg/insight.py +24 -3
- package/md_cg/linkref.py +1 -1
- package/md_cg/logref.py +327 -0
- package/md_cg/mcp_server.py +4152 -3818
- package/md_cg/mdcg.py +4208 -3547
- package/md_cg/mdcos.py +4491 -4159
- package/md_cg/mreview/pipeline.py +15 -1
- package/md_cg/nodefile.py +639 -575
- package/md_cg/protect.py +158 -11
- package/md_cg/protocol.py +41 -2
- package/md_cg/provenance.py +22 -4
- package/md_cg/reach.py +4 -4
- package/md_cg/readcache.py +76 -15
- package/md_cg/reconcile.py +1 -1
- package/md_cg/refindex.py +246 -37
- package/md_cg/refine.py +1 -1
- package/md_cg/routing.py +32 -5
- package/md_cg/run_tests.py +17 -0
- package/md_cg/scrub.py +30 -3
- package/md_cg/security.py +47 -1
- package/md_cg/self_state.py +5 -5
- package/md_cg/sources.py +23 -6
- package/md_cg/srcindex.py +352 -0
- package/md_cg/stg.py +47 -3
- package/md_cg/subgraph.py +2 -2
- package/md_cg/sustain.py +1299 -1168
- package/md_cg/tasks.py +469 -470
- package/md_cg/test_b1_auto_id_multiproc.py +277 -0
- package/md_cg/test_b1b2_write_face.py +417 -0
- package/md_cg/test_b2_sensitivity_landing.py +223 -0
- package/md_cg/test_b3_merge_keeps_content.py +576 -0
- package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
- package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
- package/md_cg/test_c3_transient_read_negative.py +793 -0
- package/md_cg/test_c8_search_rrf_gates.py +502 -0
- package/md_cg/test_ccg_form_parity.py +188 -0
- package/md_cg/test_ccgc.py +28 -3
- package/md_cg/test_govern_directread.py +17 -4
- package/md_cg/test_h2_session_view_norm.py +233 -0
- package/md_cg/test_h4_sustain_snapshot.py +1377 -0
- package/md_cg/test_hive_ingest.py +285 -285
- package/md_cg/test_issue39_utf8_stdio.py +307 -16
- package/md_cg/test_issue43_default_policy.py +865 -0
- package/md_cg/test_legacy_p3_node_id_type.py +375 -0
- package/md_cg/test_linkref.py +10 -3
- package/md_cg/test_lock.py +2 -2
- package/md_cg/test_logref.py +1109 -0
- package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
- package/md_cg/test_m3_h9_semantic_guard.py +525 -0
- package/md_cg/test_mr_m2.py +15 -3
- package/md_cg/test_n130_verify_falsified_protect.py +1 -1
- package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
- package/md_cg/test_n176_link_trust.py +221 -0
- package/md_cg/test_n178_units_jobid_gate.py +212 -0
- package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
- package/md_cg/test_n195_writepath_reload.py +283 -0
- package/md_cg/test_n196_stale_gate_skip.py +169 -0
- package/md_cg/test_n197_n208_write_face_gates.py +471 -0
- package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
- package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
- package/md_cg/test_n201_proposal_visibility.py +382 -0
- package/md_cg/test_n202_session_notes_visibility.py +461 -0
- package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
- package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
- package/md_cg/test_n209_verify_write_face_gates.py +461 -0
- package/md_cg/test_n212_n213_n224_generation_gates.py +513 -0
- package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
- package/md_cg/test_n225_nonobject_load.py +1110 -0
- package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
- package/md_cg/test_neg_condition_hits.py +333 -0
- package/md_cg/test_neg_tail_honesty.py +663 -0
- package/md_cg/test_none_id_write_guard.py +4 -3
- package/md_cg/test_opt_batch1_md_cg.py +451 -0
- package/md_cg/test_p1.py +5 -5
- package/md_cg/test_p11_consistency.py +161 -12
- package/md_cg/test_p26_refindex.py +2 -2
- package/md_cg/test_p27_docindex.py +777 -774
- package/md_cg/test_p28_refcheck.py +740 -13
- package/md_cg/test_p29_session_ingest_export.py +2 -2
- package/md_cg/test_p2_mcp.py +12 -1
- package/md_cg/test_p30_maintain.py +33 -2
- package/md_cg/test_p31_insight.py +1 -0
- package/md_cg/test_p9c_dedup_hints.py +276 -0
- package/md_cg/test_policy_required_ccg.py +426 -0
- package/md_cg/test_protocol.py +22 -0
- package/md_cg/test_rank_parity_score_mode.py +516 -0
- package/md_cg/test_read_face_input_gates.py +363 -0
- package/md_cg/test_read_face_semantics.py +489 -0
- package/md_cg/test_recall_face_guards.py +812 -0
- package/md_cg/test_rejected_credential_forms.py +188 -0
- package/md_cg/test_rejected_redact.py +146 -0
- package/md_cg/test_retr_s1b.py +2 -2
- package/md_cg/test_retr_s5.py +20 -7
- package/md_cg/test_review_conformance.py +21 -3
- package/md_cg/test_security_audit_v21.py +2 -2
- package/md_cg/test_server_version.py +67 -0
- package/md_cg/test_srcindex.py +171 -0
- package/md_cg/test_tenant_env_override_warn.py +63 -50
- package/md_cg/test_token_lowercase_form.py +315 -0
- package/md_cg/test_v21r1_package_version.py +310 -0
- package/md_cg/test_writepipe.py +10 -4
- package/md_cg/tokens.py +225 -95
- package/md_cg/tool_face.py +4 -4
- package/md_cg/trust.py +49 -5
- package/md_cg/units.py +204 -6
- package/md_cg/weights.py +4 -4
- package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
- package/md_cg/writelimit.py +42 -8
- package/md_cg/writepipe.py +651 -554
- package/package.json +3 -2
- package/skills/plugin.json +1 -1
- package/src/bridge.ts +25 -2
- package/src/hooks.ts +229 -16
- package/src/init.ts +15 -1
- package/src/lib/prompt_safety.ts +88 -1
- package/zcode/AGENTS.md +10 -10
|
@@ -0,0 +1,1377 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""H-4 止血守卫 · **全 md_cg 域**:共享索引 `nodes` 字典上的迭代一律先取快照
|
|
3
|
+
(N138 / FI-M04 跨模块止血面)。
|
|
4
|
+
|
|
5
|
+
来源:设计者裁定的 H-4(a) 契约——**只做止血**:凡从 `cg.index` 取回 `nodes`
|
|
6
|
+
后在**取用前先取快照**(`list(nodes.items())` / `list(nodes.values())` /
|
|
7
|
+
`list(nodes)`),使并发写不再触发
|
|
8
|
+
`RuntimeError: dictionary changed size during iteration`,且**既有判据与结果
|
|
9
|
+
逐位不变**(只换取用方式,不改语义)。实例级读写锁 / 写路径消息化 / 线程模型
|
|
10
|
+
重构属设计级,本批刻意不做;`SustainLoop._lock` 覆盖范围**未扩**。
|
|
11
|
+
|
|
12
|
+
缺陷(N138,docs/eval/缺陷挖掘_自主迭代_v16.md:90;跨模块面见
|
|
13
|
+
docs/eval/优化第三批_…_v1.0.md §6.3/§7.3):默认部署下 MDCG_SUSTAIN 开,
|
|
14
|
+
sustain 的后台巡检线程与请求线程**共享同一个 MdCG 实例与同一个
|
|
15
|
+
`index['nodes']` 字典**,而引擎全域无线程锁;前台 add/flush 在迭代进行中改
|
|
16
|
+
该字典即抛 RuntimeError,异常被 `SustainLoop._run` 的 `except Exception: pass`
|
|
17
|
+
吞掉 ⇒ **巡检静默丢一轮**(不是进程崩溃)。
|
|
18
|
+
|
|
19
|
+
**v2(本轮)修了什么**:v1 只把 `sustain.py` 四处迭代点包了快照——而
|
|
20
|
+
`diagnose()` **默认参数**的路径跨模块:`sustain.py:579 → evolution_candidates
|
|
21
|
+
→ weights.recalc → weights.coverage_index` 的裸 `items()`,且 `diagnose`
|
|
22
|
+
无条件调 `refindex.check_refs`(`for nid in nodes` 裸键迭代)——同一共享 dict,
|
|
23
|
+
止血面切窄了。v2 把快照面扩到**全 md_cg 域**的每一处共享 dict 迭代点
|
|
24
|
+
(含 `weights.py:408/409/425/483`、`refindex.py:672/815/878/966` 等 **80 处**:
|
|
25
|
+
并把验收判据切在**目标**上(见 G4)。
|
|
26
|
+
|
|
27
|
+
契约四项 ↔ 断言组:
|
|
28
|
+
① 迭代点全部取快照(结构判据 · **全域**)—— G1(AST 判据:全 md_cg(含测试)
|
|
29
|
+
模块零裸迭代;含判据自证 G1b:对同一份源码反包裹后必须原样报出)
|
|
30
|
+
② 并发写不再可触发 RuntimeError(**目标级**判据)—— G4:
|
|
31
|
+
G4a 确定性(注入绕开 sustain 自身快照面的写:只对 `edges` 触发 → 全默认
|
|
32
|
+
参数 `diagnose(cg)` 不得抛;**回缺陷副本必须抛** ⇒ 判据有判别力);
|
|
33
|
+
G4b 真线程 · 默认 switchinterval;G4c 真线程 · 1e-6(放大窗口)。
|
|
34
|
+
③ 判定结果逐位不变(独立 oracle 对照,不靠推理)—— G3(现实现 vs 全域
|
|
35
|
+
「回缺陷副本」的 30+ 条只读路径输出逐字节相同)+ G3b(**手算** oracle)
|
|
36
|
+
④ 记账面 `_lock` 覆盖范围**未扩**(本批不改并发契约)—— G0
|
|
37
|
+
|
|
38
|
+
运行:
|
|
39
|
+
python -X utf8 -m md_cg.test_h4_sustain_snapshot # 正向
|
|
40
|
+
python -X utf8 -m md_cg.test_h4_sustain_snapshot --mutate # 定点变异自证
|
|
41
|
+
python -X utf8 -m md_cg.test_h4_sustain_snapshot --mutate --list # 只列变异表
|
|
42
|
+
|
|
43
|
+
退出码(fail-closed):0 = 全绿;1 = 有断言失败 / 变异未按预期转红;
|
|
44
|
+
2 = **ANCHOR-MISS**(变异锚点在当前源码里找不到——实现改了却没同步本表,
|
|
45
|
+
本表就是判据,锚点漂移必须硬失败,不得静默跳过)。
|
|
46
|
+
|
|
47
|
+
**基线纪律**:本守卫**不以 git HEAD 为基线源**——所有「修复前」形态都由在当前
|
|
48
|
+
工作区源码上做**定点文本变异**(`_MUTATIONS`)或**全域反包裹**
|
|
49
|
+
(`_revert_source`)得到,基线随代码一起走。
|
|
50
|
+
|
|
51
|
+
**时序敏感项的处理**:G4b / G4c(真线程)与 G5(真实并发对撞)**不参与定点
|
|
52
|
+
变异计数**——它们只在正向跑里作防线在位证据(对照组观测记 NOTE);定点变异
|
|
53
|
+
自证由确定性判据 G1/G2/G4a 承担。
|
|
54
|
+
|
|
55
|
+
**扫描面**:`md_cg/**.py` **全部模块**(含 `test_*.py`——早先把测试面切在判据外
|
|
56
|
+
=给判据留豁免口;实测测试面另有 25 处裸站点,已一并取快照,见 `_domain_files`)。
|
|
57
|
+
"""
|
|
58
|
+
from __future__ import annotations
|
|
59
|
+
|
|
60
|
+
import ast
|
|
61
|
+
import contextlib
|
|
62
|
+
import io
|
|
63
|
+
import json
|
|
64
|
+
import os
|
|
65
|
+
import re
|
|
66
|
+
import shutil
|
|
67
|
+
import sys
|
|
68
|
+
import tempfile
|
|
69
|
+
import threading
|
|
70
|
+
import time
|
|
71
|
+
import types
|
|
72
|
+
|
|
73
|
+
# ── 隔离前置:必须在 import md_cg 子模块**之前**(tokens/theory 等在模块级把
|
|
74
|
+
# aux_root() 冻成常量)——否则会指到真实 ~/.mdcg 并在那里建密钥/令牌库。
|
|
75
|
+
# 与 md_cg/test_c3_transient_read_negative.py 同款前置(同一纪律)。
|
|
76
|
+
# 实验根一律在系统临时目录;不触任何在役数据根。 ──
|
|
77
|
+
_TMP = tempfile.mkdtemp(prefix="h4sus_")
|
|
78
|
+
os.environ["MDCG_AUX_ROOT"] = os.path.join(_TMP, "auxroot")
|
|
79
|
+
os.environ["MDCG_MASTER_KEY"] = "ab" * 32 # 哑主密钥(绝不触真实密钥面)
|
|
80
|
+
os.environ["MDCG_TOKEN_FILE"] = os.path.join(_TMP, "tokens.json")
|
|
81
|
+
os.environ["MDCG_ROOT"] = os.path.join(_TMP, "cgroot")
|
|
82
|
+
os.environ["MDCG_SUSTAIN_DIR"] = os.path.join(_TMP, "sustain")
|
|
83
|
+
os.makedirs(os.environ["MDCG_AUX_ROOT"], exist_ok=True)
|
|
84
|
+
for _k in ("MDCG_TOKEN", "MDCG_TENANT", "MDCG_CLEARANCE", "MDCG_SESSION",
|
|
85
|
+
"DSH_SESSION_ID", "MDCG_LEGACY_ENV_AUTH", "MDCG_LEGACY_ENV_ADMIN",
|
|
86
|
+
"MDCG_CAN_ADMIN", "MDCG_CAN_WRITE", "MDCG_STATE_ROOT",
|
|
87
|
+
"MDCG_DATA_ROOT", "MDCG_TENANT_REGISTRY", "MDCG_VERIFIER_MODULES"):
|
|
88
|
+
os.environ.pop(_k, None)
|
|
89
|
+
|
|
90
|
+
from . import sustain as _sustain # noqa: E402
|
|
91
|
+
from . import weights as _weights # noqa: E402
|
|
92
|
+
from . import refindex as _refindex # noqa: E402
|
|
93
|
+
from . import provenance as _provenance # noqa: E402
|
|
94
|
+
from . import trust as _trust # noqa: E402
|
|
95
|
+
from . import self_state as _self_state # noqa: E402
|
|
96
|
+
from . import identity as _identity # noqa: E402
|
|
97
|
+
from . import insight as _insight # noqa: E402
|
|
98
|
+
from . import subgraph as _subgraph # noqa: E402
|
|
99
|
+
from . import reach as _reach # noqa: E402
|
|
100
|
+
from . import export as _export # noqa: E402
|
|
101
|
+
from . import forgetting as _forgetting # noqa: E402
|
|
102
|
+
from . import consistency as _consistency # noqa: E402
|
|
103
|
+
from . import chain as _chain # noqa: E402
|
|
104
|
+
from . import protect as _protect # noqa: E402
|
|
105
|
+
from . import logref as _logref # noqa: E402
|
|
106
|
+
from . import linkref as _linkref # noqa: E402
|
|
107
|
+
from . import refine as _refine # noqa: E402
|
|
108
|
+
from . import branches as _branches # noqa: E402
|
|
109
|
+
from . import consolidate as _consolidate # noqa: E402
|
|
110
|
+
from . import reconcile as _reconcile # noqa: E402
|
|
111
|
+
from .mdcos import MdCGSecure # noqa: E402
|
|
112
|
+
from .security import Principal # noqa: E402
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
116
|
+
except AttributeError:
|
|
117
|
+
pass
|
|
118
|
+
|
|
119
|
+
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
120
|
+
_REPO = os.path.dirname(_HERE)
|
|
121
|
+
_RELPATH = os.path.join("md_cg", "sustain.py")
|
|
122
|
+
_MD_DIR = os.path.join(_REPO, "md_cg")
|
|
123
|
+
|
|
124
|
+
# 被测模块的**可替换引用**:变异模式会把其中一项换成 exec 出来的变异副本,
|
|
125
|
+
# 断言组一律经本表取模块(不直接闭包捕获原模块),否则变异对断言不可见。
|
|
126
|
+
MOD = {"sustain": _sustain, "weights": _weights, "refindex": _refindex,
|
|
127
|
+
"provenance": _provenance, "trust": _trust, "self_state": _self_state,
|
|
128
|
+
"identity": _identity, "insight": _insight, "subgraph": _subgraph,
|
|
129
|
+
"reach": _reach, "export": _export, "forgetting": _forgetting,
|
|
130
|
+
"consistency": _consistency, "chain": _chain, "protect": _protect,
|
|
131
|
+
"logref": _logref, "linkref": _linkref, "refine": _refine,
|
|
132
|
+
"branches": _branches, "consolidate": _consolidate,
|
|
133
|
+
"reconcile": _reconcile}
|
|
134
|
+
|
|
135
|
+
_PASS: list = []
|
|
136
|
+
_FAIL: list = []
|
|
137
|
+
_SKIP: list = []
|
|
138
|
+
_NOTES: list = []
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def ok(cond, msg, extra=""):
|
|
142
|
+
(_PASS if cond else _FAIL).append(msg)
|
|
143
|
+
print((" PASS " if cond else " FAIL ") + msg
|
|
144
|
+
+ ((" ← " + str(extra)) if (extra and not cond) else ""))
|
|
145
|
+
return bool(cond)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def note(msg):
|
|
149
|
+
_NOTES.append(msg)
|
|
150
|
+
print(" NOTE " + msg)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def skip(msg):
|
|
154
|
+
_SKIP.append(msg)
|
|
155
|
+
print(" SKIP " + msg)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# ═══════════════════════════════════════════════ 判据辅助(1):全域扫描器
|
|
159
|
+
#
|
|
160
|
+
# 判据:**共享索引 nodes 字典**上的迭代必须先把迭代源包成副本
|
|
161
|
+
# (`list(` / `tuple(` / `sorted(` / `set(` / `dict(`)。AST 判定链见各函数注释;
|
|
162
|
+
# 用 AST 而非文本扫描的原因:站点跨模块、形态多样(`for` / 推导式 /
|
|
163
|
+
# `sum(genexp)` / 别名变量 / 参数透传),文本匹配会漏。
|
|
164
|
+
|
|
165
|
+
_COPY_CALLS = {"list", "tuple", "sorted", "set", "frozenset", "dict"}
|
|
166
|
+
_CONSUME_CALLS = {"sum", "any", "all", "max", "min", "len", "Counter"}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _is_shared_expr(node) -> bool:
|
|
170
|
+
"""该表达式是否「从 (cg|self).index 取回 nodes」——字面量容器不算。"""
|
|
171
|
+
if isinstance(node, (ast.Dict, ast.List, ast.Set, ast.Tuple, ast.Constant)):
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
def _is_index_ref(n):
|
|
175
|
+
if isinstance(n, ast.Attribute) and n.attr == "index":
|
|
176
|
+
return True
|
|
177
|
+
if isinstance(n, ast.Name) and n.id == "index":
|
|
178
|
+
return True
|
|
179
|
+
if isinstance(n, ast.Call) and isinstance(n.func, ast.Name) \
|
|
180
|
+
and n.func.id == "getattr" and len(n.args) >= 2:
|
|
181
|
+
a = n.args[1]
|
|
182
|
+
return isinstance(a, ast.Constant) and a.value == "index"
|
|
183
|
+
return False
|
|
184
|
+
|
|
185
|
+
if not any(_is_index_ref(n) for n in ast.walk(node)):
|
|
186
|
+
return False
|
|
187
|
+
try:
|
|
188
|
+
src = ast.unparse(node)
|
|
189
|
+
except Exception: # noqa: BLE001
|
|
190
|
+
return False
|
|
191
|
+
return bool(re.search(r"""\[\s*['"]nodes['"]\s*\]""", src)
|
|
192
|
+
or re.search(r"""\.get\(\s*['"]nodes['"]""", src))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _call_name(node):
|
|
196
|
+
if isinstance(node, ast.Call):
|
|
197
|
+
f = node.func
|
|
198
|
+
if isinstance(f, ast.Name):
|
|
199
|
+
return f.id
|
|
200
|
+
if isinstance(f, ast.Attribute):
|
|
201
|
+
return f.attr
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _is_copying(node) -> bool:
|
|
206
|
+
"""该表达式的**结果**是否为独立副本(不再是共享 dict 本体)。"""
|
|
207
|
+
if isinstance(node, ast.Subscript): # list(x)[:n]
|
|
208
|
+
return _is_copying(node.value)
|
|
209
|
+
if isinstance(node, ast.BoolOp): # a or {}
|
|
210
|
+
return all(_is_copying(v) for v in node.values)
|
|
211
|
+
nm = _call_name(node)
|
|
212
|
+
if nm in _COPY_CALLS:
|
|
213
|
+
return True
|
|
214
|
+
if nm in _CONSUME_CALLS and len(node.args) == 1:
|
|
215
|
+
return True
|
|
216
|
+
return False
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _base_access(expr):
|
|
220
|
+
"""下探复制/切片外壳后取 (基表达式, 访问名) —— 判据必须同时认出
|
|
221
|
+
「已快照」与「回缺陷」两态,否则变异自证无从谈起。"""
|
|
222
|
+
e = expr
|
|
223
|
+
while True:
|
|
224
|
+
if isinstance(e, ast.Subscript):
|
|
225
|
+
e = e.value
|
|
226
|
+
continue
|
|
227
|
+
if isinstance(e, ast.Call) and isinstance(e.func, ast.Name) \
|
|
228
|
+
and e.func.id in _COPY_CALLS and len(e.args) == 1:
|
|
229
|
+
e = e.args[0]
|
|
230
|
+
continue
|
|
231
|
+
break
|
|
232
|
+
if isinstance(e, ast.Call) and isinstance(e.func, ast.Attribute) \
|
|
233
|
+
and e.func.attr in ("items", "values", "keys"):
|
|
234
|
+
return e.func.value, e.func.attr
|
|
235
|
+
if isinstance(e, ast.Attribute) and e.attr in ("items", "values", "keys"):
|
|
236
|
+
return e.value, e.attr
|
|
237
|
+
return e, None
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _mutates_self(name, body) -> bool:
|
|
241
|
+
"""循环体是否改写被迭代的那个 dict(改写自身迭代源=单线程也非法)。"""
|
|
242
|
+
for n in body:
|
|
243
|
+
for x in ast.walk(n):
|
|
244
|
+
if isinstance(x, ast.Assign):
|
|
245
|
+
for t in x.targets:
|
|
246
|
+
if isinstance(t, ast.Subscript) and \
|
|
247
|
+
isinstance(t.value, ast.Name) and t.value.id == name:
|
|
248
|
+
return True
|
|
249
|
+
if isinstance(x, ast.AugAssign) and \
|
|
250
|
+
isinstance(x.target, ast.Subscript) and \
|
|
251
|
+
isinstance(x.target.value, ast.Name) and \
|
|
252
|
+
x.target.value.id == name:
|
|
253
|
+
return True
|
|
254
|
+
if isinstance(x, ast.Delete):
|
|
255
|
+
for t in x.targets:
|
|
256
|
+
if isinstance(t, ast.Subscript) and \
|
|
257
|
+
isinstance(t.value, ast.Name) and t.value.id == name:
|
|
258
|
+
return True
|
|
259
|
+
if isinstance(x, ast.Call) and isinstance(x.func, ast.Attribute) \
|
|
260
|
+
and x.func.attr in ("pop", "update", "setdefault", "clear",
|
|
261
|
+
"popitem") \
|
|
262
|
+
and isinstance(x.func.value, ast.Name) \
|
|
263
|
+
and x.func.value.id == name:
|
|
264
|
+
return True
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _iter_sites(tree):
|
|
269
|
+
"""返回 [(迭代源节点, 宿体)]:For / 推导式的迭代源。
|
|
270
|
+
|
|
271
|
+
**必须传同一棵 AST**(调用方只 parse 一次):另起 `ast.parse` 得到的节点与
|
|
272
|
+
父表不是同一身,`enclosing_fn` 认不出所在函数,判据会退回模块级作用域而误报
|
|
273
|
+
(本守卫曾因此把 `Ledger.record` 的 list 形参误判成共享 dict)。
|
|
274
|
+
"""
|
|
275
|
+
out = []
|
|
276
|
+
for n in ast.walk(tree):
|
|
277
|
+
if isinstance(n, (ast.For, ast.AsyncFor)):
|
|
278
|
+
out.append((n.iter, n.body))
|
|
279
|
+
elif isinstance(n, (ast.ListComp, ast.SetComp, ast.GeneratorExp,
|
|
280
|
+
ast.DictComp)):
|
|
281
|
+
for g in n.generators:
|
|
282
|
+
out.append((g.iter, []))
|
|
283
|
+
return out
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
#: 源码文本 → 扫描结果(见 scan_source 的缓存说明)
|
|
287
|
+
_SCAN_CACHE: dict = {}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def scan_source(src: str) -> list:
|
|
291
|
+
"""对单份源码报出**共享 dict 的迭代点**:[(行号, 文本, owner, safe, mutates)]。
|
|
292
|
+
|
|
293
|
+
owner ∈ {inline, alias:<名>, param:<形参>};safe=True 表示迭代源已取快照。
|
|
294
|
+
|
|
295
|
+
**按源码文本缓存**:全域扫描要过 196 个文件、定点变异模式要跑十几轮,不缓存
|
|
296
|
+
则同一份源码被反复 AST 分析(实测:g1 一轮 36s,是变异模式耗时主因)。命中时
|
|
297
|
+
连同源码本体一起比对确认,不做「哈希相同就算同源」的盲信。
|
|
298
|
+
"""
|
|
299
|
+
key = (len(src), hash(src))
|
|
300
|
+
hit = _SCAN_CACHE.get(key)
|
|
301
|
+
if hit is not None and hit[0] == src:
|
|
302
|
+
return hit[1]
|
|
303
|
+
result = _scan_source_uncached(src)
|
|
304
|
+
if len(_SCAN_CACHE) > 1024:
|
|
305
|
+
_SCAN_CACHE.clear()
|
|
306
|
+
_SCAN_CACHE[key] = (src, result)
|
|
307
|
+
return result
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _scan_source_uncached(src: str) -> list:
|
|
311
|
+
tree = ast.parse(src)
|
|
312
|
+
lines = src.splitlines()
|
|
313
|
+
parent = {}
|
|
314
|
+
for n in ast.walk(tree):
|
|
315
|
+
for c in ast.iter_child_nodes(n):
|
|
316
|
+
parent[c] = n
|
|
317
|
+
|
|
318
|
+
def enclosing_fn(n):
|
|
319
|
+
p = parent.get(n)
|
|
320
|
+
while p is not None:
|
|
321
|
+
if isinstance(p, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
322
|
+
return p
|
|
323
|
+
p = parent.get(p)
|
|
324
|
+
return None
|
|
325
|
+
|
|
326
|
+
scopes = [tree] + [n for n in ast.walk(tree)
|
|
327
|
+
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
|
|
328
|
+
|
|
329
|
+
def scope_aliases(scope, extra=()):
|
|
330
|
+
names = set(extra)
|
|
331
|
+
for st in scope.body:
|
|
332
|
+
for n in ast.walk(st):
|
|
333
|
+
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef)) \
|
|
334
|
+
and n is not scope:
|
|
335
|
+
continue
|
|
336
|
+
if isinstance(n, ast.Assign) and _is_shared_expr(n.value) \
|
|
337
|
+
and not _is_copying(n.value):
|
|
338
|
+
for t in n.targets:
|
|
339
|
+
if isinstance(t, ast.Name):
|
|
340
|
+
names.add(t.id)
|
|
341
|
+
if isinstance(n, ast.AnnAssign) and n.value is not None \
|
|
342
|
+
and _is_shared_expr(n.value) and not _is_copying(n.value) \
|
|
343
|
+
and isinstance(n.target, ast.Name):
|
|
344
|
+
names.add(n.target.id)
|
|
345
|
+
return names
|
|
346
|
+
|
|
347
|
+
fn_by_name = {}
|
|
348
|
+
for s in scopes[1:]:
|
|
349
|
+
fn_by_name.setdefault(s.name, []).append(s)
|
|
350
|
+
|
|
351
|
+
# 一层过程间:以共享实参调用某函数 ⇒ 该形参在该函数体内也是共享面
|
|
352
|
+
param_shared = {}
|
|
353
|
+
for call in [n for n in ast.walk(tree) if isinstance(n, ast.Call)]:
|
|
354
|
+
nm = _call_name(call)
|
|
355
|
+
if nm not in fn_by_name:
|
|
356
|
+
continue
|
|
357
|
+
csc = enclosing_fn(call) or tree
|
|
358
|
+
local = scope_aliases(csc)
|
|
359
|
+
for callee in fn_by_name[nm]:
|
|
360
|
+
args = [a.arg for a in callee.args.args]
|
|
361
|
+
for i, a in enumerate(call.args):
|
|
362
|
+
if i >= len(args):
|
|
363
|
+
break
|
|
364
|
+
if _is_shared_expr(a) or (isinstance(a, ast.Name)
|
|
365
|
+
and a.id in local):
|
|
366
|
+
param_shared.setdefault(id(callee), set()).add(args[i])
|
|
367
|
+
for kw in call.keywords:
|
|
368
|
+
if kw.arg and (_is_shared_expr(kw.value)
|
|
369
|
+
or (isinstance(kw.value, ast.Name)
|
|
370
|
+
and kw.value.id in local)):
|
|
371
|
+
param_shared.setdefault(id(callee), set()).add(kw.arg)
|
|
372
|
+
|
|
373
|
+
cache = {id(s): scope_aliases(s, param_shared.get(id(s), set()))
|
|
374
|
+
for s in scopes}
|
|
375
|
+
|
|
376
|
+
def alias_set_for(node):
|
|
377
|
+
s = enclosing_fn(node)
|
|
378
|
+
return cache[id(tree)] if s is None else cache[id(s)]
|
|
379
|
+
|
|
380
|
+
out = []
|
|
381
|
+
for expr, body in _iter_sites(tree):
|
|
382
|
+
base, _access = _base_access(expr)
|
|
383
|
+
owner = None
|
|
384
|
+
if isinstance(base, ast.Name):
|
|
385
|
+
if base.id in alias_set_for(expr):
|
|
386
|
+
owner = "alias:%s" % base.id
|
|
387
|
+
else:
|
|
388
|
+
fn = enclosing_fn(expr)
|
|
389
|
+
if fn is not None and base.id in param_shared.get(id(fn), ()):
|
|
390
|
+
owner = "param:%s" % base.id
|
|
391
|
+
if owner is None and _is_shared_expr(base):
|
|
392
|
+
owner = "inline"
|
|
393
|
+
if owner is None:
|
|
394
|
+
continue
|
|
395
|
+
out.append({"line": expr.lineno,
|
|
396
|
+
"text": lines[expr.lineno - 1].strip(),
|
|
397
|
+
"owner": owner,
|
|
398
|
+
"safe": _is_copying(expr),
|
|
399
|
+
"mutates": _mutates_self(base.id, body)
|
|
400
|
+
if isinstance(base, ast.Name) else False})
|
|
401
|
+
return out
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _domain_files() -> list:
|
|
405
|
+
"""扫描面:**md_cg/**.py 全部模块**(含测试,不再切掉测试面)。
|
|
406
|
+
|
|
407
|
+
早先版本把 `test_*.py` 排除在外(理由:夹具单线程自读自写),但那等于给
|
|
408
|
+
判据留了个自证豁免口——「全 md_cg 域再无裸迭代」这句话要么全真、要么不该
|
|
409
|
+
这么说。实测测试面另有 25 处裸站点(12 个测试文件,且**无**改写自身迭代源
|
|
410
|
+
的形态),已一并取快照后纳入本扫描面。
|
|
411
|
+
"""
|
|
412
|
+
out = []
|
|
413
|
+
for dirpath, dirnames, filenames in os.walk(_MD_DIR):
|
|
414
|
+
dirnames[:] = [d for d in dirnames if d != "__pycache__"]
|
|
415
|
+
for f in sorted(filenames):
|
|
416
|
+
if f.endswith(".py"):
|
|
417
|
+
out.append(os.path.join(dirpath, f))
|
|
418
|
+
return out
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def scan_domain(overrides: dict = None):
|
|
422
|
+
"""全 md_cg 域扫描:返回 (共享迭代点总数, 裸迭代点列表, 扫描文件数)。
|
|
423
|
+
|
|
424
|
+
overrides:{相对仓根路径: 源码} —— 变异模式下用**内存里的变异源码**替换盘上
|
|
425
|
+
文本(否则结构判据对变异不可见,判据就成了空转)。
|
|
426
|
+
"""
|
|
427
|
+
overrides = overrides or {}
|
|
428
|
+
total, bad, files = 0, [], 0
|
|
429
|
+
for path in _domain_files():
|
|
430
|
+
rel = os.path.relpath(path, _REPO).replace(os.sep, "/")
|
|
431
|
+
if rel in overrides:
|
|
432
|
+
src = overrides[rel]
|
|
433
|
+
else:
|
|
434
|
+
with open(path, encoding="utf-8") as f:
|
|
435
|
+
src = f.read()
|
|
436
|
+
try:
|
|
437
|
+
sites = scan_source(src)
|
|
438
|
+
except SyntaxError as e: # 变异源写坏 → 硬失败
|
|
439
|
+
bad.append({"path": rel, "line": 0,
|
|
440
|
+
"text": "扫描器无法解析:%s" % e, "owner": "-",
|
|
441
|
+
"safe": False, "mutates": False})
|
|
442
|
+
files += 1
|
|
443
|
+
continue
|
|
444
|
+
total += len(sites)
|
|
445
|
+
files += 1
|
|
446
|
+
for s in sites:
|
|
447
|
+
if not s["safe"] or s["mutates"]:
|
|
448
|
+
s = dict(s)
|
|
449
|
+
s["path"] = rel
|
|
450
|
+
bad.append(s)
|
|
451
|
+
return total, bad, files
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
# ═══════════════════════════════════════════════ 判据辅助(2):全域「回缺陷副本」
|
|
455
|
+
#
|
|
456
|
+
# `_revert_source` 把快照 `list(X)` 削成 `(X)`:括号留着(多行表达式的续行不能丢),
|
|
457
|
+
# 复制没了(括号不复制)⇒ 语义=裸迭代。用途有二:
|
|
458
|
+
# * G1b 判据自证:反包裹后扫描器必须**原样报出**这些站点(判据有判别力);
|
|
459
|
+
# * G3/G4a 的对照组:全域「回缺陷副本」(这批未取快照时的样子)。
|
|
460
|
+
|
|
461
|
+
def _revert_source(src: str):
|
|
462
|
+
"""返回 (反包裹后的源码, 处数)。"""
|
|
463
|
+
tree = ast.parse(src)
|
|
464
|
+
lines = src.splitlines(keepends=True)
|
|
465
|
+
starts = [0]
|
|
466
|
+
for ln in lines:
|
|
467
|
+
starts.append(starts[-1] + len(ln))
|
|
468
|
+
|
|
469
|
+
def off(lineno, col):
|
|
470
|
+
return starts[lineno - 1] + col
|
|
471
|
+
|
|
472
|
+
bad_lines = set()
|
|
473
|
+
for s in scan_source(src):
|
|
474
|
+
bad_lines.add(s["line"])
|
|
475
|
+
spans = []
|
|
476
|
+
for expr, _body in _iter_sites(tree):
|
|
477
|
+
if isinstance(expr, ast.Call) and isinstance(expr.func, ast.Name) \
|
|
478
|
+
and expr.func.id == "list" and len(expr.args) == 1 \
|
|
479
|
+
and expr.lineno in bad_lines:
|
|
480
|
+
spans.append(off(expr.lineno, expr.col_offset))
|
|
481
|
+
body = src
|
|
482
|
+
for s in sorted(set(spans), reverse=True):
|
|
483
|
+
assert body[s:s + 5] == "list(", "反包裹锚点错位:%d" % s
|
|
484
|
+
body = body[:s] + "(" + body[s + 5:]
|
|
485
|
+
return body, len(set(spans))
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def _control_sources():
|
|
489
|
+
"""全域回缺陷源码 + 处数:{相对路径: 源码}。"""
|
|
490
|
+
out, n = {}, 0
|
|
491
|
+
for path in _domain_files():
|
|
492
|
+
with open(path, encoding="utf-8") as f:
|
|
493
|
+
src = f.read()
|
|
494
|
+
rev, k = _revert_source(src)
|
|
495
|
+
if k:
|
|
496
|
+
n += k
|
|
497
|
+
out[os.path.relpath(path, _REPO).replace(os.sep, "/")] = rev
|
|
498
|
+
return out, n
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _build_control_modules(sources: dict):
|
|
502
|
+
"""把回缺陷源码 exec 成独立模块(**只用于判据对照**,不落盘、不改工作区)。
|
|
503
|
+
|
|
504
|
+
模块名与真实模块同名但**不进 sys.modules**;对照期间经 `_Swap` 换入包属性
|
|
505
|
+
(`from . import X` 走包属性 ⇒ 换入即生效),退出即还原。
|
|
506
|
+
"""
|
|
507
|
+
mods = {}
|
|
508
|
+
for rel, src in sources.items():
|
|
509
|
+
name = rel[:-3].replace("/", ".")
|
|
510
|
+
path = os.path.join(_REPO, rel.replace("/", os.sep))
|
|
511
|
+
mod = types.ModuleType(name)
|
|
512
|
+
mod.__package__ = name.rsplit(".", 1)[0]
|
|
513
|
+
mod.__file__ = path
|
|
514
|
+
exec(compile(src, path, "exec"), mod.__dict__) # noqa: S102
|
|
515
|
+
mods[name] = mod
|
|
516
|
+
return mods
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
class _Swap:
|
|
520
|
+
"""对照期把回缺陷模块换进 sys.modules 与所属包属性(退出必还原)。
|
|
521
|
+
|
|
522
|
+
`from . import X` 走**包属性**,故两处都要换:只换 sys.modules 的话,
|
|
523
|
+
已导入过的兄弟模块仍会拿到旧对象(判据就成了空转的对照组)。
|
|
524
|
+
"""
|
|
525
|
+
|
|
526
|
+
def __init__(self, mods):
|
|
527
|
+
self.mods = mods
|
|
528
|
+
self.saved = {}
|
|
529
|
+
|
|
530
|
+
@staticmethod
|
|
531
|
+
def _pkg_of(name):
|
|
532
|
+
import importlib
|
|
533
|
+
parent = ".".join(name.split(".")[:-1])
|
|
534
|
+
pkg = sys.modules.get(parent)
|
|
535
|
+
if pkg is None:
|
|
536
|
+
pkg = importlib.import_module(parent)
|
|
537
|
+
return pkg
|
|
538
|
+
|
|
539
|
+
def __enter__(self):
|
|
540
|
+
for name, mod in self.mods.items():
|
|
541
|
+
self.saved[name] = sys.modules.get(name)
|
|
542
|
+
sys.modules[name] = mod
|
|
543
|
+
setattr(self._pkg_of(name), name.split(".")[-1], mod)
|
|
544
|
+
return self
|
|
545
|
+
|
|
546
|
+
def __exit__(self, *exc):
|
|
547
|
+
for name, old in self.saved.items():
|
|
548
|
+
if old is None:
|
|
549
|
+
sys.modules.pop(name, None)
|
|
550
|
+
else:
|
|
551
|
+
sys.modules[name] = old
|
|
552
|
+
setattr(self._pkg_of(name), name.split(".")[-1], old)
|
|
553
|
+
return False
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
# ═══════════════════════════════════════════════ 判据辅助(3):并发写注入
|
|
557
|
+
|
|
558
|
+
class _OneShotMutatingEntry(dict):
|
|
559
|
+
"""一条「取值即触发并发写」的索引条目(确定性并发注入)。
|
|
560
|
+
|
|
561
|
+
首次 `.get(trigger)` 时向**所属索引字典**添加一个影子键(模拟前台 add/flush
|
|
562
|
+
改共享索引),随后按普通 dict 语义取值。用真实 dict 的真实迭代语义:**裸迭代**
|
|
563
|
+
在该条目之后继续 `__next__` 必抛 `RuntimeError('dictionary changed size
|
|
564
|
+
during iteration')`;**快照迭代**(`list(nodes.items())`)在拷贝完成后才执行
|
|
565
|
+
`.get`,故不受影响。
|
|
566
|
+
|
|
567
|
+
`trigger=None` ⇒ 任何键都触发;`trigger="edges"` ⇒ 只在该键被读时触发
|
|
568
|
+
(用于**绕开 sustain 自身快照面**的目标级注入:写发生在 weights 的迭代里)。
|
|
569
|
+
"""
|
|
570
|
+
|
|
571
|
+
def __init__(self, data: dict, owner: dict, probe_key: str, trigger=None):
|
|
572
|
+
super().__init__(data)
|
|
573
|
+
self._owner = owner
|
|
574
|
+
self._probe_key = probe_key
|
|
575
|
+
self._trigger = trigger
|
|
576
|
+
self._fired = False
|
|
577
|
+
|
|
578
|
+
def get(self, key, default=None):
|
|
579
|
+
if not self._fired and (self._trigger is None or key == self._trigger):
|
|
580
|
+
self._fired = True
|
|
581
|
+
self._owner[self._probe_key] = dict(self)
|
|
582
|
+
return super().get(key, default)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
class _CgGetFires:
|
|
586
|
+
"""宿主代理:首次 `cg.get(nid)` 触发并发写(键迭代站点无 `.get` 中的读可取)。"""
|
|
587
|
+
|
|
588
|
+
def __init__(self, cg, owner: dict, probe_key: str):
|
|
589
|
+
self._cg = cg
|
|
590
|
+
self._owner = owner
|
|
591
|
+
self._probe_key = probe_key
|
|
592
|
+
self._fired = False
|
|
593
|
+
|
|
594
|
+
def __getattr__(self, name):
|
|
595
|
+
return getattr(self._cg, name)
|
|
596
|
+
|
|
597
|
+
def get(self, nid, *a, **kw):
|
|
598
|
+
if not self._fired:
|
|
599
|
+
self._fired = True
|
|
600
|
+
self._owner[self._probe_key] = {"path": "", "layer": "knowledge",
|
|
601
|
+
"tags": ["code"], "content_hash": "x"}
|
|
602
|
+
return self._cg.get(nid, *a, **kw)
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
class _FakeCg:
|
|
606
|
+
"""`_locked_nodes` 的最小可驱动宿主(只用到 index 与 crypto_status)。"""
|
|
607
|
+
|
|
608
|
+
def __init__(self, root, nodes):
|
|
609
|
+
self.root = root
|
|
610
|
+
self.index = {"nodes": nodes}
|
|
611
|
+
|
|
612
|
+
def crypto_status(self):
|
|
613
|
+
return {"unlocked": False}
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _with_injection(cg, trigger=None, probe="h4sus_shadow"):
|
|
617
|
+
"""在 cg.index['nodes'] 里注入「并发写」条目;返回还原函数。"""
|
|
618
|
+
owner = (getattr(cg, "index", {}) or {}).get("nodes")
|
|
619
|
+
saved = dict(owner)
|
|
620
|
+
victim = sorted(owner)[-1]
|
|
621
|
+
owner[victim] = _OneShotMutatingEntry(dict(owner[victim]), owner, probe,
|
|
622
|
+
trigger=trigger)
|
|
623
|
+
|
|
624
|
+
def restore():
|
|
625
|
+
owner.clear()
|
|
626
|
+
owner.update(saved)
|
|
627
|
+
return restore
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
# ═══════════════════════════════════════════════ fixture
|
|
631
|
+
|
|
632
|
+
def _princ(role="designer"):
|
|
633
|
+
from . import tokens
|
|
634
|
+
spec = tokens.role_spec(role)
|
|
635
|
+
return Principal(tenant="default", actor="h4sustain-" + role,
|
|
636
|
+
clearance="secret", can_write=bool(spec["can_write"]),
|
|
637
|
+
can_admin=True, role=role, session="sess_h4sustain")
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _new_cg(tag: str, n: int = 0):
|
|
641
|
+
root = tempfile.mkdtemp(prefix="h4sus_cg_%s_" % tag, dir=_TMP)
|
|
642
|
+
cg = MdCGSecure(root, principal=_princ())
|
|
643
|
+
for i in range(n):
|
|
644
|
+
cg.add("h4sus_%s_%02d" % (tag, i), "正文 %s %d h4susuniq" % (tag, i),
|
|
645
|
+
layer="knowledge")
|
|
646
|
+
if n:
|
|
647
|
+
cg.flush()
|
|
648
|
+
return cg
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def _index_nodes(cg):
|
|
652
|
+
return (getattr(cg, "index", {}) or {}).get("nodes") or {}
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def _strip_time(obj):
|
|
656
|
+
"""去掉时间戳键(唯一非确定量)后的规范化形态(用于逐位对照)。"""
|
|
657
|
+
_time_keys = ("t", "last_t", "updated_at", "created_at", "first_t",
|
|
658
|
+
"started_ts", "heartbeat_ts", "finished_ts", "duration_s",
|
|
659
|
+
"elapsed_s", "elapsed_ms", "checked_at", "time_range",
|
|
660
|
+
"duration_ms", "took_ms", "access_count", "generated_at",
|
|
661
|
+
"mtime", "size", "ts", "now", "batch")
|
|
662
|
+
if isinstance(obj, dict):
|
|
663
|
+
return {k: _strip_time(v) for k, v in obj.items()
|
|
664
|
+
if k not in _time_keys}
|
|
665
|
+
if isinstance(obj, (list, tuple)):
|
|
666
|
+
return [_strip_time(v) for v in obj]
|
|
667
|
+
if isinstance(obj, (set, frozenset)):
|
|
668
|
+
return sorted(_strip_time(v) for v in obj)
|
|
669
|
+
if isinstance(obj, bool):
|
|
670
|
+
return obj
|
|
671
|
+
if isinstance(obj, float):
|
|
672
|
+
return "<TS>" if obj > 1e9 else round(obj, 6)
|
|
673
|
+
if isinstance(obj, int):
|
|
674
|
+
return "<TS>" if obj > 10 ** 9 else obj
|
|
675
|
+
if isinstance(obj, str):
|
|
676
|
+
s = obj.replace(_TMP, "<TMP>")
|
|
677
|
+
s = re.sub(r"\d{8}-\d{6}[-\w]*", "<STAMP>", s)
|
|
678
|
+
# 消息里内嵌的 set repr(元素序随字符串哈希随机)→ 排序后比
|
|
679
|
+
return re.sub(r"\{('(?:[^']*)'(?:, )?)+\}",
|
|
680
|
+
lambda m: "{" + ", ".join(
|
|
681
|
+
sorted(m.group(0)[1:-1].split(", "))) + "}", s)
|
|
682
|
+
return obj
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _j(obj) -> str:
|
|
686
|
+
return json.dumps(_strip_time(obj), ensure_ascii=False, sort_keys=True,
|
|
687
|
+
default=str)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _build_fixture(tag: str, n: int = 3):
|
|
691
|
+
"""哑实验库(系统临时目录):n 个 knowledge 节点,删掉其中 1 个的文件造孤儿。"""
|
|
692
|
+
cg = _new_cg(tag, n)
|
|
693
|
+
ids = sorted(_index_nodes(cg))
|
|
694
|
+
orphan = ids[-1]
|
|
695
|
+
p = os.path.join(cg.root, _index_nodes(cg)[orphan]["path"])
|
|
696
|
+
os.remove(p) # 索引在、文件没了 → index_orphan
|
|
697
|
+
return cg, ids, orphan
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
# ═══════════════════════════════════════════════ 断言组
|
|
701
|
+
|
|
702
|
+
def g0():
|
|
703
|
+
"""记账面未扩:`with self._lock:` 仍恰 4 处(本批不改并发契约)。"""
|
|
704
|
+
with open(os.path.join(_REPO, _RELPATH), encoding="utf-8") as f:
|
|
705
|
+
src = f.read()
|
|
706
|
+
n = src.count("with self._lock:")
|
|
707
|
+
ok(n == 4, "G0 记账面未扩:`with self._lock:` 仍恰 4 处(tidys/evolves/"
|
|
708
|
+
"scrubs/heals)", f"实测 {n} 处")
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def _override_for(relpath_hint: str = None):
|
|
712
|
+
"""变异模式下把内存里的变异源码交给判据(否则判据看不见变异=空转)。
|
|
713
|
+
|
|
714
|
+
返回 {相对路径: 源码};无变异上下文时为 {}(判据读盘上源码)。
|
|
715
|
+
"""
|
|
716
|
+
rel = relpath_hint or _MUT_CTX.get("rel")
|
|
717
|
+
if not rel:
|
|
718
|
+
return {}
|
|
719
|
+
key = os.path.basename(rel)[:-3]
|
|
720
|
+
src = getattr(MOD.get(key), "__h4_src__", None)
|
|
721
|
+
return {rel.replace(os.sep, "/"): src} if src is not None else {}
|
|
722
|
+
|
|
723
|
+
|
|
724
|
+
def g1():
|
|
725
|
+
"""结构判据(全域):md_cg 全部模块(含测试)零裸迭代点。"""
|
|
726
|
+
overrides = _override_for()
|
|
727
|
+
total, bad, files = scan_domain(overrides)
|
|
728
|
+
ok(total >= 80, "G1a 扫描器覆盖面(共享 nodes 迭代点总数 ≥ 80,"
|
|
729
|
+
"防判据空转——扫描器若瞎了总数会掉)",
|
|
730
|
+
f"实测 total={total} files={files}")
|
|
731
|
+
ok(not bad, "G1a 全 md_cg 域(含测试模块)零裸迭代点:凡从 cg.index 取回的 "
|
|
732
|
+
"nodes 均先取快照",
|
|
733
|
+
f"裸迭代点 {len(bad)} 处:{[(b['path'], b['line']) for b in bad[:5]]}")
|
|
734
|
+
for b in bad[:5]:
|
|
735
|
+
print(" " + "%s:%s %s" % (b["path"], b["line"], b["text"][:90]))
|
|
736
|
+
|
|
737
|
+
# G1b 判据自证(判别力):把当前源码**反包裹**后,扫描器必须报出同批站点
|
|
738
|
+
src_all = {}
|
|
739
|
+
for path in _domain_files():
|
|
740
|
+
with open(path, encoding="utf-8") as f:
|
|
741
|
+
src_all[os.path.relpath(path, _REPO).replace(os.sep, "/")] = f.read()
|
|
742
|
+
rev, n_rev = {}, 0
|
|
743
|
+
for rel, src in src_all.items():
|
|
744
|
+
r, k = _revert_source(src)
|
|
745
|
+
if k:
|
|
746
|
+
rev[rel] = r
|
|
747
|
+
n_rev += k
|
|
748
|
+
_t2, bad2, _f2 = scan_domain(rev)
|
|
749
|
+
# 与**盘上现役源码**比(不是变异源码):G1b 判的是「扫描器认不认得出反包裹
|
|
750
|
+
# 形态」这件事本身;把它与变异源码比会让每一次定点变异都连带打红本项,
|
|
751
|
+
# 红项数就掺进了不相干的因素(判据要各司其职)。
|
|
752
|
+
_t3, bad_disk, _f3 = scan_domain()
|
|
753
|
+
ok(n_rev >= 70 and len(bad2) == n_rev + len(bad_disk),
|
|
754
|
+
"G1b 判据自证:反包裹后扫描器原样报出全部站点(判别力在位)",
|
|
755
|
+
f"反包裹 {n_rev} 处 + 现役裸 {len(bad_disk)} 处 vs 报出 {len(bad2)} 处"
|
|
756
|
+
f"——不等即判据漏了站点")
|
|
757
|
+
ok(all(b["mutates"] is False for b in bad2),
|
|
758
|
+
"G1b 反包裹副本里无「改写自身迭代源」的站点(快照不掩盖单线程非法)",
|
|
759
|
+
f"{[b['text'] for b in bad2 if b['mutates']][:2]}")
|
|
760
|
+
|
|
761
|
+
# G1c 站点在位(防「删掉迭代」冒充「已快照」):契约点名的八个站点逐字
|
|
762
|
+
tokens = [
|
|
763
|
+
("md_cg/sustain.py", "for e in list(nodes.values())"),
|
|
764
|
+
("md_cg/sustain.py", "for nid, e in list(nodes.items()):"),
|
|
765
|
+
("md_cg/sustain.py",
|
|
766
|
+
"for k, v in list(nodes.items()) if v.get(\"layer\") == layer"),
|
|
767
|
+
("md_cg/sustain.py", "[nid for nid, e in list(nodes.items())"),
|
|
768
|
+
("md_cg/weights.py", "for nid, e in list(nodes.items()):"),
|
|
769
|
+
("md_cg/weights.py", "indeg = {nid: 0 for nid in list(nodes)}"),
|
|
770
|
+
("md_cg/weights.py", "ids = [nid for nid, e in list(nodes.items())"),
|
|
771
|
+
("md_cg/refindex.py", "todo = [nid for nid in list(nodes)"),
|
|
772
|
+
("md_cg/refindex.py", "for nid in list(nodes):"),
|
|
773
|
+
]
|
|
774
|
+
for rel, token in tokens:
|
|
775
|
+
src = overrides.get(rel)
|
|
776
|
+
if src is None:
|
|
777
|
+
with open(os.path.join(_REPO, rel.replace("/", os.sep)),
|
|
778
|
+
encoding="utf-8") as f:
|
|
779
|
+
src = f.read()
|
|
780
|
+
ok(token in src, "G1c 快照站点在位:%s ← %s" % (rel, token[:44]))
|
|
781
|
+
|
|
782
|
+
|
|
783
|
+
def g2_behavior():
|
|
784
|
+
"""行为判据(确定性注入):取值/迭代期间改索引 → 迭代点不得抛 RuntimeError。"""
|
|
785
|
+
# ① _ccg_backlog(sustain.py:422 面)
|
|
786
|
+
d = {"n1": {"verification_basis": None, "has_neg_conditions": False}}
|
|
787
|
+
d["n2"] = _OneShotMutatingEntry({"verification_basis": None,
|
|
788
|
+
"has_neg_conditions": False}, d, "shadow1")
|
|
789
|
+
try:
|
|
790
|
+
cb = MOD["sustain"]._ccg_backlog(d, 5)
|
|
791
|
+
ok(cb["n"] == 2 and cb["no_basis"] == 2 and cb["no_neg"] == 2,
|
|
792
|
+
"G2a _ccg_backlog 在并发写索引下不崩溃且计数正确", str(cb))
|
|
793
|
+
except RuntimeError as e:
|
|
794
|
+
ok(False, "G2a _ccg_backlog 在并发写索引下不崩溃且计数正确", repr(e))
|
|
795
|
+
|
|
796
|
+
# ② evolution_candidates 的 layer 过滤(sustain.py:449 面)
|
|
797
|
+
cg = _new_cg("g2b", 2)
|
|
798
|
+
restore = _with_injection(cg)
|
|
799
|
+
try:
|
|
800
|
+
ev = MOD["sustain"].evolution_candidates(cg, layer="knowledge", top=3)
|
|
801
|
+
ok(ev["ok"] is True,
|
|
802
|
+
"G2b evolution_candidates(layer=…) 在并发写索引下不崩溃",
|
|
803
|
+
json.dumps(ev, ensure_ascii=False)[:200])
|
|
804
|
+
except RuntimeError as e:
|
|
805
|
+
ok(False, "G2b evolution_candidates(layer=…) 在并发写索引下不崩溃",
|
|
806
|
+
repr(e))
|
|
807
|
+
finally:
|
|
808
|
+
restore()
|
|
809
|
+
|
|
810
|
+
# ③ diagnose 的孤儿扫描(sustain.py:485 面)
|
|
811
|
+
cg = _new_cg("g2c", 2)
|
|
812
|
+
restore = _with_injection(cg)
|
|
813
|
+
try:
|
|
814
|
+
rep = MOD["sustain"].diagnose(cg, name="h4_g2c", check_evolution=False)
|
|
815
|
+
ok("index_orphan" in {i["code"] for i in rep["issues"]}
|
|
816
|
+
or rep["stats"]["nodes_indexed"] >= 2,
|
|
817
|
+
"G2c diagnose 孤儿扫描在并发写索引下不崩溃", _j(rep)[:200])
|
|
818
|
+
except RuntimeError as e:
|
|
819
|
+
ok(False, "G2c diagnose 孤儿扫描在并发写索引下不崩溃", repr(e))
|
|
820
|
+
finally:
|
|
821
|
+
restore()
|
|
822
|
+
|
|
823
|
+
# ④ _locked_nodes(sustain.py:397 面)
|
|
824
|
+
owner = {"x": {"sensitivity": "internal"}}
|
|
825
|
+
owner["y"] = _OneShotMutatingEntry({"sensitivity": "internal"}, owner,
|
|
826
|
+
"shadow_locked")
|
|
827
|
+
try:
|
|
828
|
+
fake = _FakeCg(tempfile.mkdtemp(prefix="h4sus_fake_", dir=_TMP), owner)
|
|
829
|
+
n = MOD["sustain"]._locked_nodes(fake)
|
|
830
|
+
ok(n == 0, "G2d _locked_nodes 在并发写索引下不崩溃(内部级不计密文)",
|
|
831
|
+
f"locked={n}")
|
|
832
|
+
except RuntimeError as e:
|
|
833
|
+
ok(False, "G2d _locked_nodes 在并发写索引下不崩溃(内部级不计密文)",
|
|
834
|
+
repr(e))
|
|
835
|
+
|
|
836
|
+
# ⑤ weights.coverage_index(weights.py:408/409 面——v2 扩面站点)
|
|
837
|
+
cg = _new_cg("g2e", 2)
|
|
838
|
+
restore = _with_injection(cg)
|
|
839
|
+
try:
|
|
840
|
+
indeg = MOD["weights"].coverage_index(cg)
|
|
841
|
+
ok(isinstance(indeg, dict) and len(indeg) >= 2,
|
|
842
|
+
"G2e weights.coverage_index 在并发写索引下不崩溃且入度表完整",
|
|
843
|
+
str(indeg)[:120])
|
|
844
|
+
except RuntimeError as e:
|
|
845
|
+
ok(False, "G2e weights.coverage_index 在并发写索引下不崩溃且入度表完整",
|
|
846
|
+
repr(e))
|
|
847
|
+
finally:
|
|
848
|
+
restore()
|
|
849
|
+
|
|
850
|
+
# ⑥ weights.redundancy_map(weights.py:425 面)
|
|
851
|
+
cg = _new_cg("g2f", 2)
|
|
852
|
+
restore = _with_injection(cg)
|
|
853
|
+
try:
|
|
854
|
+
red = MOD["weights"].redundancy_map(cg)
|
|
855
|
+
ok(isinstance(red, dict) and len(red) >= 2,
|
|
856
|
+
"G2f weights.redundancy_map 在并发写索引下不崩溃且覆盖全节点",
|
|
857
|
+
str(red)[:120])
|
|
858
|
+
except RuntimeError as e:
|
|
859
|
+
ok(False, "G2f weights.redundancy_map 在并发写索引下不崩溃且覆盖全节点",
|
|
860
|
+
repr(e))
|
|
861
|
+
finally:
|
|
862
|
+
restore()
|
|
863
|
+
|
|
864
|
+
# ⑦ weights.recalc(weights.py:483 面)
|
|
865
|
+
cg = _new_cg("g2g", 2)
|
|
866
|
+
restore = _with_injection(cg)
|
|
867
|
+
try:
|
|
868
|
+
rep = MOD["weights"].recalc(cg, apply=False)
|
|
869
|
+
ok(rep.get("nodes_scanned", 0) >= 2,
|
|
870
|
+
"G2g weights.recalc 在并发写索引下不崩溃且扫全节点",
|
|
871
|
+
json.dumps(rep, ensure_ascii=False)[:160])
|
|
872
|
+
except RuntimeError as e:
|
|
873
|
+
ok(False, "G2g weights.recalc 在并发写索引下不崩溃且扫全节点", repr(e))
|
|
874
|
+
finally:
|
|
875
|
+
restore()
|
|
876
|
+
|
|
877
|
+
# ⑧ refindex.check_refs 的键迭代(refindex.py:672 面)
|
|
878
|
+
cg = _new_cg("g2h", 2)
|
|
879
|
+
restore = _with_injection(cg)
|
|
880
|
+
try:
|
|
881
|
+
refs = MOD["refindex"].check_refs(
|
|
882
|
+
cg, ledger=MOD["refindex"].Ledger(cg.root), only_tagged=True)
|
|
883
|
+
ok(isinstance(refs, dict),
|
|
884
|
+
"G2h refindex.check_refs 键迭代在并发写索引下不崩溃",
|
|
885
|
+
str(refs)[:160])
|
|
886
|
+
except RuntimeError as e:
|
|
887
|
+
ok(False, "G2h refindex.check_refs 键迭代在并发写索引下不崩溃", repr(e))
|
|
888
|
+
finally:
|
|
889
|
+
restore()
|
|
890
|
+
|
|
891
|
+
# ⑨ refindex.rebuild 的键迭代(refindex.py:966 面,无 .get 可取 → 宿主代理触发)
|
|
892
|
+
cg = _new_cg("g2i", 1)
|
|
893
|
+
owner = _index_nodes(cg)
|
|
894
|
+
saved = dict(owner)
|
|
895
|
+
try:
|
|
896
|
+
proxy = _CgGetFires(cg, owner, "h4sus_g2i_shadow")
|
|
897
|
+
try:
|
|
898
|
+
out = MOD["refindex"].rebuild(proxy, max_files=1, max_items=1)
|
|
899
|
+
ok(isinstance(out, dict),
|
|
900
|
+
"G2i refindex.rebuild 键迭代在并发写索引下不崩溃",
|
|
901
|
+
str(out)[:160])
|
|
902
|
+
except RuntimeError as e:
|
|
903
|
+
ok(False, "G2i refindex.rebuild 键迭代在并发写索引下不崩溃", repr(e))
|
|
904
|
+
except Exception as e: # noqa: BLE001
|
|
905
|
+
note("G2i rebuild 抛出非 RuntimeError 异常(与裸迭代无关,仅记录):"
|
|
906
|
+
"%s: %s" % (type(e).__name__, e))
|
|
907
|
+
finally:
|
|
908
|
+
owner.clear()
|
|
909
|
+
owner.update(saved)
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
# ⑩ refindex.prune_orphans 的裸 items()(refindex.py:815 面)
|
|
913
|
+
cg = _new_cg("g2j", 2)
|
|
914
|
+
restore = _with_injection(cg)
|
|
915
|
+
try:
|
|
916
|
+
out = MOD["refindex"].prune_orphans(
|
|
917
|
+
cg, kind="code_ref", root=cg.root,
|
|
918
|
+
items=[{"path": "h4sus_absent.py", "name": "f",
|
|
919
|
+
"lineno": 1, "end": 2, "hash": "x"}],
|
|
920
|
+
dry_run=True)
|
|
921
|
+
ok(isinstance(out, dict),
|
|
922
|
+
"G2j refindex.prune_orphans 裸 items() 在并发写索引下不崩溃",
|
|
923
|
+
str(out)[:160])
|
|
924
|
+
except RuntimeError as e:
|
|
925
|
+
ok(False, "G2j refindex.prune_orphans 裸 items() 在并发写索引下不崩溃",
|
|
926
|
+
repr(e))
|
|
927
|
+
finally:
|
|
928
|
+
restore()
|
|
929
|
+
|
|
930
|
+
# ⑪ refindex.prune_dangling 的裸 items()(refindex.py:878 面)
|
|
931
|
+
cg = _new_cg("g2k", 2)
|
|
932
|
+
restore = _with_injection(cg)
|
|
933
|
+
try:
|
|
934
|
+
# only_roots 非空 ⇒ 跳过前面的幽灵条目循环(:867 是**既有**快照站点,
|
|
935
|
+
# 会先把一次性注入用掉),判据才能命中 :878 这一段
|
|
936
|
+
out = MOD["refindex"].prune_dangling(cg, only_roots=[cg.root],
|
|
937
|
+
dry_run=True)
|
|
938
|
+
ok(isinstance(out, dict),
|
|
939
|
+
"G2k refindex.prune_dangling 裸 items() 在并发写索引下不崩溃",
|
|
940
|
+
str(out)[:160])
|
|
941
|
+
except RuntimeError as e:
|
|
942
|
+
ok(False, "G2k refindex.prune_dangling 裸 items() 在并发写索引下不崩溃",
|
|
943
|
+
repr(e))
|
|
944
|
+
finally:
|
|
945
|
+
restore()
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
def g3_oracle():
|
|
949
|
+
"""逐位不变:现实现 vs 全域「回缺陷副本」,**同一 fixture** 上逐字节相同。"""
|
|
950
|
+
ctrl, n_rev = _ctrl_table()
|
|
951
|
+
cg, _ids, _orphan = _build_fixture("g3", 4)
|
|
952
|
+
keys = sorted(_index_nodes(cg))
|
|
953
|
+
rows = [
|
|
954
|
+
("weights.coverage_index", lambda m: m["weights"].coverage_index(cg)),
|
|
955
|
+
("weights.redundancy_map",
|
|
956
|
+
lambda m: m["weights"].redundancy_map(cg, layer="knowledge")),
|
|
957
|
+
("weights.recalc", lambda m: m["weights"].recalc(cg, apply=False,
|
|
958
|
+
limit=3)),
|
|
959
|
+
("weights.node_importance",
|
|
960
|
+
lambda m: m["weights"].node_importance(cg, keys[0])),
|
|
961
|
+
("refindex.check_refs",
|
|
962
|
+
lambda m: m["refindex"].check_refs(
|
|
963
|
+
cg, ledger=m["refindex"].Ledger(cg.root))),
|
|
964
|
+
("refindex.check_refs:tagged",
|
|
965
|
+
lambda m: m["refindex"].check_refs(
|
|
966
|
+
cg, ledger=m["refindex"].Ledger(cg.root), only_tagged=True)),
|
|
967
|
+
("refindex.prune_dangling",
|
|
968
|
+
lambda m: m["refindex"].prune_dangling(cg, dry_run=True)),
|
|
969
|
+
("sustain.diagnose", lambda m: m["sustain"].diagnose(cg, name="h4_g3")),
|
|
970
|
+
("sustain.diagnose:noevolve",
|
|
971
|
+
lambda m: m["sustain"].diagnose(cg, name="h4_g3",
|
|
972
|
+
check_evolution=False)),
|
|
973
|
+
("sustain.evolution_candidates",
|
|
974
|
+
lambda m: m["sustain"].evolution_candidates(cg, top=4)),
|
|
975
|
+
("sustain.evolution_candidates:layer",
|
|
976
|
+
lambda m: m["sustain"].evolution_candidates(cg, layer="knowledge",
|
|
977
|
+
top=4)),
|
|
978
|
+
("sustain.heal:dry",
|
|
979
|
+
lambda m: m["sustain"].heal(cg, name="h4_g3", dry_run=True)),
|
|
980
|
+
("sustain._ccg_backlog",
|
|
981
|
+
lambda m: m["sustain"]._ccg_backlog(_index_nodes(cg), 5)),
|
|
982
|
+
("sustain._locked_nodes", lambda m: m["sustain"]._locked_nodes(cg)),
|
|
983
|
+
("provenance.index_edges", lambda m: m["provenance"].index_edges(cg)),
|
|
984
|
+
("trust.dependents_index", lambda m: m["trust"].dependents_index(cg)),
|
|
985
|
+
("trust.propagate", lambda m: m["trust"].propagate(cg)),
|
|
986
|
+
("self_state.snapshot", lambda m: m["self_state"].snapshot(cg)),
|
|
987
|
+
("self_state.audit", lambda m: m["self_state"].audit(cg)),
|
|
988
|
+
("self_state.relations", lambda m: m["self_state"].relations(cg)),
|
|
989
|
+
("self_state._relation_counts",
|
|
990
|
+
lambda m: m["self_state"]._relation_counts(cg, None)),
|
|
991
|
+
("identity.positions", lambda m: m["identity"].positions(cg, limit=5)),
|
|
992
|
+
("identity.profile", lambda m: m["identity"].profile(cg, keys[0])),
|
|
993
|
+
("insight._events", lambda m: m["insight"]._events(cg)),
|
|
994
|
+
("insight.outlook", lambda m: m["insight"].outlook(cg)),
|
|
995
|
+
("subgraph.separation_pairs",
|
|
996
|
+
lambda m: m["subgraph"].separation_pairs(cg, limit=5)),
|
|
997
|
+
("reach._hash_complete", lambda m: m["reach"]._hash_complete(cg)),
|
|
998
|
+
("export.export_stat", lambda m: m["export"].export_stat(cg)),
|
|
999
|
+
("forgetting.longterm_assess",
|
|
1000
|
+
lambda m: m["forgetting"].longterm_assess(cg)),
|
|
1001
|
+
("consistency.check",
|
|
1002
|
+
lambda m: m["consistency"].check(cg, "正文 h4susuniq probe")),
|
|
1003
|
+
("chain.adjacency", lambda m: m["chain"].adjacency(cg)),
|
|
1004
|
+
("protect.stats", lambda m: m["protect"].stats(cg)),
|
|
1005
|
+
("logref.session_src_state",
|
|
1006
|
+
lambda m: m["logref"].session_src_state(cg)),
|
|
1007
|
+
("linkref.known_ids", lambda m: m["linkref"].known_ids(cg)),
|
|
1008
|
+
("refine._pool", lambda m: m["refine"]._pool(cg, "h4sus")),
|
|
1009
|
+
("branches.list_branches",
|
|
1010
|
+
lambda m: m["branches"].list_branches(cg)),
|
|
1011
|
+
("consolidate.induce_memories",
|
|
1012
|
+
lambda m: m["consolidate"].induce_memories(cg, limit=5)),
|
|
1013
|
+
("reconcile.reconcile_state",
|
|
1014
|
+
lambda m: m["reconcile"].reconcile_state(cg, apply=False)),
|
|
1015
|
+
]
|
|
1016
|
+
live_rows = {}
|
|
1017
|
+
for tag, fn in rows:
|
|
1018
|
+
try:
|
|
1019
|
+
live_rows[tag] = _j(fn(MOD))
|
|
1020
|
+
except Exception as e: # noqa: BLE001
|
|
1021
|
+
live_rows[tag] = "__exc__" + type(e).__name__ + ":" + str(e)
|
|
1022
|
+
with _Swap(_CTRL_MODS):
|
|
1023
|
+
ctrl_rows = {}
|
|
1024
|
+
for tag, fn in rows:
|
|
1025
|
+
try:
|
|
1026
|
+
ctrl_rows[tag] = _j(fn(ctrl))
|
|
1027
|
+
except Exception as e: # noqa: BLE001
|
|
1028
|
+
ctrl_rows[tag] = "__exc__" + type(e).__name__ + ":" + str(e)
|
|
1029
|
+
diffs = [t for t, _ in rows if live_rows[t] != ctrl_rows[t]]
|
|
1030
|
+
ok(not diffs, "G3 逐位不变:%d 条只读路径在快照版与全域回缺陷副本上输出"
|
|
1031
|
+
"逐字节相同" % len(rows),
|
|
1032
|
+
f"差异 {diffs[:3]}:" + "; ".join(
|
|
1033
|
+
"%s live=%s ctrl=%s" % (t, live_rows[t][:120], ctrl_rows[t][:120])
|
|
1034
|
+
for t in diffs[:1]))
|
|
1035
|
+
ok(n_rev >= 70, "G3 回缺陷副本覆盖面(≥70 处反包裹,防对照空转)",
|
|
1036
|
+
f"实测 {n_rev} 处")
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
# 对照组模块表:与 MOD 同名的回缺陷模块(进程内只建一次:盘上源码在本次运行里不变)
|
|
1040
|
+
_CTRL: dict = {}
|
|
1041
|
+
_CTRL_MODS: dict = {}
|
|
1042
|
+
_CTRL_N = [0]
|
|
1043
|
+
# 变异模式下的上下文:{"rel": "md_cg/xxx.py"} —— 结构判据据此读**内存里的变异源码**
|
|
1044
|
+
_MUT_CTX: dict = {}
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def _ctrl_table():
|
|
1048
|
+
"""返回 (回缺陷模块表, 反包裹处数);首次调用时构建。"""
|
|
1049
|
+
if not _CTRL_MODS:
|
|
1050
|
+
sources, n = _control_sources()
|
|
1051
|
+
mods = _build_control_modules(sources)
|
|
1052
|
+
_CTRL_MODS.update(mods)
|
|
1053
|
+
_CTRL_N[0] = n
|
|
1054
|
+
for key in MOD:
|
|
1055
|
+
full = "md_cg." + key
|
|
1056
|
+
_CTRL[key] = mods.get(full, MOD[key]) # 无站点的模块 → 用现役模块
|
|
1057
|
+
return _CTRL, _CTRL_N[0]
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def g3b():
|
|
1061
|
+
"""手算 oracle:fixture 的已知事实与 diagnose 的报数逐项相等(不靠自比)。"""
|
|
1062
|
+
cg, _ids, orphan = _build_fixture("g3b", 3)
|
|
1063
|
+
rep = MOD["sustain"].diagnose(cg, name="h4_g3b")
|
|
1064
|
+
codes = {i["code"] for i in rep["issues"]}
|
|
1065
|
+
ok(rep["stats"]["nodes_indexed"] == 3 and rep["stats"]["nodes_on_disk"] == 2,
|
|
1066
|
+
"G3b 手算:索引 3 / 磁盘 2(删掉 1 个文件)",
|
|
1067
|
+
json.dumps(rep["stats"], ensure_ascii=False))
|
|
1068
|
+
ok("index_drift" in codes and "index_orphan" in codes,
|
|
1069
|
+
"G3b 手算:漂移与孤儿两条告警均在位", str(sorted(codes)))
|
|
1070
|
+
org = next(i for i in rep["issues"] if i["code"] == "index_orphan")
|
|
1071
|
+
ok(org.get("sample") == [orphan], "G3b 手算:孤儿样本恰为被删的那个节点",
|
|
1072
|
+
f"{org.get('sample')} != [{orphan}]")
|
|
1073
|
+
ev = rep["evolve"]["ccg_backlog"]
|
|
1074
|
+
ok(ev["n"] == 3 and ev["no_basis"] == 3 and ev["no_neg"] == 3,
|
|
1075
|
+
"G3b 手算:3 个节点全无验证基底/负条件 → 候补 3",
|
|
1076
|
+
json.dumps(ev, ensure_ascii=False))
|
|
1077
|
+
ok(rep["evolve"]["importance_drift"]["scanned"] >= 3,
|
|
1078
|
+
"G3b 手算:重要性巡检覆盖全部节点", str(rep["evolve"]["importance_drift"]))
|
|
1079
|
+
# 手算跨模块面:weights 的入度表对 h4 环状引用逐项对账
|
|
1080
|
+
indeg = MOD["weights"].coverage_index(cg)
|
|
1081
|
+
ok(set(indeg) == set(_index_nodes(cg)) and all(v == 0 for v in indeg.values()),
|
|
1082
|
+
"G3b 手算:weights.coverage_index 覆盖全节点且无出边时入度全 0",
|
|
1083
|
+
str(indeg)[:120])
|
|
1084
|
+
|
|
1085
|
+
|
|
1086
|
+
def g4a_target_deterministic():
|
|
1087
|
+
"""**目标级判据(确定性)**:注入绕开 sustain 自身快照面的并发写后,
|
|
1088
|
+
以**全默认参数**调 `diagnose(cg)` 不得抛 RuntimeError;**回缺陷副本必须抛**
|
|
1089
|
+
(同一判据上的一红一绿 ⇒ 判据有判别力)。
|
|
1090
|
+
|
|
1091
|
+
注入是**一次性**的(第一次读 `edges` 时改索引)⇒ 现实现与对照组必须各用
|
|
1092
|
+
一个**同形新夹具**:同一个夹具上跑两遍的话,第一遍已经把注入用掉了,
|
|
1093
|
+
对照组的绿是假绿(判据自己踩过的坑,注释在此留痕)。
|
|
1094
|
+
"""
|
|
1095
|
+
def _run(mods_ctx, mod, tag):
|
|
1096
|
+
cg, _ids, _orphan = _build_fixture("g4a" + tag, 4)
|
|
1097
|
+
restore = _with_injection(cg, trigger="edges")
|
|
1098
|
+
try:
|
|
1099
|
+
with mods_ctx:
|
|
1100
|
+
rep = mod["sustain"].diagnose(cg) # 全默认参数
|
|
1101
|
+
return ("green", rep.get("stats", {}).get("nodes_indexed"))
|
|
1102
|
+
except RuntimeError as e:
|
|
1103
|
+
import traceback
|
|
1104
|
+
tb = [ln.strip() for ln in traceback.format_exc().splitlines()
|
|
1105
|
+
if "md_cg" in ln]
|
|
1106
|
+
return ("RED", "%s: %s | %s" % (type(e).__name__, e,
|
|
1107
|
+
tb[-1] if tb else "?"))
|
|
1108
|
+
finally:
|
|
1109
|
+
restore()
|
|
1110
|
+
|
|
1111
|
+
_ctrl_table() # 首次构建(幂等)
|
|
1112
|
+
live = _run(contextlib.nullcontext(), MOD, "l")
|
|
1113
|
+
ok(live[0] == "green",
|
|
1114
|
+
"G4a 目标级(确定性):全默认参数 diagnose 在并发写下不抛 RuntimeError",
|
|
1115
|
+
str(live))
|
|
1116
|
+
ctrl = _run(_Swap(_CTRL_MODS), _CTRL, "c")
|
|
1117
|
+
ok(ctrl[0] == "RED",
|
|
1118
|
+
"G4a 判据有判别力:同一注入下**回缺陷副本**必须抛 RuntimeError",
|
|
1119
|
+
str(ctrl))
|
|
1120
|
+
if ctrl[0] == "RED":
|
|
1121
|
+
note("G4a 对照组崩溃栈顶:%s" % ctrl[1])
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
def g4bc_threads():
|
|
1125
|
+
"""真线程形态(默认 switchinterval / 1e-6):全默认参数 diagnose 与前台写
|
|
1126
|
+
对撞,零 RuntimeError。时序敏感,不参与定点变异计数(见文件头)。"""
|
|
1127
|
+
for tag, switch in (("G4b", None), ("G4c", 1e-6)):
|
|
1128
|
+
cg, _ids, _orphan = _build_fixture("h4%s" % tag.lower(), 60)
|
|
1129
|
+
errors = []
|
|
1130
|
+
stop = threading.Event()
|
|
1131
|
+
|
|
1132
|
+
def _inspector():
|
|
1133
|
+
while not stop.is_set():
|
|
1134
|
+
try:
|
|
1135
|
+
MOD["sustain"].diagnose(cg) # 全默认参数
|
|
1136
|
+
except Exception as e: # noqa: BLE001 —— 本组抓它
|
|
1137
|
+
errors.append("%s: %s" % (type(e).__name__, e))
|
|
1138
|
+
return
|
|
1139
|
+
|
|
1140
|
+
old_switch = sys.getswitchinterval()
|
|
1141
|
+
if switch is not None:
|
|
1142
|
+
sys.setswitchinterval(switch)
|
|
1143
|
+
th = threading.Thread(target=_inspector, daemon=True)
|
|
1144
|
+
th.start()
|
|
1145
|
+
i, deadline = 0, time.time() + 3.0
|
|
1146
|
+
try:
|
|
1147
|
+
while time.time() < deadline:
|
|
1148
|
+
cg.add("h4%s_w%04d" % (tag.lower(), i),
|
|
1149
|
+
"并发写入正文 %d" % i, layer="knowledge")
|
|
1150
|
+
i += 1
|
|
1151
|
+
if i % 5 == 0:
|
|
1152
|
+
cg.flush()
|
|
1153
|
+
finally:
|
|
1154
|
+
stop.set()
|
|
1155
|
+
th.join(timeout=15)
|
|
1156
|
+
sys.setswitchinterval(old_switch)
|
|
1157
|
+
ok(not errors,
|
|
1158
|
+
"%s 目标级(真线程%s):写 %d 条 + 全默认 diagnose 对撞零 RuntimeError"
|
|
1159
|
+
% (tag, ",switchinterval=1e-6" if switch else ",默认 switchinterval",
|
|
1160
|
+
i),
|
|
1161
|
+
f"首个异常={errors[:1]}")
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
def g5_concurrent():
|
|
1165
|
+
"""真实多线程对撞(sustain 巡检 vs 前台写,时序敏感加分项,不参与变异计数)。"""
|
|
1166
|
+
cg, _ids, _orphan = _build_fixture("g5", 2)
|
|
1167
|
+
errors = []
|
|
1168
|
+
stop = threading.Event()
|
|
1169
|
+
|
|
1170
|
+
def _healer():
|
|
1171
|
+
while not stop.is_set():
|
|
1172
|
+
try:
|
|
1173
|
+
MOD["sustain"].diagnose(cg, name="h4_g5")
|
|
1174
|
+
MOD["sustain"].heal(cg, name="h4_g5", dry_run=True)
|
|
1175
|
+
MOD["sustain"].evolution_candidates(cg)
|
|
1176
|
+
except Exception as e: # noqa: BLE001 —— 本组就是抓它
|
|
1177
|
+
errors.append("%s: %s" % (type(e).__name__, e))
|
|
1178
|
+
return
|
|
1179
|
+
|
|
1180
|
+
th = threading.Thread(target=_healer, daemon=True)
|
|
1181
|
+
th.start()
|
|
1182
|
+
i, deadline = 0, time.time() + 2.0
|
|
1183
|
+
try:
|
|
1184
|
+
while time.time() < deadline:
|
|
1185
|
+
cg.add("h4sus_g5_%d" % i, "并发写入正文 %d h4susuniq" % i,
|
|
1186
|
+
layer="knowledge")
|
|
1187
|
+
i += 1
|
|
1188
|
+
if i % 3 == 0:
|
|
1189
|
+
cg.flush()
|
|
1190
|
+
finally:
|
|
1191
|
+
stop.set()
|
|
1192
|
+
th.join(timeout=10)
|
|
1193
|
+
ok(not errors, f"G5 真实并发(写 {i} 条 + 巡检对撞)零异常",
|
|
1194
|
+
f"首个异常={errors[:1]}")
|
|
1195
|
+
|
|
1196
|
+
|
|
1197
|
+
# ------------------------------------------------------------------ 定点变异
|
|
1198
|
+
|
|
1199
|
+
# 「修复前」形态 = 把 `list(X)` 削成 `(X)`(括号不复制 ⇒ 等价裸迭代;括号留着
|
|
1200
|
+
# 是因为多行表达式的续行不能丢)。每条锚点都必须**唯一命中**目标站点:
|
|
1201
|
+
# 同一文件里重复出现的锚点由更长的上下文锚定(否则变异打到别的站点上,
|
|
1202
|
+
# 红项数就对不上,本表即判据)。
|
|
1203
|
+
_MUTATIONS = (
|
|
1204
|
+
# (名称, 相对路径, 锚点原文, 变异后, 期望红项数)
|
|
1205
|
+
("H-4a-1 回缺陷:_ccg_backlog 裸迭代(sustain.py:422)",
|
|
1206
|
+
"md_cg/sustain.py",
|
|
1207
|
+
"for nid, e in list(nodes.items()):", "for nid, e in (nodes.items()):", 3),
|
|
1208
|
+
("H-4a-2 回缺陷:evolution_candidates 的 layer 过滤裸迭代(:449)",
|
|
1209
|
+
"md_cg/sustain.py",
|
|
1210
|
+
"for k, v in list(nodes.items()) if v.get(\"layer\") == layer",
|
|
1211
|
+
"for k, v in (nodes.items()) if v.get(\"layer\") == layer", 3),
|
|
1212
|
+
("H-4a-3 回缺陷:diagnose 孤儿扫描裸迭代(:485)",
|
|
1213
|
+
"md_cg/sustain.py",
|
|
1214
|
+
"[nid for nid, e in list(nodes.items())",
|
|
1215
|
+
"[nid for nid, e in (nodes.items())", 3),
|
|
1216
|
+
("H-4a-4 回缺陷:_locked_nodes 裸迭代 values()(:397)",
|
|
1217
|
+
"md_cg/sustain.py",
|
|
1218
|
+
"for e in list(nodes.values())", "for e in (nodes.values())", 3),
|
|
1219
|
+
("H-4a-5 回缺陷:weights.coverage_index 键迭代(weights.py:408)",
|
|
1220
|
+
"md_cg/weights.py",
|
|
1221
|
+
"indeg = {nid: 0 for nid in list(nodes)}",
|
|
1222
|
+
"indeg = {nid: 0 for nid in (nodes)}", 2),
|
|
1223
|
+
("H-4a-6 回缺陷:weights.coverage_index 裸 items()(weights.py:409)",
|
|
1224
|
+
"md_cg/weights.py",
|
|
1225
|
+
"for nid, e in list(nodes.items()):\n for t in _targets(e):",
|
|
1226
|
+
"for nid, e in (nodes.items()):\n for t in _targets(e):", 3),
|
|
1227
|
+
("H-4a-7 回缺陷:weights.redundancy_map 裸 items()(weights.py:425)",
|
|
1228
|
+
"md_cg/weights.py",
|
|
1229
|
+
"seen, out = {}, {}\n for nid, e in list(nodes.items()):",
|
|
1230
|
+
"seen, out = {}, {}\n for nid, e in (nodes.items()):", 2),
|
|
1231
|
+
("H-4a-8 回缺陷:weights.recalc 裸推导(weights.py:483)",
|
|
1232
|
+
"md_cg/weights.py",
|
|
1233
|
+
"ids = [nid for nid, e in list(nodes.items())",
|
|
1234
|
+
"ids = [nid for nid, e in (nodes.items())", 2),
|
|
1235
|
+
("H-4a-9 回缺陷:refindex.check_refs 裸键迭代(refindex.py:672)",
|
|
1236
|
+
"md_cg/refindex.py",
|
|
1237
|
+
"todo = [nid for nid in list(nodes)", "todo = [nid for nid in (nodes)", 3),
|
|
1238
|
+
("H-4a-10 回缺陷:refindex.rebuild 裸键迭代(refindex.py:966)",
|
|
1239
|
+
"md_cg/refindex.py",
|
|
1240
|
+
"for nid in list(nodes):", "for nid in (nodes):", 3),
|
|
1241
|
+
("H-4a-11 回缺陷:refindex.prune_orphans 裸 items()(refindex.py:815)",
|
|
1242
|
+
"md_cg/refindex.py",
|
|
1243
|
+
"plan = []\n for nid, e in list(nodes.items()):",
|
|
1244
|
+
"plan = []\n for nid, e in (nodes.items()):", 2),
|
|
1245
|
+
("H-4a-12 回缺陷:refindex.prune_dangling 裸 items()(refindex.py:878)",
|
|
1246
|
+
"md_cg/refindex.py",
|
|
1247
|
+
"regenerable_skipped = []\n for nid, e in list(nodes.items()):",
|
|
1248
|
+
"regenerable_skipped = []\n for nid, e in (nodes.items()):", 2),
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
|
|
1252
|
+
def _exec_module(name: str, path: str, src: str):
|
|
1253
|
+
mod = types.ModuleType(name)
|
|
1254
|
+
mod.__package__ = name.rsplit(".", 1)[0]
|
|
1255
|
+
mod.__file__ = path
|
|
1256
|
+
mod.__h4_src__ = src # 结构判据取此源码(变异对判据可见)
|
|
1257
|
+
exec(compile(src, path, "exec"), mod.__dict__) # noqa: S102
|
|
1258
|
+
return mod
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
def _load_one(relpath: str, old: str, new: str):
|
|
1262
|
+
"""单点变异:只把该文件的 old 退回裸迭代(其余站点维持快照)。"""
|
|
1263
|
+
path = os.path.join(_REPO, relpath.replace("/", os.sep))
|
|
1264
|
+
with open(path, encoding="utf-8") as f:
|
|
1265
|
+
src = f.read()
|
|
1266
|
+
if old not in src:
|
|
1267
|
+
return None, src
|
|
1268
|
+
mut_src = src.replace(old, new, 1)
|
|
1269
|
+
name = "md_cg._h4sus_mut1_%s" % os.path.basename(path)[:-3]
|
|
1270
|
+
return _exec_module(name, path, mut_src), src
|
|
1271
|
+
|
|
1272
|
+
|
|
1273
|
+
def _run_groups(with_threads: bool = True) -> int:
|
|
1274
|
+
_PASS.clear()
|
|
1275
|
+
_FAIL.clear()
|
|
1276
|
+
_SKIP.clear()
|
|
1277
|
+
_NOTES.clear()
|
|
1278
|
+
for g in (g0, g1, g2_behavior, g3_oracle, g3b, g4a_target_deterministic):
|
|
1279
|
+
try:
|
|
1280
|
+
g()
|
|
1281
|
+
except Exception as exc: # noqa: BLE001
|
|
1282
|
+
import traceback
|
|
1283
|
+
ok(False, "断言组 %s 抛异常:%s" % (g.__name__, exc))
|
|
1284
|
+
print(traceback.format_exc()[-800:])
|
|
1285
|
+
if with_threads:
|
|
1286
|
+
for g in (g4bc_threads, g5_concurrent):
|
|
1287
|
+
try:
|
|
1288
|
+
g()
|
|
1289
|
+
except Exception as exc: # noqa: BLE001
|
|
1290
|
+
import traceback
|
|
1291
|
+
ok(False, "断言组 %s 抛异常:%s" % (g.__name__, exc))
|
|
1292
|
+
print(traceback.format_exc()[-800:])
|
|
1293
|
+
else:
|
|
1294
|
+
skip("G4b/G4c/G5 真线程组:定点变异模式下不计分(时序敏感,见文件头)")
|
|
1295
|
+
return len(_FAIL)
|
|
1296
|
+
|
|
1297
|
+
|
|
1298
|
+
def _mutate_mode(list_only: bool = False) -> int:
|
|
1299
|
+
print("!! 定点变异自证:逐条把快照取用改回裸迭代,套件必须按预期条数转红\n")
|
|
1300
|
+
if list_only:
|
|
1301
|
+
for name, rel, _old, _new, exp in _MUTATIONS:
|
|
1302
|
+
print(" %-56s expect_red=%d (%s)" % (name, exp, rel))
|
|
1303
|
+
return 0
|
|
1304
|
+
|
|
1305
|
+
anchor_miss, bad = [], []
|
|
1306
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
1307
|
+
clean = _run_groups(with_threads=True)
|
|
1308
|
+
print(" 未变异基线:失败=%d(必须为 0)" % clean)
|
|
1309
|
+
if clean:
|
|
1310
|
+
bad.append("未变异基线即失败")
|
|
1311
|
+
|
|
1312
|
+
for name, rel, old, new, expect in _MUTATIONS:
|
|
1313
|
+
mut, _src = _load_one(rel, old, new)
|
|
1314
|
+
if mut is None:
|
|
1315
|
+
print(" ANCHOR-MISS %s —— 锚点在 %s 当前源码里找不到"
|
|
1316
|
+
"(实现改了却没同步本表;基线不得静默漂移)" % (name, rel))
|
|
1317
|
+
anchor_miss.append(name)
|
|
1318
|
+
continue
|
|
1319
|
+
key = os.path.basename(rel)[:-3]
|
|
1320
|
+
live = MOD.get(key)
|
|
1321
|
+
MOD[key] = mut
|
|
1322
|
+
_MUT_CTX["rel"] = rel.replace(os.sep, "/")
|
|
1323
|
+
try:
|
|
1324
|
+
with contextlib.redirect_stdout(buf := io.StringIO()):
|
|
1325
|
+
reds = _run_groups(with_threads=False)
|
|
1326
|
+
detail = buf.getvalue()
|
|
1327
|
+
finally:
|
|
1328
|
+
_MUT_CTX.pop("rel", None)
|
|
1329
|
+
if live is not None:
|
|
1330
|
+
MOD[key] = live
|
|
1331
|
+
red_lines = [l for l in detail.splitlines() if l.strip().startswith("FAIL ")]
|
|
1332
|
+
verdict = "红" if reds else "**仍全绿 = 该判据空转**"
|
|
1333
|
+
mark = "OK " if reds == expect else "MISMATCH"
|
|
1334
|
+
print(" %s %-56s 红项=%d 预期=%d %s" % (mark, name, reds, expect,
|
|
1335
|
+
verdict))
|
|
1336
|
+
for l in red_lines:
|
|
1337
|
+
print(" " + l.strip()[5:])
|
|
1338
|
+
if reds != expect:
|
|
1339
|
+
bad.append("%s(红=%d 预期=%d)" % (name, reds, expect))
|
|
1340
|
+
|
|
1341
|
+
if anchor_miss:
|
|
1342
|
+
print("\nANCHOR-MISS:%s" % "、".join(anchor_miss))
|
|
1343
|
+
print("退出码 2(fail-closed):变异表锚点漂移即判失败,不得静默跳过")
|
|
1344
|
+
return 2
|
|
1345
|
+
print("\n定点变异自证:%s"
|
|
1346
|
+
% ("PASS(每处判据都被打红且恰好命中预期项数)" if not bad
|
|
1347
|
+
else "FAIL —— " + "、".join(bad)))
|
|
1348
|
+
return 0 if not bad else 1
|
|
1349
|
+
|
|
1350
|
+
|
|
1351
|
+
def main() -> int:
|
|
1352
|
+
if "--mutate" in sys.argv:
|
|
1353
|
+
try:
|
|
1354
|
+
return _mutate_mode("--list" in sys.argv)
|
|
1355
|
+
finally:
|
|
1356
|
+
_cleanup()
|
|
1357
|
+
try:
|
|
1358
|
+
n_fail = _run_groups(with_threads=True)
|
|
1359
|
+
print("\nH-4(a) 守卫(全 md_cg 域迭代点取快照):%d 通过,%d 失败,%d 跳过"
|
|
1360
|
+
% (len(_PASS), n_fail, len(_SKIP)))
|
|
1361
|
+
return 0 if not n_fail else 1
|
|
1362
|
+
finally:
|
|
1363
|
+
_cleanup()
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
def _cleanup():
|
|
1367
|
+
import gc
|
|
1368
|
+
for _ in range(6):
|
|
1369
|
+
shutil.rmtree(_TMP, ignore_errors=True)
|
|
1370
|
+
if not os.path.exists(_TMP):
|
|
1371
|
+
return
|
|
1372
|
+
gc.collect()
|
|
1373
|
+
time.sleep(0.2)
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
if __name__ == "__main__":
|
|
1377
|
+
sys.exit(main())
|