Files
ModelTrainEval/tests/test_config.py
T
Shaun ZhouandClaude Opus 5 e581d9b3d7 fix(backbones): 修复 shufflenet 无法构建,并让换骨架不再踩配置合并的坑
- shufflenet_v2_x1_0 列在支持清单里却直接 AttributeError:ShuffleNetV2 既不是
  扁平 children 也没有 .features,需要按 conv1/maxpool/stage2..4/conv5 自己拼。
  该 bug 同时影响 SimCLR 与检测模型
- 新增参数化测试覆盖清单里的每个 arch:能前向、声明的 out_channels 与实际一致
- 配置支持 `_replace_: true`:子字典整块替换而不与 base 合并。换骨架时
  tiny_conv 的 widths/depths 会残留并传给新骨架,此前无论用 --set 还是 _base_
  都绕不开,只能改原文件
- Registry.build 提前校验字段,报错直接指出是哪些字段不被接受、该怎么办,
  取代裸 TypeError
- README 补充可选骨架表与三种改法(含不该用 --set 改 name 的说明)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 15:28:49 +08:00

86 lines
3.2 KiB
Python

import pytest
pytest.importorskip("yaml")
from utils.config import Config, apply_overrides, deep_merge, load_config, parse_override # noqa: E402
def test_deep_merge_is_recursive():
merged = deep_merge({"a": {"b": 1, "c": 2}}, {"a": {"c": 3}, "d": 4})
assert merged == {"a": {"b": 1, "c": 3}, "d": 4}
def test_parse_override_literals():
assert parse_override("train.lr=1e-3") == ("train.lr", 1e-3)
assert parse_override("train.amp=False") == ("train.amp", False)
assert parse_override("model.backbone.name=tiny_conv") == ("model.backbone.name", "tiny_conv")
def test_apply_overrides_creates_missing_nodes():
cfg = apply_overrides({}, ["a.b.c=1"])
assert cfg == {"a": {"b": {"c": 1}}}
def test_load_config_with_base(tmp_path):
(tmp_path / "base.yaml").write_text("train:\n lr: 0.1\n epochs: 10\n", encoding="utf-8")
(tmp_path / "child.yaml").write_text(
"_base_: ./base.yaml\ntrain:\n lr: 0.01\nwork_dir: runs/x\n", encoding="utf-8"
)
cfg = load_config(tmp_path / "child.yaml", ["train.epochs=2"])
assert isinstance(cfg, Config)
assert cfg.train.lr == 0.01
assert cfg.train.epochs == 2
assert cfg.get_path("work_dir") == "runs/x"
def test_load_config_detects_cycle(tmp_path):
(tmp_path / "a.yaml").write_text("_base_: ./b.yaml\n", encoding="utf-8")
(tmp_path / "b.yaml").write_text("_base_: ./a.yaml\n", encoding="utf-8")
with pytest.raises(ValueError, match="循环引用"):
load_config(tmp_path / "a.yaml")
def test_project_configs_are_loadable():
cfg = load_config("configs/train/baseline.yaml")
assert cfg["model"]["name"] == "defect_net"
assert cfg["data"]["train"]["dataset"]["name"] == "film_defect"
assert len(cfg["classes"]) > 0
def test_replace_key_swaps_a_whole_subdict(tmp_path):
"""换组件时要能整块替换,否则旧组件的专有字段会残留下来传给新组件。"""
(tmp_path / "base.yaml").write_text(
"backbone:\n name: tiny_conv\n widths: [32, 64]\n depths: [1, 2]\n block: dw\n",
encoding="utf-8",
)
(tmp_path / "child.yaml").write_text(
"_base_: ./base.yaml\nbackbone:\n _replace_: true\n name: torchvision\n arch: resnet18\n",
encoding="utf-8",
)
cfg = load_config(tmp_path / "child.yaml")
assert cfg["backbone"] == {"name": "torchvision", "arch": "resnet18"}
assert "_replace_" not in cfg["backbone"] # 标记本身不能漏进构造参数
def test_without_replace_key_subdicts_still_merge(tmp_path):
(tmp_path / "base.yaml").write_text("backbone:\n name: tiny_conv\n block: dw\n", encoding="utf-8")
(tmp_path / "child.yaml").write_text(
"_base_: ./base.yaml\nbackbone:\n block: res\n", encoding="utf-8"
)
cfg = load_config(tmp_path / "child.yaml")
assert cfg["backbone"] == {"name": "tiny_conv", "block": "res"}
def test_registry_reports_incompatible_fields_actionably():
"""--set 只改 name 时旧字段会残留,报错要说清楚是哪些字段、该怎么办。"""
import pytest
from models.registry import BACKBONES
with pytest.raises(TypeError, match=r"不接受的字段.*widths"):
BACKBONES.build({"name": "torchvision", "arch": "resnet18", "widths": [32]})