"""测试 G-code 准备阶段缓存行为,确保重复副本不会重复执行预处理。 验证内容: - 同一 G-code 成员多次复制时,补丁只应用一次 - 同一 G-code 成员多次复制时,M73 偏移模板只计算一次 """ from __future__ import annotations import hashlib import tempfile import unittest import zipfile from pathlib import Path from unittest.mock import patch import a1_swap_mod_packer.builder as builder from a1_swap_mod_packer.models import BuildOptions, GcodePatchConfig, GcodePatchRule, PlateJob class GcodePrepareCacheTest(unittest.TestCase): """测试 G-code 准备阶段的缓存:补丁和 M73 模板应每成员仅执行一次。""" def test_repeated_copies_prepare_gcode_once_per_member(self) -> None: """验证同一 G-code 成员多次复制时,补丁仅在首次准备时应用一次。""" with tempfile.TemporaryDirectory() as directory: root = Path(directory) source_3mf = root / "source.3mf" output_3mf = root / "out.3mf" swap_gcode = root / "swap.gcode" self.write_archive(source_3mf) # 创建测试用 3MF 归档 swap_gcode.write_text("; swap\nG1 X5\n", encoding="utf-8") # 配置一条简单的替换规则 patch_config = GcodePatchConfig( rules=( GcodePatchRule( name="test", find="G0 Y254 F3000", replace="G0 Y250 F3000 ;Patched", ), ) ) patch_calls = 0 # 记录补丁被调用次数 captured_gcode = "" # 捕获最终 G-code 内容 def count_patch_calls(text: str, config: GcodePatchConfig) -> str: """模拟补丁应用并累计调用次数。""" nonlocal patch_calls patch_calls += 1 return text.replace("G0 Y254 F3000", "G0 Y250 F3000 ;Patched", 1) def capture_gcode( base_3mf: Path, output_3mf: Path, gcode_bytes: bytes, sources: list[builder.PlateSource], options: BuildOptions, ) -> str: """截获最终写入的 G-code 字节以便断言。""" nonlocal captured_gcode captured_gcode = gcode_bytes.decode("utf-8") return hashlib.md5(gcode_bytes).hexdigest() options = BuildOptions( swap_gcode=swap_gcode, output_3mf=output_3mf, apply_gcode_patches=True, show_plate_number=False, swap_after_final=False, line_ending="lf", add_preview_label=False, ) # 构建 3 份副本,验证补丁仅调用 1 次但 3 份均被替换 with patch.object(builder, "parse_patch_config", return_value=patch_config), patch.object( builder, "apply_gcode_patches", side_effect=count_patch_calls, ), patch.object(builder, "write_output_3mf", side_effect=capture_gcode): result = builder.build_packed_3mf([PlateJob(source_3mf, 3)], options) self.assertEqual(result.plate_count, 3) # 3 份副本 self.assertEqual(patch_calls, 1) # 补丁仅调用 1 次(缓存生效) self.assertEqual(captured_gcode.count("G0 Y250 F3000 ;Patched"), 3) # 但 3 份都已替换 self.assertNotIn("G0 Y254 F3000", captured_gcode) # 原始行已不存在 def test_repeated_copies_prepare_m73_template_once_per_member(self) -> None: """验证同一 G-code 成员多次复制时,M73 偏移模板仅计算一次。""" with tempfile.TemporaryDirectory() as directory: root = Path(directory) source_3mf = root / "source.3mf" output_3mf = root / "out.3mf" swap_gcode = root / "swap.gcode" self.write_archive(source_3mf) swap_gcode.write_text("; swap\nG1 X5\n", encoding="utf-8") m73_template_calls = 0 # 记录模板准备调用次数 captured_gcode = "" original_prepare_m73 = builder.prepare_m73_offset_template # 保存原始函数引用 def count_m73_template_calls(text: str): """模拟 M73 模板准备并累计调用次数。""" nonlocal m73_template_calls m73_template_calls += 1 return original_prepare_m73(text) def capture_gcode( base_3mf: Path, output_3mf: Path, gcode_bytes: bytes, sources: list[builder.PlateSource], options: BuildOptions, ) -> str: """截获最终写入的 G-code 字节以便断言。""" nonlocal captured_gcode captured_gcode = gcode_bytes.decode("utf-8") return hashlib.md5(gcode_bytes).hexdigest() options = BuildOptions( swap_gcode=swap_gcode, output_3mf=output_3mf, apply_gcode_patches=False, show_plate_number=True, # 启用板号显示,触发 M73 偏移 swap_after_final=False, line_ending="lf", add_preview_label=False, ) # 构建 3 份副本,验证模板仅准备 1 次,但每份各自偏移 with ( patch.object(builder, "prepare_m73_offset_template", side_effect=count_m73_template_calls), patch.object( builder, "write_output_3mf", side_effect=capture_gcode, ), ): result = builder.build_packed_3mf([PlateJob(source_3mf, 3)], options) self.assertEqual(result.plate_count, 3) self.assertEqual(m73_template_calls, 1) # 模板仅准备 1 次 # 3 份副本的 M73 剩余时间已分别偏移 self.assertIn("M73 P0 R6010", captured_gcode) self.assertIn("M73 P0 R12010", captured_gcode) self.assertIn("M73 P0 R18010", captured_gcode) @staticmethod def write_archive(path: Path) -> None: """创建包含带 M73 指令和待替换行的 plate_1 G-code 的 3MF 测试归档。""" slice_info = """ """ gcode = """G0 X128 F30000 G0 Y254 F3000 M73 P0 R10 ;=====printer finish sound========= """ with zipfile.ZipFile(path, "w") as archive: archive.writestr("Metadata/slice_info.config", slice_info) archive.writestr("Metadata/plate_1.gcode", gcode) if __name__ == "__main__": unittest.main()