1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
from __future__ import annotations
from pathlib import Path
import pytest
from mcp_sd15.config import ConfigError, load_config
def make_models(root: Path) -> None:
for folder, names in {
"checkpoints": ["base.safetensors"],
"vae": ["vae.safetensors"],
"lora": ["a.safetensors", "b.safetensors"],
"controlnet": ["depth.safetensors", "pose.safetensors", "inpaint.safetensors"],
}.items():
destination = root / folder
destination.mkdir(parents=True)
for name in names:
(destination / name).touch()
def valid_toml(root: Path) -> str:
return f'''[model]
root = "{root.as_posix()}"
checkpoint = "base.safetensors"
vae = "vae.safetensors"
[[model.lora]]
file = "a.safetensors"
weight = 0.8
[[model.lora]]
file = "b.safetensors"
weight = -0.2
[generation]
steps = 20
cfg_scale = 6.5
scheduler = "euler_a"
default_size = "portrait_512"
[controlnet.depth]
enabled = true
model = "depth.safetensors"
preprocessor = "midas"
[controlnet.openpose]
enabled = true
model = "pose.safetensors"
[controlnet.inpaint]
enabled = true
model = "inpaint.safetensors"
[output]
enabled = true
directory = "generated"
'''
def test_loads_and_resolves_all_model_paths(tmp_path: Path) -> None:
root = tmp_path / "models"
make_models(root)
path = tmp_path / "config.toml"
path.write_text(valid_toml(root), encoding="utf-8")
config = load_config(path)
assert config.model.checkpoint_path() == root / "checkpoints" / "base.safetensors"
assert config.model.vae_path() == root / "vae" / "vae.safetensors"
assert config.model.lora_paths() == (
root / "lora" / "a.safetensors",
root / "lora" / "b.safetensors",
)
assert config.generation.default_size == "portrait_512"
assert config.output.directory == tmp_path / "generated"
assert config.controlnet_path("openpose_full") == root / "controlnet" / "pose.safetensors"
@pytest.mark.parametrize(
"line, message",
[
('checkpoint = "../base.safetensors"', "file name"),
("steps = 0", "steps"),
('scheduler = "unknown"', "scheduler"),
('default_size = "1024"', "default_size"),
('preprocessor = "zoe"', "preprocessor"),
],
)
def test_rejects_invalid_values(tmp_path: Path, line: str, message: str) -> None:
root = tmp_path / "models"
make_models(root)
text = valid_toml(root)
if line.startswith("checkpoint"):
text = text.replace('checkpoint = "base.safetensors"', line)
elif line.startswith("steps"):
text = text.replace("steps = 20", line)
elif line.startswith("scheduler"):
text = text.replace('scheduler = "euler_a"', line)
elif line.startswith("default_size"):
text = text.replace('default_size = "portrait_512"', line)
else:
text = text.replace('preprocessor = "midas"', line)
path = tmp_path / "bad.toml"
path.write_text(text, encoding="utf-8")
with pytest.raises(ConfigError, match=message):
load_config(path)
def test_missing_model_fails_startup_validation(tmp_path: Path) -> None:
root = tmp_path / "models"
make_models(root)
(root / "controlnet" / "depth.safetensors").unlink()
path = tmp_path / "config.toml"
path.write_text(valid_toml(root), encoding="utf-8")
with pytest.raises(ConfigError, match=r"depth\.safetensors"):
load_config(path)
def test_can_skip_path_checks_for_schema_only_use(tmp_path: Path) -> None:
path = tmp_path / "config.toml"
path.write_text(
'[model]\nroot = "missing"\ncheckpoint = "base.safetensors"\n', encoding="utf-8"
)
config = load_config(path, validate_paths=False)
assert config.model.root == (tmp_path / "missing").resolve()
|