summaryrefslogtreecommitdiff
path: root/tests/test_pipeline_factory.py
blob: 454a5b1f41e0621e762ed5e9dfc39737540c482e (plain)
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
from __future__ import annotations

from dataclasses import replace
from types import SimpleNamespace

import pytest

from mcp_sd15.config import LoRAConfig
from mcp_sd15.errors import ConfigError
from mcp_sd15.pipeline_factory import PipelineFactory


class FakePipeline:
    def __init__(self, *, with_vae: bool = True) -> None:
        self.vae = object() if with_vae else None
        self.scheduler = SimpleNamespace(config={})
        self.lora_loads: list[dict[str, object]] = []
        self.adapters: tuple[list[str], list[float]] | None = None
        self.device: str | None = None

    def load_lora_weights(self, directory: str, **kwargs: object) -> None:
        self.lora_loads.append({"directory": directory, **kwargs})

    def set_adapters(self, names: list[str], *, adapter_weights: list[float]) -> None:
        self.adapters = (names, adapter_weights)

    def set_progress_bar_config(self, *, disable: bool) -> None:
        assert disable is True

    def to(self, device: str) -> FakePipeline:
        self.device = device
        return self


class FakeScheduler:
    @classmethod
    def from_config(cls, config: dict[str, object]) -> object:
        return {"scheduler": cls.__name__, **config}


def fake_diffusers(pipeline: FakePipeline, external_vae: object) -> SimpleNamespace:
    class StableDiffusionPipeline:
        @classmethod
        def from_single_file(cls, path: str, **kwargs: object) -> FakePipeline:
            pipeline.base_load = {"path": path, **kwargs}  # type: ignore[attr-defined]
            return pipeline

    class AutoencoderKL:
        @classmethod
        def from_single_file(cls, path: str, **kwargs: object) -> object:
            pipeline.vae_load = {"path": path, **kwargs}  # type: ignore[attr-defined]
            return external_vae

    return SimpleNamespace(
        StableDiffusionPipeline=StableDiffusionPipeline,
        AutoencoderKL=AutoencoderKL,
        DPMSolverMultistepScheduler=FakeScheduler,
        DDIMScheduler=FakeScheduler,
        EulerDiscreteScheduler=FakeScheduler,
        EulerAncestralDiscreteScheduler=FakeScheduler,
    )


def test_base_pipeline_requires_vae_and_applies_all_loras(app_config, monkeypatch) -> None:
    model = replace(
        app_config.model,
        vae="external.safetensors",
        loras=(
            LoRAConfig("detail.safetensors", 0.8),
            LoRAConfig("style.safetensors", 0.5),
        ),
    )
    factory = PipelineFactory(replace(app_config, model=model))
    pipeline = FakePipeline()
    external_vae = object()
    runtime = (SimpleNamespace(float16="float16"), fake_diffusers(pipeline, external_vae))
    monkeypatch.setattr(factory, "_runtime", lambda: runtime)

    result = factory.create_base()

    assert result is pipeline
    assert pipeline.vae is external_vae
    assert pipeline.adapters == (["configured_lora_0", "configured_lora_1"], [0.8, 0.5])
    assert [item["weight_name"] for item in pipeline.lora_loads] == [
        "detail.safetensors",
        "style.safetensors",
    ]
    assert all(item["local_files_only"] is True for item in pipeline.lora_loads)
    assert pipeline.device == "cuda"


def test_checkpoint_without_vae_fails_pipeline_initialization(app_config, monkeypatch) -> None:
    factory = PipelineFactory(app_config)
    pipeline = FakePipeline(with_vae=False)
    runtime = (SimpleNamespace(float16="float16"), fake_diffusers(pipeline, object()))
    monkeypatch.setattr(factory, "_runtime", lambda: runtime)

    with pytest.raises(ConfigError, match="does not provide a VAE"):
        factory.create_base()