feat(pack): FACTORY 工厂镜像 + 内置 profile 改名 CeramixNano_MB_Dual_FW
This commit is contained in:
+100
-20
@@ -10,9 +10,10 @@ fw_pack / gd32e230_firmware_tool v2 回归测试套件
|
||||
4. A 包整页扩展规则(恰整页 / 余量<4 / 余量==4 / 超槽);
|
||||
5. B 包固定满槽 + 记录(满槽输入 / 小输入 / 超槽-8);
|
||||
6. FULL 拼装 + 逐项字节校验 + 回读复核(含篡改检测);
|
||||
7. 真机固件回归(CeramixNano 1.0.17/1.0.18/V1.0.12, 字节级复现
|
||||
7. FACTORY 工厂镜像(BL+B+flag 无 A, 锚点擦除态) + 篡改检测;
|
||||
8. 真机固件回归(CeramixNano 1.0.17/1.0.18/V1.0.12, 字节级复现
|
||||
2026-10-22 验收包, 文件缺失时自动跳过);
|
||||
8. CLI 冒烟(v2 / legacy / 错误路径)。
|
||||
9. CLI 冒烟(v2 / legacy / 错误路径)。
|
||||
|
||||
运行: python test_fw_pack.py [-v]
|
||||
全部通过 = v2 升级验收通过。
|
||||
@@ -89,11 +90,11 @@ def synth_bin(base, size, rst_off=0x101):
|
||||
|
||||
|
||||
def ceramix_profile():
|
||||
return fw_pack.load_profile('ceramixnano_mb_v51')
|
||||
return fw_pack.load_profile('CeramixNano_MB_Dual_FW')
|
||||
|
||||
|
||||
def variant_profile(slot_a_k, slot_b_k, with_b=True):
|
||||
raw = json.loads(json.dumps(fw_pack.BUILTIN_PROFILES['ceramixnano_mb_v51']))
|
||||
raw = json.loads(json.dumps(fw_pack.BUILTIN_PROFILES['CeramixNano_MB_Dual_FW']))
|
||||
raw['slot_a']['size'] = f'{slot_a_k}K'
|
||||
if with_b:
|
||||
raw['slot_b']['size'] = f'{slot_b_k}K'
|
||||
@@ -183,7 +184,7 @@ class TestProfile(unittest.TestCase):
|
||||
fw_pack.parse_num('')
|
||||
|
||||
def _invalid(self, mutate, needle):
|
||||
raw = json.loads(json.dumps(fw_pack.BUILTIN_PROFILES['ceramixnano_mb_v51']))
|
||||
raw = json.loads(json.dumps(fw_pack.BUILTIN_PROFILES['CeramixNano_MB_Dual_FW']))
|
||||
mutate(raw)
|
||||
with self.assertRaises(fw_pack.FwPackError) as cm:
|
||||
fw_pack.Profile(raw)
|
||||
@@ -208,9 +209,9 @@ class TestProfile(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
path = os.path.join(td, 'p.json')
|
||||
with open(path, 'w', encoding='utf-8') as f:
|
||||
json.dump(fw_pack.BUILTIN_PROFILES['ceramixnano_mb_v51'], f)
|
||||
json.dump(fw_pack.BUILTIN_PROFILES['CeramixNano_MB_Dual_FW'], f)
|
||||
p = fw_pack.load_profile(path)
|
||||
self.assertEqual(p.name, 'ceramixnano_mb_v51')
|
||||
self.assertEqual(p.name, 'CeramixNano_MB_Dual_FW')
|
||||
bad = os.path.join(td, 'bad.json')
|
||||
with open(bad, 'w', encoding='utf-8') as f:
|
||||
f.write('{ not json')
|
||||
@@ -441,7 +442,60 @@ class TestFull(unittest.TestCase):
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 7. 真机回归: CeramixNano v5.1 (字节级复现验收包)
|
||||
# 7. FACTORY 工厂镜像 (BL + B 满槽 + flag, 无 A)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
class TestFactory(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.p = ceramix_profile()
|
||||
self.bl = synth_bin(BL_BASE, 4044, rst_off=0xD00)
|
||||
self.b_body = synth_bin(B_BASE, SLOT_B_SIZE - 8)
|
||||
self.bp = fw_pack.build_b_pack(self.b_body, self.p)
|
||||
|
||||
def test_factory_layout_bytes(self):
|
||||
"""BL + B 满槽 + flag; A 槽(含锚点)整体擦除态 → 上电判定链落 B"""
|
||||
img = fw_pack.build_factory(self.p, self.bl, self.bp)
|
||||
self.assertEqual(len(img), 64 * 1024)
|
||||
self.assertEqual(img[:4044], self.bl)
|
||||
self.assertTrue(all(b == 0xFF for b in img[4044:0xB000])) # BL 余量 + 整个 A 槽
|
||||
self.assertEqual(img[0xAFFC:0xB000], b'\xff' * 4) # 锚点擦除态
|
||||
self.assertEqual(img[0xB000:0xFC00], self.bp.data)
|
||||
self.assertEqual(img[0xFFFC:0x10000], b'\xee\xee\x00\x00')
|
||||
|
||||
def test_verify_factory_all_pass(self):
|
||||
img = fw_pack.build_factory(self.p, self.bl, self.bp)
|
||||
checks = fw_pack.verify_factory_blob(img, self.p, self.bl, self.b_body)
|
||||
bad = [(n, t) for n, ok, t in checks if not ok]
|
||||
self.assertEqual(bad, [], checks)
|
||||
self.assertGreaterEqual(len(checks), 10)
|
||||
|
||||
def test_verify_factory_detects_corruption(self):
|
||||
img = bytearray(fw_pack.build_factory(self.p, self.bl, self.bp))
|
||||
img[0xB000 + 100] ^= 0xFF # 破坏 B 体
|
||||
checks = fw_pack.verify_factory_blob(bytes(img), self.p, self.bl,
|
||||
self.b_body)
|
||||
self.assertFalse(all(ok for _n, ok, _t in checks))
|
||||
img2 = bytearray(fw_pack.build_factory(self.p, self.bl, self.bp))
|
||||
img2[0xAFFC] = 0x14 # A 槽出现锚点痕迹
|
||||
checks = fw_pack.verify_factory_blob(bytes(img2), self.p, self.bl,
|
||||
self.b_body)
|
||||
self.assertFalse(all(ok for _n, ok, _t in checks))
|
||||
|
||||
def test_requires_b_pack(self):
|
||||
with self.assertRaises(fw_pack.FwPackError):
|
||||
fw_pack.build_factory(self.p, self.bl, None)
|
||||
|
||||
def test_suggest_factory_name(self):
|
||||
self.assertEqual(
|
||||
fw_pack.suggest_factory_name(
|
||||
r'x\CeramixNano-MB_V1.0.12_APP_2026-10-20.bin'),
|
||||
'CeramixNano-MB_FACTORY_B-V1.0.12_2026-10-20.bin')
|
||||
self.assertEqual(fw_pack.suggest_factory_name(r'x\app_b.bin'),
|
||||
'app_b_FACTORY.bin')
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 8. 真机回归: CeramixNano v5.1 (字节级复现验收包)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
@unittest.skipUnless(HAVE_REAL, '需要 CeramixNano 固件(见 test_fw_pack.py 头部路径)')
|
||||
@@ -499,25 +553,31 @@ class TestRegressionCeramixNano(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as td:
|
||||
bl_path = os.path.join(td, 'bl.bin')
|
||||
open(bl_path, 'wb').write(self.bl)
|
||||
out = fw_pack.pack_all('ceramixnano_mb_v51', A17, b_path=B12,
|
||||
out = fw_pack.pack_all('CeramixNano_MB_Dual_FW', A17, b_path=B12,
|
||||
bl_path=bl_path, out_dir=td, progress=quiet())
|
||||
upd = open(out.paths['update'], 'rb').read()
|
||||
bpk = open(out.paths['b'], 'rb').read()
|
||||
full = open(out.paths['full'], 'rb').read()
|
||||
fac = open(out.paths['factory'], 'rb').read()
|
||||
self.assertEqual(upd, open(U17_REF, 'rb').read())
|
||||
self.assertEqual(bpk[-8:], bytes.fromhex('de c0 13 00 6f 18 ff ff'))
|
||||
self.assertEqual(full, self.ref_full)
|
||||
# FACTORY = 参考 FULL 把 A 槽区(含锚点)整体置 0xFF
|
||||
expect_fac = bytearray(self.ref_full)
|
||||
expect_fac[0x2000:0xB000] = b'\xFF' * (0xB000 - 0x2000)
|
||||
self.assertEqual(fac, bytes(expect_fac))
|
||||
self.assertTrue(out.ok)
|
||||
# FULL 自动命名与验收包一致
|
||||
# 自动命名: FULL 与验收包一致, FACTORY 带 B 版本号
|
||||
self.assertEqual(os.path.basename(out.paths['full']),
|
||||
'CeramixNano-MB_FULL_A-V1.0.17_B-V1.0.12_2026-10-21.bin')
|
||||
# MD5 清单包含全部产物与正确摘要
|
||||
self.assertEqual(os.path.basename(out.paths['factory']),
|
||||
'CeramixNano-MB_FACTORY_B-V1.0.12_2026-10-20.bin')
|
||||
# MD5 清单挂在 FULL, 包含全部产物与正确摘要
|
||||
mani = open(out.paths['md5'], encoding='utf-8').read()
|
||||
for key in ('update', 'b', 'full'):
|
||||
for key in ('update', 'full', 'factory'):
|
||||
blob = open(out.paths[key], 'rb').read()
|
||||
self.assertIn(hashlib.md5(blob).hexdigest(), mani)
|
||||
self.assertIn('ceramixnano_mb_v51', mani)
|
||||
self.assertIn('CeramixNano_MB_Dual_FW', mani)
|
||||
self.assertIn('0xD30F0014', mani)
|
||||
self.assertIn('DE C0 13 00 6F 18 FF FF', mani)
|
||||
|
||||
def test_suggest_full_name(self):
|
||||
self.assertEqual(
|
||||
@@ -539,8 +599,10 @@ class TestCLI(unittest.TestCase):
|
||||
self.dir = self.td.name
|
||||
self.a = os.path.join(self.dir, 'app_a.bin')
|
||||
self.b = os.path.join(self.dir, 'app_b.bin')
|
||||
self.bl = os.path.join(self.dir, 'bl.bin')
|
||||
open(self.a, 'wb').write(synth_bin(A_BASE, 19580))
|
||||
open(self.b, 'wb').write(synth_bin(B_BASE, SLOT_B_SIZE))
|
||||
open(self.bl, 'wb').write(synth_bin(BL_BASE, 4044, rst_off=0xD00))
|
||||
|
||||
def tearDown(self):
|
||||
self.td.cleanup()
|
||||
@@ -548,21 +610,31 @@ class TestCLI(unittest.TestCase):
|
||||
def test_v2_full_pipeline(self):
|
||||
rc, out, err = run_cli(self.a, self.b, '-o', self.dir)
|
||||
self.assertEqual(rc, 0, err or out)
|
||||
for name in ('app_a_UPDATE.bin', 'app_b_B.bin', 'app_a_FULL.bin'):
|
||||
for name in ('app_a_UPDATE.bin', 'app_a_FULL.bin', 'app_b_FACTORY.bin'):
|
||||
self.assertTrue(os.path.isfile(os.path.join(self.dir, name)), name)
|
||||
self.assertTrue(os.path.isfile(
|
||||
os.path.join(self.dir, 'app_a_FULL.bin.md5.txt')))
|
||||
full = open(os.path.join(self.dir, 'app_a_FULL.bin'), 'rb').read()
|
||||
self.assertEqual(len(full), 65536)
|
||||
fac = open(os.path.join(self.dir, 'app_b_FACTORY.bin'), 'rb').read()
|
||||
self.assertEqual(fac[0x2000:0xB000], b'\xFF' * 0x9000) # A 槽擦除态
|
||||
self.assertFalse([f for f in os.listdir(self.dir)
|
||||
if f.endswith('_B.bin')]) # 不再单独出 B 包
|
||||
self.assertIn('回读复核通过', out)
|
||||
|
||||
def test_v2_b_only_no_full(self):
|
||||
"""仅 B 输入 → 只出 *_B.bin, 不拼 FULL (无 A 运行槽的整片镜像不可启动)"""
|
||||
rc, out, err = run_cli(self.b, '-o', self.dir)
|
||||
def test_v2_b_only_factory(self):
|
||||
"""仅 B 输入 → 只出 *_FACTORY.bin(+MD5), 不拼 FULL"""
|
||||
rc, out, err = run_cli(self.b, '-b', self.bl, '-o', self.dir)
|
||||
self.assertEqual(rc, 0, err or out)
|
||||
self.assertTrue(os.path.isfile(os.path.join(self.dir, 'app_b_B.bin')))
|
||||
self.assertTrue(os.path.isfile(os.path.join(self.dir, 'app_b_FACTORY.bin')))
|
||||
self.assertTrue(os.path.isfile(os.path.join(self.dir, 'app_b_FACTORY.bin.md5.txt')))
|
||||
self.assertFalse([f for f in os.listdir(self.dir) if 'FULL' in f])
|
||||
|
||||
def test_v2_no_factory_flag(self):
|
||||
rc, out, err = run_cli(self.a, self.b, '-o', self.dir, '--no-factory')
|
||||
self.assertEqual(rc, 0, err or out)
|
||||
self.assertFalse([f for f in os.listdir(self.dir) if 'FACTORY' in f])
|
||||
|
||||
def test_v2_explicit_conflict(self):
|
||||
rc, out, err = run_cli('--b', self.a) # A 固件显式塞给 B 槽
|
||||
self.assertNotEqual(rc, 0)
|
||||
@@ -603,8 +675,16 @@ class TestCLI(unittest.TestCase):
|
||||
td, 'CeramixNano-MB_FULL_A-V1.0.17_B-V1.0.12_2026-10-21.bin'),
|
||||
'rb').read()
|
||||
self.assertEqual(full, open(FULL_REF, 'rb').read())
|
||||
expect_fac = bytearray(open(FULL_REF, 'rb').read())
|
||||
expect_fac[0x2000:0xB000] = b'\xFF' * (0xB000 - 0x2000)
|
||||
fac = open(os.path.join(td,
|
||||
'CeramixNano-MB_FACTORY_B-V1.0.12_2026-10-20.bin'),
|
||||
'rb').read()
|
||||
self.assertEqual(fac, bytes(expect_fac))
|
||||
self.assertIn('0xD30F0014', out)
|
||||
self.assertIn('DE C0 13 00 6F 18 FF FF', out)
|
||||
self.assertIn('FACTORY', out)
|
||||
self.assertFalse([f for f in os.listdir(td) if f.endswith('_B.bin')])
|
||||
|
||||
def test_legacy_fill(self):
|
||||
out = os.path.join(self.dir, 'legacy_update.bin')
|
||||
|
||||
Reference in New Issue
Block a user