The wording fix a commit ago was written by hand in two files and the same mislabel was still sitting in four others -- traits, chain, modifier install and node schema all reported a thing that loaded, recognised its work and then raised as one that "did not load", sending a reader after an import error that never happened. Three hand-written strings would have drifted, so log.broke() now says it once and every site calls it. Two of the new tests were vacuous when first written -- "nothing said did not load" is also true where nothing ran at all, and picking the first provider found lands on one whose traits exclude the test's model. Both assert the line is present before asserting the wrong line is not. One finding, confirmed, plus four sites it did not name.
225 lines
7.4 KiB
Python
225 lines
7.4 KiB
Python
"""Traits: what is read off a model, and what a model's own module contributes.
|
|
|
|
The split under test is the one the architecture depends on -- core reads only
|
|
facts that need no knowledge of any particular model, and everything else is
|
|
announced. If core ever learned a model's name, supporting the next one would
|
|
mean editing core, which is the whole thing this avoids.
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from core.contract import ModuleSpec
|
|
from core.traits import contributed, has_block, missing_for, split, traits_of, universal
|
|
|
|
|
|
class _LatentFormat:
|
|
def __init__(self, dims, tdr=1):
|
|
self.latent_dimensions = dims
|
|
self.temporal_downscale_ratio = tdr
|
|
|
|
|
|
class _Kind:
|
|
def __init__(self, name):
|
|
self.name = name
|
|
|
|
|
|
class _Model:
|
|
"""The shape traits_of() reads: patcher -> model -> model_config."""
|
|
|
|
def __init__(self, dims=3, tdr=4, kind="FLOW"):
|
|
outer = self
|
|
|
|
class Inner:
|
|
model_type = _Kind(kind)
|
|
|
|
class model_config:
|
|
latent_format = _LatentFormat(dims, tdr)
|
|
|
|
self.model = Inner()
|
|
del outer
|
|
|
|
|
|
def test_latent_rank_is_read_not_guessed():
|
|
assert "temporal_latent" in universal(_Model(dims=3))
|
|
assert "spatial_latent" in universal(_Model(dims=2))
|
|
assert "waveform_latent" in universal(_Model(dims=1))
|
|
|
|
|
|
def test_the_prediction_type_is_named():
|
|
assert "predict_flow" in universal(_Model(kind="FLOW"))
|
|
assert "predict_eps" in universal(_Model(kind="EPS"))
|
|
|
|
|
|
def test_an_unreadable_model_yields_nothing_rather_than_raising():
|
|
class Bare:
|
|
model = None
|
|
|
|
assert traits_of(Bare()) == []
|
|
|
|
|
|
def test_a_module_contributes_traits_core_could_not_read():
|
|
spec = ModuleSpec(id="m", title="M", mount="", traits=lambda model: ["audio_stream"])
|
|
assert "audio_stream" in traits_of(_Model(), [spec])
|
|
|
|
|
|
def test_a_provider_that_raises_does_not_silence_the_others():
|
|
def explodes(model):
|
|
raise RuntimeError("boom")
|
|
|
|
bad = ModuleSpec(id="bad", title="B", mount="", traits=explodes)
|
|
good = ModuleSpec(id="good", title="G", mount="", traits=lambda m: ["audio_stream"])
|
|
|
|
found = traits_of(_Model(), [bad, good])
|
|
assert "audio_stream" in found, "one bad provider disabled every other module"
|
|
assert "temporal_latent" in found
|
|
|
|
|
|
def test_a_provider_that_raises_is_not_reported_as_one_that_never_loaded():
|
|
"""The log line, not only the surviving traits.
|
|
|
|
A traits provider checks whether the model is its own FIRST, so reaching the
|
|
exception means it loaded, recognised the model and then broke. Saying "did
|
|
not load" sends a reader after an import error that never happened -- and
|
|
losing a model's traits quietly narrows which modifiers are considered
|
|
compatible, so the log is the only thing pointing at the cause.
|
|
"""
|
|
from core import log
|
|
|
|
def explodes(model):
|
|
raise RuntimeError("adaln lookup exploded")
|
|
|
|
log._reset()
|
|
traits_of(_Model(), [ModuleSpec(id="bad", title="B", mount="", traits=explodes)])
|
|
|
|
said = log.history()
|
|
assert said, "a provider broke and nothing was said"
|
|
assert not any("did not load" in r["message"] for r in said), \
|
|
[r["message"] for r in said]
|
|
assert any("failed while" in r["message"] for r in said), [r["message"] for r in said]
|
|
assert said[-1]["level"] == log.WARNING
|
|
|
|
|
|
def test_declaring_no_traits_means_compatible_with_everything():
|
|
# Narrowing, not opting in -- the property that lets a modifier reach models
|
|
# FunPack knows nothing about.
|
|
plain = ModuleSpec(id="plain", title="P", mount="")
|
|
compatible, incompatible = split([plain], [])
|
|
assert compatible == [plain] and incompatible == []
|
|
|
|
|
|
def test_declaring_a_trait_excludes_the_models_that_lack_it():
|
|
picky = ModuleSpec(id="picky", title="P", mount="", requires=["audio_stream"])
|
|
compatible, incompatible = split([picky], ["temporal_latent"])
|
|
assert incompatible == [picky]
|
|
assert missing_for(picky, ["temporal_latent"]) == ["audio_stream"]
|
|
|
|
|
|
def test_a_structural_probe_finds_a_block_by_class_name():
|
|
class Target:
|
|
pass
|
|
|
|
class Root:
|
|
def modules(self):
|
|
return [self, Target()]
|
|
|
|
class Model:
|
|
class model:
|
|
diffusion_model = Root()
|
|
|
|
# The question a modifier actually has: does this model have the thing I
|
|
# touch -- not "is this the model I was written for".
|
|
assert has_block(Model(), "Target")
|
|
assert not has_block(Model(), "SomethingElse")
|
|
|
|
|
|
def test_a_probe_on_a_model_with_no_module_tree_is_false_not_fatal():
|
|
class Model:
|
|
model = None
|
|
|
|
assert has_block(Model(), "Anything") is False
|
|
|
|
|
|
# --- against ComfyUI's real model configs ---------------------------------
|
|
|
|
@pytest.fixture(scope="module")
|
|
def real_configs(comfyui):
|
|
import comfy.supported_models as sm
|
|
return sm.models
|
|
|
|
|
|
def test_every_real_model_config_can_be_read(real_configs):
|
|
# The point is coverage, not a particular answer: whatever ships upstream,
|
|
# reading it must not raise and must not invent anything.
|
|
known = {"temporal_latent", "spatial_latent", "waveform_latent",
|
|
"temporal_compression"}
|
|
for cfg in real_configs:
|
|
model = _Model.__new__(_Model)
|
|
|
|
class Inner:
|
|
model_type = None
|
|
model_config = cfg
|
|
|
|
model.model = Inner()
|
|
found = universal(model)
|
|
assert found <= known, f"{cfg.__name__} produced an unexpected trait: {found - known}"
|
|
|
|
|
|
def test_traits_describe_the_tensor_not_an_assumed_modality(real_configs):
|
|
"""QwenImage is an IMAGE model with a 3-D latent. LTXV is a video model with
|
|
a 3-D latent. They must read the same, because the latent really is the same
|
|
shape -- claiming otherwise would be FunPack guessing at modality, which
|
|
ComfyUI itself does not classify."""
|
|
by_name = {cfg.__name__: cfg for cfg in real_configs}
|
|
qwen, ltxv = by_name.get("QwenImage"), by_name.get("LTXV")
|
|
if qwen is None or ltxv is None:
|
|
pytest.skip("upstream renamed these configs")
|
|
|
|
def read(cfg):
|
|
model = _Model.__new__(_Model)
|
|
|
|
class Inner:
|
|
model_type = None
|
|
model_config = cfg
|
|
|
|
model.model = Inner()
|
|
return universal(model)
|
|
|
|
assert "temporal_latent" in read(qwen)
|
|
assert "temporal_latent" in read(ltxv)
|
|
|
|
|
|
def test_a_bare_probe_name_collides_across_architectures():
|
|
"""`Attention`, `MLP`, `Block` are not distinctive. A bare name matches any
|
|
of them, which is why a probe that needs to be sure must qualify."""
|
|
|
|
class Root:
|
|
def modules(self):
|
|
# A class named Attention, but from an unrelated architecture.
|
|
other = type("Attention", (), {})
|
|
other.__module__ = "somewhere.else.entirely"
|
|
return [self, other()]
|
|
|
|
class Model:
|
|
class model:
|
|
diffusion_model = Root()
|
|
|
|
assert has_block(Model(), "Attention") # the collision, unqualified
|
|
assert not has_block(Model(), "comfy.ldm.minimax.model.Attention")
|
|
|
|
|
|
def test_a_qualified_probe_matches_its_own_architecture():
|
|
class Root:
|
|
def modules(self):
|
|
target = type("AdalnProj", (), {})
|
|
target.__module__ = "comfy.ldm.minimax.model"
|
|
return [self, target()]
|
|
|
|
class Model:
|
|
class model:
|
|
diffusion_model = Root()
|
|
|
|
assert has_block(Model(), "comfy.ldm.minimax.model.AdalnProj")
|
|
assert has_block(Model(), "minimax.model.AdalnProj") # tail match
|
|
assert has_block(Model(), "AdalnProj") # distinctive enough
|
|
assert not has_block(Model(), "other.pack.AdalnProj")
|