"""测试独立批量构建(Individual Batch Build)的任务调度与工作器计数。 验证内容: - 工作器数量根据任务数和 CPU 核心数合理计算 - 串行执行器保持成功任务顺序,同时收集失败任务信息 """ from __future__ import annotations import unittest from pathlib import Path from unittest.mock import patch import a1_swap_mod_packer.batch as batch from a1_swap_mod_packer.models import BuildOptions, BuildResult, PlateJob class IndividualBatchBuildTest(unittest.TestCase): """测试独立批量构建的工作器计数和运行器行为。""" def test_worker_count_uses_task_count_and_caps_cpu_count(self) -> None: """验证工作器数量 = min(任务数, CPU 核心数),且受上下限约束。""" self.assertEqual(batch.individual_batch_worker_count(0), 0) # 无任务 → 0 个工作器 self.assertEqual(batch.individual_batch_worker_count(3, max_workers=99), 3) # 任务数 < 上限 → 按任务数 self.assertEqual(batch.individual_batch_worker_count(3, max_workers=0), 1) # 上限为 0 → 退化为 1 with patch.object(batch.os, "cpu_count", return_value=32): # 任务数 20,CPU 32 核,上限 = INDIVIDUAL_BATCH_MAX_WORKERS self.assertEqual(batch.individual_batch_worker_count(20), batch.INDIVIDUAL_BATCH_MAX_WORKERS) with patch.object(batch.os, "cpu_count", return_value=None): # cpu_count 返回 None → 回退为 1 self.assertEqual(batch.individual_batch_worker_count(20), 1) def test_serial_runner_keeps_success_order_and_collects_failures(self) -> None: """验证串行执行器按提交顺序返回成功结果,并收集失败任务。""" # 准备 3 个任务,其中第 2 个计划失败 tasks = [ self.task("one.3mf", "one.out.3mf", copies=2), self.task("fail.3mf", "fail.out.3mf", copies=3), self.task("two.3mf", "two.out.3mf", copies=4), ] def fake_build(jobs: list[PlateJob], options: BuildOptions) -> BuildResult: """模拟构建:fail.3mf 触发 ValueError,其余正常返回。""" job = jobs[0] if job.source_3mf.name == "fail.3mf": raise ValueError("planned failure") return BuildResult( output_3mf=options.output_3mf, plate_count=job.copies, total_prediction_seconds=None, total_weight_grams=None, gcode_md5=job.source_3mf.stem, ) # 使用单工作器串行运行,验证顺序与失败收集 with patch.object(batch, "build_packed_3mf", side_effect=fake_build): result = batch.run_individual_batch_builds(tasks, max_workers=1) self.assertEqual(result.worker_count, 1) # 成功结果按原始顺序排列,跳过失败项 self.assertEqual( [item.output_3mf.name for item in result.results], ["one.out.3mf", "two.out.3mf"], ) self.assertEqual([item.plate_count for item in result.results], [2, 4]) # 失败详情:索引、任务源文件名、错误信息 self.assertEqual(len(result.failures), 1) self.assertEqual(result.failures[0].index, 1) self.assertEqual(result.failures[0].job.source_3mf.name, "fail.3mf") self.assertIn("planned failure", result.failures[0].error) @staticmethod def task(source_name: str, output_name: str, copies: int) -> batch.IndividualBuildTask: """构造独立构建任务辅助方法。 Args: source_name: 源 3MF 文件名 output_name: 输出 3MF 文件名 copies: 打印板份数 Returns: 包装好的 IndividualBuildTask """ return batch.IndividualBuildTask( PlateJob(Path(source_name), copies), BuildOptions(swap_gcode=Path("swap.gcode"), output_3mf=Path(output_name)), ) if __name__ == "__main__": unittest.main()