让 Codex 分摊 100 元:先写清总额守恒与余数归属
10000 个整数单位分给三组,单独四舍五入会少 1。先确定余数归属,再用整数算法与可复跑测试验收。
1. 100 元分给三组,少掉的 1 分归谁
把 100 元平均分给三个成本组,如果每组单独保留到分,会得到 33.33 元、33.33 元、33.33 元,合计只有 99.99 元。即使使用精确的十进制计算,“分别取整以后相加”也不保证等于原总额。缺少的是对不可再分单位的分配规则。
这篇教程用一个合成示例,把 100 元预先表示成 10000 个整数单位,给编码助手一份可以机械验收的任务:按权重分配、总额不丢失、并列余数有明确归属、输入调换顺序后结果不变。金额只是帮助理解的例子;函数只处理整数,不解析货币,也不连接账单或付款服务。
这里采用已有的最大余数法:先取整数商,再按余数从大到小补单位。余数相同,ID 字典序靠前者先补。这个约定方便复现,但是否适合你的业务,必须由业务规则决定。本文给出可本地复跑的参考实现和测试,不声称执行过真实 Codex 会话。
2. 先固定输入、输出和失败方式
接口是 allocate(total_minor, rows)。每一行是 (ID, 整数权重),返回按 ID 升序排列的 (ID, 分配的整数单位数) 列表。ID 原样比较,不去空格、不转换大小写。函数先验证全部输入,再计算结果;不修改输入对象,不返回半份结果。
total_minor的类型必须恰为int,范围为 0 到 10^12(含端点)。True和False均不接受。rows类型必须恰为内置list,含 1 到 1000 行。每行类型恰为内置list或tuple,长度为 2,依次为 ID 和权重。- ID 类型必须恰为内置
str,是匹配[A-Z][A-Z0-9_]{0,31}的 ASCII 字符串,必须唯一。 - 权重的类型必须恰为
int,范围为 0 到 10^9(含端点)。总权重必须为正,即使待分总额为 0。 - 零权重行始终得到 0。有效输入的总额为 0 时,每行均得到 0。
检查顺序固定为:总额 → 集合形状及行数 → 按输入顺序逐行检查形状、ID、重复 ID、权重 → 正总权重。失败时抛出 ValueError,消息为稳定错误码。单一规则的错误码测试一次只引入一个错误,以免把多错输入的优先级与某个验证规则混淆。
INVALID_TOTAL:总额类型或范围错误INVALID_ROWS:列表、行数或行形状不符合约定INVALID_ID:ID 类型或格式错误DUPLICATE_ID:ID 重复,不静默合并INVALID_WEIGHT:权重类型或范围错误ZERO_WEIGHT_SUM:所有权重均为 0
上述“恰为内置类型”也排除自定义子类;这是为了让教学接口边界明确,并非所有 Python API 都需要采用的习惯。
3. 用整数商和余数推导补单位规则
设总额为 T,总权重为 W,第 i 行权重为 w。对每行计算 q, r = divmod(T * w, W),即 T * w = q * W + r,且 0 ≤ r < W。q 是该行先拿到的整数份额;r 与精确份额的小数部分同序,因为所有行使用同一个分母 W。
先把所有 q 相加,再令 left = T - sum(q)。把各行等式相加可得 left = sum(r) / W,所以 left 是非负整数,并且小于行数。按 (-r, ID) 排序后,让前 left 行各加 1 就够了,不必循环撒很多轮,也不必用浮点数比较比例。
这个过程直接给出三项可测试性质:结果合计为 T;每行得到精确份额的向下取整或向上取整;零权重行不会获补。最后一项的原因是:若有 left 个单位待补,就必然有多于 left 个正余数,排序不会轮到余数为零的行。
例如 T=7,权重 A=5、B=3、C=2,W=10。整数商依次为 3、2、1,余数为 5、1、4;还剩 1 单位,补给 A,结果为 4、2、1。对于 10000 和三个相同权重,商都是 3333,余数都是 1,ID 升序决定 A 得到最后 1 单位。
这里要做两次不同的排序:分配补单位时按余数及 ID;返回时仅按 ID。后一项固定了输出次序,有效输入行重排以后,返回的有序二元组列表仍一致;使用相同的序列化方式时,字节结果也一致。
4. 大整数下,浮点数还可能认错余数先后
即使总额和权重都在边界以内,先除成浮点数再看小数也可能丢掉关键差别。令 T=999999999501,权重 A=999999999、B=1000000000,共同分母为 1999999999。
用整数除法得到 A 的商为 499999999500、余数为 999999999;B 的商为 500000000000、余数为 1000000000。剩余 1 单位应给 B,结果是 A=499999999500, B=500000000001。在本次测试的 Python 环境中,两个浮点份额的小数部分却都会变成 .5,误把 B 更大的余数看成并列。
完整测试保留这个精度陷阱。用整数余数比较,能够直接保留那 1 的差别。即便排序本身稳定,如果只按余数排序,并列时仍会沿用输入顺序;要让有效输入的重排不改变结果,必须显式把 ID 放进排序键。
5. 完整参考实现
将下面代码保存为 allocate.py。它只用 Python 标准库,运算路径中没有 float。特别注意 type(x) is int:Python 的布尔值属于整数类型的子类,单用 isinstance(x, int) 会把布尔值一起收进来。本案例有意采用严格的内置类型契约。
"""Exact largest-remainder allocation of a nonnegative integer total."""
import re
_ID = re.compile(r"[A-Z][A-Z0-9_]{0,31}", re.ASCII)
def allocate(total_minor, rows):
"""Return canonical (ID, amount) pairs; reject bad input with a stable code.
rows must be an exact built-in list of 1..1000 exact built-in list/tuple
pairs. IDs are exact built-in strings; totals and weights are exact ints.
No input is modified. See README.md for bounds and validation precedence.
"""
if type(total_minor) is not int or not 0 <= total_minor <= 10**12:
raise ValueError("INVALID_TOTAL")
if type(rows) is not list or not 1 <= len(rows) <= 1000:
raise ValueError("INVALID_ROWS")
seen = set()
validated = []
for row in rows:
if type(row) not in (list, tuple) or len(row) != 2:
raise ValueError("INVALID_ROWS")
identifier, weight = row
if type(identifier) is not str or _ID.fullmatch(identifier) is None:
raise ValueError("INVALID_ID")
if identifier in seen:
raise ValueError("DUPLICATE_ID")
seen.add(identifier)
if type(weight) is not int or not 0 <= weight <= 10**9:
raise ValueError("INVALID_WEIGHT")
validated.append((identifier, weight))
sum_weights = sum(weight for _, weight in validated)
if sum_weights == 0:
raise ValueError("ZERO_WEIGHT_SUM")
amounts = {}
remainders = []
for identifier, weight in validated:
quotient, remainder = divmod(total_minor * weight, sum_weights)
amounts[identifier] = quotient
remainders.append((identifier, remainder))
remaining = total_minor - sum(amounts.values())
ranked = sorted(remainders, key=lambda item: (-item[1], item[0]))
for identifier, _ in ranked[:remaining]:
amounts[identifier] += 1
return sorted(amounts.items())
6. 先看可以逐项核对的输入输出
以下调用使用 Python 的列表和元组表示法。输出统一按 ID 排列;示例包中的 JSON 测试数据使用数组表示这些二元组。
allocate(10000, [("C", 1), ("A", 1), ("B", 1)])
# [("A", 3334), ("B", 3333), ("C", 3333)]
allocate(7, [("A", 5), ("B", 3), ("C", 2)])
# [("A", 4), ("B", 2), ("C", 1)]
allocate(2, [("C", 1), ("B", 1), ("A", 1)])
# [("A", 1), ("B", 1), ("C", 0)]
allocate(5, [("B", 2), ("A", 0)])
# [("A", 0), ("B", 5)]
allocate(0, [("B", 2), ("A", 1)])
# [("A", 0), ("B", 0)]
allocate(11, [("ONLY", 7)])
# [("ONLY", 11)]
allocate(10**12, [("A", 10**9), ("B", 10**9)])
# [("A", 500000000000), ("B", 500000000000)]
allocate(True, [("A", 1)]) # ValueError: INVALID_TOTAL
allocate(1, [("A", True)]) # ValueError: INVALID_WEIGHT
allocate(1, [("A", 1), ("A", 2)]) # ValueError: DUPLICATE_ID
allocate(0, [("A", 0)]) # ValueError: ZERO_WEIGHT_SUM7. 验收既要看总额,也要看这 1 单位给了谁
只测试“结果加起来等于总额”不够。例如把所有差额都塞给最后一行,可能守住总额,却违反余数顺序、单行上下界或者输入重排不变性。测试需要同时核对精确样例、整数与非负、总额守恒、零权重、每行的精确份额上下界、规范输出顺序和输入不被修改。
完整测试用 Fraction 表示精确份额,避免验收代码再引入浮点误差。三个三行样例各检查全部 6 种排列。小范围穷举覆盖总额 0 到 30、三个权重各为 0 到 5 的组合,排除全零权重,共 31 × (6^3 − 1) = 6665 组输入。这个范围用来发现小反例;它不等于证明所有输入都正确。
错误输入还覆盖布尔值、非整数、负数、越界、重复 ID、空列表、非法行形状和 ID 字符。另有专门的多错输入测试,只验证已声明的检查优先级;总额为零仍必须检查后续所有行。
负向对照会分别把实现改成六种有意错误的版本:各行独立四舍五入;差额全给最后一行;并列按输入顺序;接受布尔总额;接受布尔权重;静默合并重复 ID。每种错误实现都应让同一套断言失败。验证脚本还检查代码可以编译、加载的是隔离目录中的变体、失败确实来自断言而非导入或运行错误。仅看进程非零退出码,不能算有效对照。
8. 本地运行记录与复跑方法
2026-10-14,参考样例在本地 Python 3.12.14 环境运行。基线 13 个单元测试方法全部通过,24 组 JSON 测试数据的演示输出与预期文件在统一换行后的文本比较中一致,6665 组小范围输入完成性质检查。六种有意错误的实现全部被断言拒绝,且没有测试运行错误。另一次独立核对包含 42 组明确测试数据、排列及大数检查,累计执行 42065 次分配函数调用并通过;这不是 42065 个独立测试方法。这里记录的是这份代码的本地测试结果,不是 Codex 产品效果测试,也不是生产验收。
下载并解压文末样例包,在解压后的文件目录运行:
python -m unittest -v test_allocate
python demo.py
python verify.py第一条运行单元测试;第二条打印测试数据结果;第三条重跑基线、演示输出对比和六种负向对照,并更新该目录的验证记录。命令不需要第三方依赖、账号或网络。测试记录中的耗时只描述当次环境,不作为性能承诺。
2026-10-20 复核时修正了一处验收表述:verify.py 以文本模式读取子进程输出和预期文件,CRLF、CR 和 LF 会统一为换行符,因此它验证的是文本一致,不是原始字节一致。报告字段已从 byte_equal_expected 改为 normalized_text_equal_expected。单独把预期文件改成 CRLF 后仍应通过;其他文本差异仍应失败。分配算法、输入契约和已有测试范围均未改变。
9. 总额增加,某一行反而可能减少
不要把这套规则扩展成“总额增加时,每个对象的分配都不会减少”。固定权重 A=5、B=3、C=1,当总额为 4 时,整数商是 2、1、0,余数是 2、3、4,剩余 1 单位补给 C,得到 (2, 1, 1)。
总额变成 5 时,整数商仍是 2、1、0,余数变成 7、6、5,剩余 2 单位依次补给 A 和 B,得到 (3, 2, 0)。总额多了 1,C 却从 1 变成 0。完整测试保留这个反例,防止说明文字或后续修改误报单调性。
如果你的需求禁止这种变化,就需要另选分配政策,并重新写验收条件。同样,ID 升序只是一种固定的并列裁决;更换 ID 可能改变谁拿到末尾单位。不要把 ID 重新命名视为与结果无关的整理操作;拆成多个 ID 也会改变分配对象,必须受同一套身份与政策约束。
10. 把任务交给编码助手时,可以直接用这段要求
先确定政策,再把实现任务交给 Codex 或其他编码助手。下面的提示词可以独立使用;拿到代码以后,仍应自己查看输入验证和余数排序,并运行测试。没有日志的“全部通过”不能替代验收记录。
请实现纯函数 allocate(total_minor, rows),使用 Python 标准库。
输入契约:total_minor 类型恰为 int,0..10**12;rows 类型恰为 list,含 1..1000 行;每行类型恰为 list 或 tuple 且长度为 2;ID 类型恰为 str,匹配 ASCII [A-Z][A-Z0-9_]{0,31},唯一且不做规范化;weight 类型恰为 int,0..10**9;总权重为正。拒绝 bool 和所有上述内置类型的子类。
先完整验证,不修改输入,不返回部分结果。检查顺序为总额、集合及行数、按输入顺序逐行的形状/ID/重复/权重、总权重。用 ValueError(CODE) 返回 INVALID_TOTAL、INVALID_ROWS、INVALID_ID、DUPLICATE_ID、INVALID_WEIGHT 或 ZERO_WEIGHT_SUM。
计算 divmod(total_minor * weight, sum_weights)。先分配商,再把剩余单位按余数降序、ID 升序逐行各加 1。最后按 ID 升序返回二元组列表。零权重始终为 0,总额为 0 仍验证全部输入。
交付 allocate.py、test_allocate.py 和运行记录。测试精确样例 10000/[1,1,1]→[3334,3333,3333]、7/[5,3,2]→[4,2,1]、2/[1,1,1]→[1,1,0],ID 均按 A/B/C;检查全部六种输入排列、零权重、零总额、单行、边界及非法输入。用 Fraction 检查精确份额的 floor/ceil;穷举 total=0..30 和三权重=0..5(排除全零),验证守恒、整数非负和输入未改。
负向对照必须拒绝独立四舍五入、差额全塞最后一行、并列按输入顺序、接受布尔总额、接受布尔权重、静默合并重复 ID。确认失败来自断言,而不是语法/导入错误。
保留权重 [5,3,1] 在总额 4→5 时结果 [2,1,1]→[3,2,0] 的反例,不承诺总额单调性。只报告实际运行的命令、环境及结果;没有运行就明确写未运行。不要添加 API、文件导入、货币解析、负数退款或付款功能。11. 使用边界与语言依据
这是合成算法教学案例,不提供税务、会计、工资、账单或金融合规结论。输入已经换成同一明确单位的整数;十进制输入解析、不同币种或小数位、汇率、负额退款、单行上限、跨批次累计公平性及实际付款,均不在此函数范围内。代码层面的确定性也不能替代政策审批。
Python 官方文档说明了本文使用的语言机制:divmod 的商和余数、整数精度与布尔类型关系、排序键。这些文档支持实现手段,不替具体业务选择分配政策。参考资料核对日期为 2026-10-14。
12. 完整测试与辅助脚本
下面可以展开查看完整测试及辅助脚本。下载包同时附带 JSON 输入/预期输出、说明文件和本次运行记录;所有数据均为合成示例。
test_allocate.py 完整源码
"""Explicit acceptance tests: python -m unittest -v test_allocate."""
import copy
from fractions import Fraction
import itertools
import json
from pathlib import Path
import unittest
from allocate import allocate
ROOT = Path(__file__).resolve().parent
class AllocationTests(unittest.TestCase):
def assert_invariants(self, total, rows):
before = copy.deepcopy(rows)
result = allocate(total, rows)
self.assertEqual(rows, before)
self.assertEqual([identifier for identifier, _ in result], sorted(r[0] for r in rows))
self.assertEqual(sum(amount for _, amount in result), total)
weights = dict(rows)
denominator = sum(weights.values())
for identifier, amount in result:
self.assertIs(type(amount), int)
self.assertGreaterEqual(amount, 0)
quota = Fraction(total * weights[identifier], denominator)
floor = quota.numerator // quota.denominator
ceiling = -(-quota.numerator // quota.denominator)
self.assertIn(amount, (floor, ceiling))
if weights[identifier] == 0:
self.assertEqual(amount, 0)
return result
def assert_code(self, code, total, rows):
before = copy.deepcopy(rows)
with self.assertRaises(ValueError) as caught:
allocate(total, rows)
self.assertEqual(str(caught.exception), code)
self.assertEqual(rows, before)
def test_fixtures(self):
cases = json.loads((ROOT / "fixtures-input.json").read_text())
expected = json.loads((ROOT / "fixtures-output.json").read_text())
self.assertEqual(len(cases), len(expected))
for case, output in zip(cases, expected):
with self.subTest(case=case):
if "error" in output:
self.assert_code(output["error"], case["total_minor"], case["rows"])
else:
result = self.assert_invariants(case["total_minor"], case["rows"])
self.assertEqual([list(pair) for pair in result], output["result"])
def test_all_three_row_fixture_permutations(self):
fixtures = [(10000, [("C", 1), ("A", 1), ("B", 1)], [("A",3334),("B",3333),("C",3333)]),
(7, [("A",5),("B",3),("C",2)], [("A",4),("B",2),("C",1)]),
(2, [("C",1),("B",1),("A",1)], [("A",1),("B",1),("C",0)])]
for total, rows, expected in fixtures:
for permutation in itertools.permutations(rows):
result = self.assert_invariants(total, list(permutation))
self.assertEqual(result, expected)
self.assertEqual(json.dumps(result), json.dumps(expected))
def test_exhaustive_small_cases(self):
# 31 * (6**3 - 1) = 6,665 exact-quota and conservation checks.
for weights in itertools.product(range(6), repeat=3):
if not any(weights):
continue
rows = list(zip(("A", "B", "C"), weights))
for total in range(31):
self.assert_invariants(total, rows)
def test_collection_contract(self):
for rows in (None, {}, (), (("A", 1),), "A", [], [["A"]], [["A",1,2]], [None], ["A1"], [{"A":1}]):
self.assert_code("INVALID_ROWS", 1, rows)
self.assert_code("INVALID_ROWS", 1, [(f"A{i}", 1) for i in range(1001)])
self.assertEqual(allocate(2, [["B", 1], ("A", 1)]), [("A",1),("B",1)])
rows = [(f"A{i}", 1) for i in range(1000)]
self.assert_invariants(10**12, rows)
with self.assertRaisesRegex(ValueError, "^INVALID_ROWS$"):
allocate(1, iter([("A", 1)]))
def test_invalid_ids(self):
for identifier in ("a", "", 0, True, None, " A", "A ", "A\n", "A", "Aé", "A-1", "1A", "A"*33):
self.assert_code("INVALID_ID", 1, [(identifier, 1)])
self.assertEqual(allocate(1, [("A" + "_"*31, 1)]), [("A" + "_"*31, 1)])
self.assert_code("DUPLICATE_ID", 1, [("A",1),("A",2)])
def test_exact_integer_types(self):
class IntSubclass(int):
pass
for total in (True, False, 1.0, "1", None, -1, 10**12+1, IntSubclass(1)):
self.assert_code("INVALID_TOTAL", total, [("A",1)])
for weight in (True, False, 1.0, "1", None, -1, 10**9+1, IntSubclass(1)):
self.assert_code("INVALID_WEIGHT", 1, [("A",weight)])
def test_exact_container_and_id_types(self):
class ListSubclass(list):
pass
class TupleSubclass(tuple):
pass
class StringSubclass(str):
pass
self.assert_code("INVALID_ROWS", 1, ListSubclass([("A",1)]))
self.assert_code("INVALID_ROWS", 1, [ListSubclass(["A",1])])
self.assert_code("INVALID_ROWS", 1, [TupleSubclass(("A",1))])
self.assert_code("INVALID_ID", 1, [(StringSubclass("A"),1)])
def test_zero_cases(self):
for total in (0, 1):
self.assert_code("ZERO_WEIGHT_SUM", total, [("A",0)])
self.assertEqual(allocate(0, [("B",2),("A",0)]), [("A",0),("B",0)])
self.assertEqual(allocate(1, [("A",0),("B",1)]), [("A",0),("B",1)])
def test_validation_precedence(self):
# Intentionally multiple faults: these specifically test precedence.
self.assert_code("INVALID_TOTAL", True, [])
self.assert_code("INVALID_ROWS", 1, [["a"]])
self.assert_code("INVALID_ID", 1, [("a",-1)])
self.assert_code("DUPLICATE_ID", 1, [("A",1),("A",-1)])
self.assert_code("INVALID_WEIGHT", 1, [("A",-1),("a",1)])
self.assert_code("INVALID_ID", 0, [("a",0)])
def test_zero_total_still_validates_late_rows(self):
self.assert_code("INVALID_WEIGHT", 0, [["B",2],["A",True]])
self.assert_code("DUPLICATE_ID", 0, [["B",2],["B",1]])
def test_large_exact_products(self):
self.assert_invariants(10**12-1, [("C",10**9),("A",10**9-1),("B",1)])
def test_exact_remainder_precision_trap(self):
self.assertEqual(
allocate(999999999501, [("A",999999999),("B",1000000000)]),
[("A",499999999500),("B",500000000001)])
def test_total_increase_can_reduce_a_row(self):
rows = [("A",5),("B",3),("C",1)]
self.assertEqual(allocate(4, rows), [("A",2),("B",1),("C",1)])
self.assertEqual(allocate(5, rows), [("A",3),("B",2),("C",0)])
if __name__ == "__main__":
unittest.main()
demo.py 完整源码
"""Print deterministic fixture outputs; no service or coding-agent calls."""
import json
from pathlib import Path
from allocate import allocate
def evaluate(case):
try:
return {"result": allocate(case["total_minor"], case["rows"])}
except ValueError as error:
return {"error": str(error)}
if __name__ == "__main__":
cases = json.loads(Path(__file__).with_name("fixtures-input.json").read_text())
print(json.dumps([evaluate(case) for case in cases], indent=2))
build_fixtures.py 完整源码
"""Generate committed synthetic fixtures from explicit expected answers.
This authoring helper does not import or call the implementation under test.
"""
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parent
cases = []
outputs = []
def add(total, rows, result=None, error=None):
cases.append({"total_minor": total, "rows": rows})
outputs.append({"error": error} if error else {"result": result})
add(10000, [["C",1],["A",1],["B",1]], [["A",3334],["B",3333],["C",3333]])
add(7, [["A",5],["B",3],["C",2]], [["A",4],["B",2],["C",1]])
add(2, [["C",1],["B",1],["A",1]], [["A",1],["B",1],["C",0]])
add(5, [["B",2],["A",0]], [["A",0],["B",5]])
add(0, [["B",2],["A",1]], [["A",0],["B",0]])
add(11, [["ONLY",7]], [["ONLY",11]])
add(10**12, [["A",10**9],["B",10**9]], [["A",500000000000],["B",500000000000]])
for total in (-1, True, 1.0, 10**12+1):
add(total, [["A",1]], error="INVALID_TOTAL")
add(1, [], error="INVALID_ROWS")
add(1, [[f"A{i}",1] for i in range(1001)], error="INVALID_ROWS")
for identifier in ("a", "", 0):
add(1, [[identifier,1]], error="INVALID_ID")
add(1, [["A",1],["A",2]], error="DUPLICATE_ID")
for weight in (-1, True, 1.5, "1", 10**9+1):
add(1, [["A",weight]], error="INVALID_WEIGHT")
for total in (0,1):
add(total, [["A",0]], error="ZERO_WEIGHT_SUM")
for filename, data in (("fixtures-input.json", cases), ("fixtures-output.json", outputs)):
(ROOT / filename).write_text(json.dumps(data, indent=2) + "\n")
verify.py 完整源码
"""Capture actual baseline, deterministic demo, and isolated mutation results.
Each mutant must compile, be imported from its own isolated directory, fail
acceptance by assertion failure, and produce no test errors/import failures.
"""
import hashlib
import json
import os
from pathlib import Path
import platform
import shutil
import subprocess
import sys
import tempfile
import time
ROOT = Path(__file__).resolve().parent
LOGS = ROOT / "verification"
LOGS.mkdir(exist_ok=True)
SOURCE = (ROOT / "allocate.py").read_text()
def replace_once(old, new):
if SOURCE.count(old) != 1:
raise AssertionError(f"Mutation anchor must occur once: {old!r}")
return SOURCE.replace(old, new, 1)
mutations = {
"independent_rounding": replace_once(
" return sorted(amounts.items())",
" return sorted((identifier, round(total_minor * weight / sum_weights))\n"
" for identifier, weight in validated)"),
"dump_residual_into_last_row": replace_once(
" for identifier, _ in ranked[:remaining]:\n amounts[identifier] += 1",
" amounts[validated[-1][0]] += remaining"),
"input_order_tie_ownership": replace_once(
"key=lambda item: (-item[1], item[0])", "key=lambda item: -item[1]"),
"accept_boolean_total": replace_once(
"type(total_minor) is not int", "not isinstance(total_minor, int)"),
"accept_boolean_weight": replace_once(
"type(weight) is not int", "not isinstance(weight, int)"),
"silently_merge_duplicate_ids": replace_once(
" if identifier in seen:\n raise ValueError(\"DUPLICATE_ID\")",
" if identifier in seen:\n"
" validated = [(key, value + weight if key == identifier else value)\n"
" for key, value in validated]\n"
" continue"),
}
def run(command, cwd, name):
start = time.perf_counter()
env = os.environ.copy()
env.pop("PYTHONPATH", None)
process = subprocess.run(command, cwd=cwd, env=env, text=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
duration = time.perf_counter() - start
(LOGS / (name + ".stdout.txt")).write_text(process.stdout)
(LOGS / (name + ".stderr.txt")).write_text(process.stderr)
return process, {"command": command, "exit_code": process.returncode,
"seconds": duration, "stdout": name + ".stdout.txt",
"stderr": name + ".stderr.txt"}
report = {"python": sys.version, "executable": sys.executable,
"platform": platform.platform(),
"source_sha256": hashlib.sha256(SOURCE.encode()).hexdigest(),
"small_invariant_cases": 6665, "fixture_cases": 24, "mutations": {}}
baseline, report["baseline"] = run([sys.executable, "-m", "unittest", "-v", "test_allocate"], ROOT, "baseline")
if baseline.returncode:
raise SystemExit("Baseline failed; see verification/baseline.stderr.txt")
demo, report["demo"] = run([sys.executable, "demo.py"], ROOT, "demo")
expected = (ROOT / "fixtures-output.json").read_text()
report["demo"]["normalized_text_equal_expected"] = demo.stdout == expected
if demo.returncode or demo.stdout != expected:
raise SystemExit("Demo differs from committed expected output")
for name, source in mutations.items():
compile(source, "allocate.py", "exec")
(LOGS / (name + ".py")).write_text(source)
with tempfile.TemporaryDirectory(prefix="mutant_", dir=ROOT) as directory:
directory = Path(directory)
(directory / "allocate.py").write_text(source)
for filename in ("test_allocate.py", "fixtures-input.json", "fixtures-output.json"):
shutil.copyfile(ROOT / filename, directory / filename)
probe, probe_data = run([sys.executable, "-c", "import allocate; print(allocate.__file__)"], directory, name + ".import")
if probe.returncode or Path(probe.stdout.strip()) != directory / "allocate.py":
raise SystemExit(f"Wrong mutation import: {name}")
result, record = run([sys.executable, "-m", "unittest", "-v", "test_allocate"], directory, name)
record["compile_ok"] = True
record["isolated_import_verified"] = True
record["assertion_failure_present"] = "FAIL:" in result.stderr
record["no_test_errors"] = "ERROR:" not in result.stderr
record["killed"] = (result.returncode != 0 and record["assertion_failure_present"] and record["no_test_errors"])
report["mutations"][name] = record
report["all_mutations_killed"] = all(record["killed"] for record in report["mutations"].values())
(LOGS / "report.json").write_text(json.dumps(report, indent=2) + "\n")
print(json.dumps(report, indent=2))
if not report["all_mutations_killed"]:
raise SystemExit("A mutant survived or failed for an invalid reason")
13. 下载可复跑样例
下载完整 Python 样例 ZIP。如果浏览器不支持页面中的内嵌下载,可从上面的源码区复制五个 Python 文件到同一目录,先运行 python build_fixtures.py 生成内置合成测试数据,再运行三条验收命令。该辅助脚本使用写死的预期答案,不从待测实现生成答案;正常测试不应重建预期文件。