Files

564 lines
19 KiB
Python

"""Framework-independent capability declaration and compatibility tests."""
from __future__ import annotations
from dataclasses import FrozenInstanceError, replace
from typing import Any, cast
import pytest
from comfyui_sigmax.core import (
CapabilityCompatibilityError,
CapabilityDimension,
CompatibilityDecision,
CompatibilityLevel,
CompatibilityReason,
ExecutionBehavior,
ExecutionFeatureRequest,
ModelCapabilities,
NoiseOwnership,
PredictionType,
ProfileCapabilities,
SamplerCapabilities,
SamplerState,
ScheduleContractError,
ScheduleOwnership,
SigmaDomain,
TerminalRequirement,
TerminalSigma,
evaluate_compatibility,
require_compatible,
)
def _model() -> ModelCapabilities:
return ModelCapabilities(
model_family="krea2",
model_variant="turbo",
accepted_prediction_types=(PredictionType.FLOW_VELOCITY,),
accepted_sigma_domains=(SigmaDomain.UNIT_FLOW,),
accepted_ownerships=(ScheduleOwnership.EXTERNAL_SIGMAS,),
supports_partial_denoise=True,
supports_per_token_timesteps=False,
)
def _profile() -> ProfileCapabilities:
return ProfileCapabilities(
profile_id="krea2.turbo.official",
profile_version="1",
model_family="krea2",
model_variant="turbo",
prediction_type=PredictionType.FLOW_VELOCITY,
sigma_domain=SigmaDomain.UNIT_FLOW,
ownership=ScheduleOwnership.EXTERNAL_SIGMAS,
terminal_sigma=TerminalSigma.ZERO,
allowed_execution_behaviors=(ExecutionBehavior.DETERMINISTIC,),
allowed_noise_ownerships=(NoiseOwnership.NONE,),
allowed_sampler_state=(SamplerState.STEP_INDEX,),
supports_partial_denoise=True,
supports_per_token_timesteps=False,
reference_sampler_ids=("comfy.euler",),
)
def _sampler() -> SamplerCapabilities:
return SamplerCapabilities(
sampler_id="comfy.euler",
sampler_version="native",
accepted_prediction_types=(PredictionType.FLOW_VELOCITY,),
accepted_sigma_domains=(SigmaDomain.UNIT_FLOW,),
accepted_ownerships=(ScheduleOwnership.EXTERNAL_SIGMAS,),
terminal_requirement=TerminalRequirement.REQUIRES_ZERO,
execution_behavior=ExecutionBehavior.DETERMINISTIC,
noise_ownership=NoiseOwnership.NONE,
required_state=(),
supports_partial_denoise=True,
supports_per_token_timesteps=False,
)
def _decision(
*,
model: ModelCapabilities | None = None,
profile: ProfileCapabilities | None = None,
sampler: SamplerCapabilities | None = None,
request: ExecutionFeatureRequest | None = None,
) -> CompatibilityDecision:
return evaluate_compatibility(
model=model or _model(),
profile=profile or _profile(),
sampler=sampler or _sampler(),
request=request or ExecutionFeatureRequest(),
)
def test_krea_like_native_euler_capabilities_allow_execution() -> None:
decision = _decision()
assert decision.level is CompatibilityLevel.ALLOW
assert decision.reasons == (CompatibilityReason.COMPATIBLE,)
assert decision.considered == tuple(CapabilityDimension)
assert require_compatible(decision) is decision
def test_capability_compatible_nonreference_sampler_warns() -> None:
decision = _decision(sampler=replace(_sampler(), sampler_id="other.euler"))
assert decision.level is CompatibilityLevel.WARN
assert decision.reasons == (CompatibilityReason.SAMPLER_NOT_PROFILE_REFERENCE,)
assert require_compatible(decision) is decision
@pytest.mark.parametrize(
("terminal", "requirement"),
[
(TerminalSigma.ZERO, TerminalRequirement.ACCEPTS_EITHER),
(TerminalSigma.NONZERO, TerminalRequirement.ACCEPTS_EITHER),
(TerminalSigma.NONZERO, TerminalRequirement.FORBIDS_ZERO),
],
)
def test_compatible_terminal_policies_allow(
terminal: TerminalSigma,
requirement: TerminalRequirement,
) -> None:
decision = _decision(
profile=replace(_profile(), terminal_sigma=terminal),
sampler=replace(_sampler(), terminal_requirement=requirement),
)
assert decision.level is CompatibilityLevel.ALLOW
@pytest.mark.parametrize(
("model", "profile", "sampler", "reason"),
[
(
replace(_model(), model_family="other"),
_profile(),
_sampler(),
CompatibilityReason.MODEL_FAMILY_MISMATCH,
),
(
replace(_model(), model_variant="raw"),
_profile(),
_sampler(),
CompatibilityReason.MODEL_VARIANT_MISMATCH,
),
(
replace(
_model(),
accepted_prediction_types=(PredictionType.EPSILON,),
),
_profile(),
_sampler(),
CompatibilityReason.MODEL_PREDICTION_UNSUPPORTED,
),
(
replace(
_model(),
accepted_sigma_domains=(SigmaDomain.CONTINUOUS_EDM,),
),
_profile(),
_sampler(),
CompatibilityReason.MODEL_SIGMA_DOMAIN_UNSUPPORTED,
),
(
replace(
_model(),
accepted_ownerships=(ScheduleOwnership.MODEL_NATIVE,),
),
_profile(),
_sampler(),
CompatibilityReason.MODEL_OWNERSHIP_UNSUPPORTED,
),
(
_model(),
_profile(),
replace(
_sampler(),
accepted_prediction_types=(PredictionType.EPSILON,),
),
CompatibilityReason.SAMPLER_PREDICTION_UNSUPPORTED,
),
(
_model(),
_profile(),
replace(
_sampler(),
accepted_sigma_domains=(SigmaDomain.CONTINUOUS_EDM,),
),
CompatibilityReason.SAMPLER_SIGMA_DOMAIN_UNSUPPORTED,
),
(
_model(),
_profile(),
replace(
_sampler(),
accepted_ownerships=(ScheduleOwnership.MODEL_NATIVE,),
),
CompatibilityReason.SAMPLER_OWNERSHIP_UNSUPPORTED,
),
(
_model(),
_profile(),
replace(
_sampler(),
terminal_requirement=TerminalRequirement.FORBIDS_ZERO,
),
CompatibilityReason.TERMINAL_REQUIREMENT_MISMATCH,
),
(
_model(),
_profile(),
replace(
_sampler(),
execution_behavior=ExecutionBehavior.STOCHASTIC,
noise_ownership=NoiseOwnership.SAMPLER,
),
CompatibilityReason.EXECUTION_BEHAVIOR_MISMATCH,
),
(
_model(),
replace(
_profile(),
allowed_execution_behaviors=(
ExecutionBehavior.DETERMINISTIC,
ExecutionBehavior.STOCHASTIC,
),
allowed_noise_ownerships=(NoiseOwnership.NONE, NoiseOwnership.CALLER),
),
replace(
_sampler(),
execution_behavior=ExecutionBehavior.STOCHASTIC,
noise_ownership=NoiseOwnership.SAMPLER,
),
CompatibilityReason.NOISE_OWNERSHIP_MISMATCH,
),
(
_model(),
_profile(),
replace(_sampler(), required_state=(SamplerState.MULTISTEP_HISTORY,)),
CompatibilityReason.SAMPLER_STATE_UNSUPPORTED,
),
],
)
def test_each_model_profile_sampler_semantic_mismatch_rejects(
model: ModelCapabilities,
profile: ProfileCapabilities,
sampler: SamplerCapabilities,
reason: CompatibilityReason,
) -> None:
decision = _decision(model=model, profile=profile, sampler=sampler)
assert decision.level is CompatibilityLevel.REJECT
assert reason in decision.reasons
with pytest.raises(CapabilityCompatibilityError, match=reason.value):
require_compatible(decision)
@pytest.mark.parametrize(
("feature", "component", "reason"),
[
(
"partial",
"model",
CompatibilityReason.PARTIAL_DENOISE_UNSUPPORTED_BY_MODEL,
),
(
"partial",
"profile",
CompatibilityReason.PARTIAL_DENOISE_UNSUPPORTED_BY_PROFILE,
),
(
"partial",
"sampler",
CompatibilityReason.PARTIAL_DENOISE_UNSUPPORTED_BY_SAMPLER,
),
(
"per_token",
"model",
CompatibilityReason.PER_TOKEN_TIMESTEPS_UNSUPPORTED_BY_MODEL,
),
(
"per_token",
"profile",
CompatibilityReason.PER_TOKEN_TIMESTEPS_UNSUPPORTED_BY_PROFILE,
),
(
"per_token",
"sampler",
CompatibilityReason.PER_TOKEN_TIMESTEPS_UNSUPPORTED_BY_SAMPLER,
),
],
)
def test_requested_execution_feature_requires_three_party_support(
feature: str,
component: str,
reason: CompatibilityReason,
) -> None:
model = _model()
profile = _profile()
sampler = _sampler()
if feature == "partial":
request = ExecutionFeatureRequest(use_partial_denoise=True)
if component == "model":
model = replace(model, supports_partial_denoise=False)
elif component == "profile":
profile = replace(profile, supports_partial_denoise=False)
else:
sampler = replace(sampler, supports_partial_denoise=False)
else:
request = ExecutionFeatureRequest(use_per_token_timesteps=True)
model = replace(model, supports_per_token_timesteps=True)
profile = replace(profile, supports_per_token_timesteps=True)
sampler = replace(sampler, supports_per_token_timesteps=True)
if component == "model":
model = replace(model, supports_per_token_timesteps=False)
elif component == "profile":
profile = replace(profile, supports_per_token_timesteps=False)
else:
sampler = replace(sampler, supports_per_token_timesteps=False)
decision = _decision(model=model, profile=profile, sampler=sampler, request=request)
assert decision.level is CompatibilityLevel.REJECT
assert reason in decision.reasons
def test_supported_partial_and_per_token_features_allow() -> None:
request = ExecutionFeatureRequest(
use_partial_denoise=True,
use_per_token_timesteps=True,
)
decision = _decision(
model=replace(_model(), supports_per_token_timesteps=True),
profile=replace(_profile(), supports_per_token_timesteps=True),
sampler=replace(_sampler(), supports_per_token_timesteps=True),
request=request,
)
assert decision.level is CompatibilityLevel.ALLOW
def test_multiple_rejections_have_stable_evaluator_order_and_no_warning() -> None:
decision = _decision(
model=replace(
_model(),
model_family="other",
accepted_prediction_types=(PredictionType.EPSILON,),
supports_partial_denoise=False,
),
sampler=replace(_sampler(), sampler_id="other.euler", supports_partial_denoise=False),
request=ExecutionFeatureRequest(use_partial_denoise=True),
)
assert decision.level is CompatibilityLevel.REJECT
assert decision.reasons == (
CompatibilityReason.MODEL_FAMILY_MISMATCH,
CompatibilityReason.MODEL_PREDICTION_UNSUPPORTED,
CompatibilityReason.PARTIAL_DENOISE_UNSUPPORTED_BY_MODEL,
CompatibilityReason.PARTIAL_DENOISE_UNSUPPORTED_BY_SAMPLER,
)
def test_rejected_gate_stops_before_execution_sentinel() -> None:
executed: list[bool] = []
decision = _decision(
sampler=replace(
_sampler(),
accepted_sigma_domains=(SigmaDomain.CONTINUOUS_EDM,),
)
)
def guarded_execution() -> None:
require_compatible(decision)
executed.append(True)
with pytest.raises(CapabilityCompatibilityError):
guarded_execution()
assert executed == []
def test_capability_contracts_are_frozen() -> None:
model = _model()
decision = _decision()
with pytest.raises(FrozenInstanceError):
model.model_variant = "raw" # type: ignore[misc]
with pytest.raises(FrozenInstanceError):
decision.level = CompatibilityLevel.REJECT # type: ignore[misc]
@pytest.mark.parametrize(
"factory",
[
lambda: replace(_model(), model_family=""),
lambda: replace(_model(), model_family="Krea2"),
lambda: replace(_model(), model_family="x" * 257),
lambda: replace(_model(), supports_partial_denoise=cast(Any, 1)),
lambda: replace(_model(), accepted_prediction_types=cast(Any, [])),
lambda: replace(_model(), accepted_prediction_types=()),
lambda: replace(
_model(),
accepted_prediction_types=(
PredictionType.FLOW_VELOCITY,
PredictionType.FLOW_VELOCITY,
),
),
lambda: replace(
_model(),
accepted_prediction_types=(
PredictionType.EPSILON,
PredictionType.FLOW_VELOCITY,
),
),
lambda: replace(
_model(),
accepted_prediction_types=cast(Any, ("flow_velocity",)),
),
lambda: replace(_profile(), reference_sampler_ids=("",)),
lambda: replace(_profile(), reference_sampler_ids=cast(Any, [])),
lambda: replace(_profile(), profile_version="x" * 257),
lambda: replace(_profile(), prediction_type=cast(Any, "flow_velocity")),
lambda: replace(_profile(), sigma_domain=cast(Any, "unit_flow")),
lambda: replace(_profile(), ownership=cast(Any, "external_sigmas")),
lambda: replace(_profile(), terminal_sigma=cast(Any, "zero")),
lambda: replace(
_profile(),
reference_sampler_ids=("comfy.euler", "comfy.euler"),
),
lambda: replace(
_profile(),
reference_sampler_ids=("z.euler", "a.euler"),
),
lambda: replace(
_profile(),
allowed_execution_behaviors=(ExecutionBehavior.DETERMINISTIC,),
allowed_noise_ownerships=(NoiseOwnership.SAMPLER,),
),
lambda: replace(
_profile(),
allowed_execution_behaviors=(ExecutionBehavior.STOCHASTIC,),
allowed_noise_ownerships=(NoiseOwnership.NONE,),
),
lambda: replace(
_profile(),
allowed_execution_behaviors=(
ExecutionBehavior.DETERMINISTIC,
ExecutionBehavior.STOCHASTIC,
),
allowed_noise_ownerships=(NoiseOwnership.NONE,),
),
lambda: replace(
_profile(),
allowed_execution_behaviors=(ExecutionBehavior.DETERMINISTIC,),
allowed_noise_ownerships=(NoiseOwnership.NONE, NoiseOwnership.CALLER),
),
lambda: replace(
_sampler(),
terminal_requirement=cast(Any, "requires_zero"),
),
lambda: replace(
_sampler(),
execution_behavior=cast(Any, "deterministic"),
),
lambda: replace(
_sampler(),
noise_ownership=cast(Any, "none"),
),
lambda: replace(
_sampler(),
execution_behavior=ExecutionBehavior.DETERMINISTIC,
noise_ownership=NoiseOwnership.SAMPLER,
),
lambda: replace(
_sampler(),
execution_behavior=ExecutionBehavior.STOCHASTIC,
noise_ownership=NoiseOwnership.NONE,
),
lambda: ExecutionFeatureRequest(use_partial_denoise=cast(Any, 1)),
],
)
def test_invalid_capability_declarations_fail_closed(factory: Any) -> None:
with pytest.raises(ScheduleContractError):
factory()
@pytest.mark.parametrize(
"factory",
[
lambda: CompatibilityDecision(
level=CompatibilityLevel.ALLOW,
considered=tuple(CapabilityDimension),
reasons=(CompatibilityReason.SAMPLER_NOT_PROFILE_REFERENCE,),
),
lambda: CompatibilityDecision(
level=CompatibilityLevel.WARN,
considered=tuple(CapabilityDimension),
reasons=(CompatibilityReason.COMPATIBLE,),
),
lambda: CompatibilityDecision(
level=CompatibilityLevel.REJECT,
considered=tuple(CapabilityDimension),
reasons=(CompatibilityReason.SAMPLER_NOT_PROFILE_REFERENCE,),
),
lambda: CompatibilityDecision(
level=CompatibilityLevel.REJECT,
considered=tuple(CapabilityDimension),
reasons=(
CompatibilityReason.MODEL_FAMILY_MISMATCH,
CompatibilityReason.MODEL_FAMILY_MISMATCH,
),
),
lambda: CompatibilityDecision(
level=CompatibilityLevel.REJECT,
considered=(),
reasons=(CompatibilityReason.MODEL_FAMILY_MISMATCH,),
),
lambda: CompatibilityDecision(
level=cast(Any, "allow"),
considered=tuple(CapabilityDimension),
reasons=(CompatibilityReason.COMPATIBLE,),
),
lambda: CompatibilityDecision(
level=CompatibilityLevel.REJECT,
considered=cast(Any, list(CapabilityDimension)),
reasons=(CompatibilityReason.MODEL_FAMILY_MISMATCH,),
),
lambda: CompatibilityDecision(
level=CompatibilityLevel.REJECT,
considered=tuple(reversed(tuple(CapabilityDimension))),
reasons=(CompatibilityReason.MODEL_FAMILY_MISMATCH,),
),
lambda: CompatibilityDecision(
level=CompatibilityLevel.REJECT,
considered=tuple(CapabilityDimension)[:-1],
reasons=(CompatibilityReason.MODEL_FAMILY_MISMATCH,),
),
],
)
def test_invalid_compatibility_decisions_fail_closed(factory: Any) -> None:
with pytest.raises(ScheduleContractError):
factory()
def test_evaluator_rejects_noncontract_arguments() -> None:
valid_arguments = {
"model": _model(),
"profile": _profile(),
"sampler": _sampler(),
"request": ExecutionFeatureRequest(),
}
for name, expected in (
("model", "ModelCapabilities"),
("profile", "ProfileCapabilities"),
("sampler", "SamplerCapabilities"),
("request", "ExecutionFeatureRequest"),
):
invalid_arguments = {**valid_arguments, name: object()}
with pytest.raises(ScheduleContractError, match=expected):
evaluate_compatibility(**cast(Any, invalid_arguments))
with pytest.raises(ScheduleContractError, match="CompatibilityDecision"):
require_compatible(cast(Any, object()))