222 lines
6.6 KiB
Python
222 lines
6.6 KiB
Python
# SimpleSyrup - workflow-focused ComfyUI extensions for image generation
|
|
# Copyright (C) 2026 Artificial Sweetener and contributors
|
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
"""Verify immutable spatial view and view-major layout invariants."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import FrozenInstanceError
|
|
|
|
import pytest
|
|
|
|
from simple_syrup.domain.spatial_views import (
|
|
SpatialBatchLayout,
|
|
SpatialView,
|
|
SpatialViewKind,
|
|
)
|
|
|
|
|
|
def test_spatial_view_records_source_and_model_geometry_immutably() -> None:
|
|
"""Expose exact rectangle edges while preventing mutation."""
|
|
|
|
view = _tile(x=4, y=8, width=16, height=12, model_width=8, model_height=6)
|
|
|
|
assert view.source_right == 20
|
|
assert view.source_bottom == 20
|
|
with pytest.raises(FrozenInstanceError):
|
|
view.source_x = 0 # type: ignore[misc]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"values",
|
|
[
|
|
{"source_x": -1},
|
|
{"source_y": -1},
|
|
{"source_width": 0},
|
|
{"source_height": 0},
|
|
{"model_width": 0},
|
|
{"model_height": 0},
|
|
],
|
|
)
|
|
def test_spatial_view_rejects_invalid_geometry(values: dict[str, int]) -> None:
|
|
"""Reject negative origins and non-positive source or model dimensions."""
|
|
|
|
with pytest.raises(ValueError):
|
|
_tile(**values)
|
|
|
|
|
|
def test_spatial_view_rejects_untyped_kind() -> None:
|
|
"""Require callers to select one explicit view-kind value."""
|
|
|
|
with pytest.raises(TypeError, match="SpatialViewKind"):
|
|
SpatialView(
|
|
kind="tile", # type: ignore[arg-type]
|
|
source_x=0,
|
|
source_y=0,
|
|
source_width=8,
|
|
source_height=8,
|
|
model_width=8,
|
|
model_height=8,
|
|
)
|
|
|
|
|
|
def test_full_view_preserves_source_dimensions() -> None:
|
|
"""Reject resized full views while allowing reduced Contextual global views."""
|
|
|
|
with pytest.raises(ValueError, match="preserve its source dimensions"):
|
|
SpatialView(
|
|
kind=SpatialViewKind.FULL,
|
|
source_x=0,
|
|
source_y=0,
|
|
source_width=16,
|
|
source_height=12,
|
|
model_width=8,
|
|
model_height=6,
|
|
)
|
|
|
|
contextual_global = SpatialView(
|
|
kind=SpatialViewKind.CONTEXTUAL_GLOBAL,
|
|
source_x=0,
|
|
source_y=0,
|
|
source_width=16,
|
|
source_height=12,
|
|
model_width=8,
|
|
model_height=6,
|
|
)
|
|
assert contextual_global.model_width == 8
|
|
assert contextual_global.model_height == 6
|
|
|
|
|
|
def test_layout_expands_views_in_exact_view_major_order() -> None:
|
|
"""Keep each view's complete source batch contiguous in model-call order."""
|
|
|
|
left = _tile(x=0, width=8)
|
|
right = _tile(x=8, width=8)
|
|
layout = SpatialBatchLayout(
|
|
canvas_width=16,
|
|
canvas_height=8,
|
|
views=(left, right),
|
|
input_batch_size=3,
|
|
)
|
|
|
|
assert layout.view_count == 2
|
|
assert layout.expanded_batch_size == 6
|
|
assert layout.expanded_views == (left, left, left, right, right, right)
|
|
assert layout.expanded_view_indices == (0, 0, 0, 1, 1, 1)
|
|
assert layout.expanded_source_batch_indices == (0, 1, 2, 0, 1, 2)
|
|
assert layout.expanded_index(0, 2) == 2
|
|
assert layout.expanded_index(1, 0) == 3
|
|
with pytest.raises(FrozenInstanceError):
|
|
layout.input_batch_size = 1 # type: ignore[misc]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("field", "value", "message"),
|
|
[
|
|
("canvas_width", 0, "canvas dimensions"),
|
|
("canvas_height", 0, "canvas dimensions"),
|
|
("input_batch_size", 0, "input batch size"),
|
|
("views", (), "at least one view"),
|
|
],
|
|
)
|
|
def test_layout_rejects_invalid_dimensions_or_empty_views(
|
|
field: str,
|
|
value: object,
|
|
message: str,
|
|
) -> None:
|
|
"""Reject layouts that cannot describe a non-empty model call."""
|
|
|
|
values: dict[str, object] = {
|
|
"canvas_width": 8,
|
|
"canvas_height": 8,
|
|
"views": (_tile(),),
|
|
"input_batch_size": 1,
|
|
}
|
|
values[field] = value
|
|
with pytest.raises(ValueError, match=message):
|
|
SpatialBatchLayout(**values) # type: ignore[arg-type]
|
|
|
|
|
|
def test_layout_requires_an_immutable_typed_view_tuple() -> None:
|
|
"""Reject mutable containers and values that are not spatial views."""
|
|
|
|
with pytest.raises(TypeError, match="immutable tuple"):
|
|
SpatialBatchLayout(8, 8, [_tile()], 1) # type: ignore[arg-type]
|
|
with pytest.raises(TypeError, match="SpatialView values"):
|
|
SpatialBatchLayout(8, 8, (object(),), 1) # type: ignore[arg-type]
|
|
|
|
|
|
def test_layout_rejects_out_of_canvas_or_mixed_views() -> None:
|
|
"""Reject source overflow and heterogeneous model-call view semantics."""
|
|
|
|
with pytest.raises(ValueError, match="inside the canvas"):
|
|
SpatialBatchLayout(8, 8, (_tile(x=4, width=8),), 1)
|
|
|
|
full = SpatialView(
|
|
SpatialViewKind.FULL,
|
|
0,
|
|
0,
|
|
8,
|
|
8,
|
|
8,
|
|
8,
|
|
)
|
|
with pytest.raises(ValueError, match="cannot mix view kinds"):
|
|
SpatialBatchLayout(8, 8, (full, _tile()), 1)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"kind",
|
|
[SpatialViewKind.FULL, SpatialViewKind.CONTEXTUAL_GLOBAL],
|
|
)
|
|
def test_full_source_layout_requires_one_complete_canvas_view(
|
|
kind: SpatialViewKind,
|
|
) -> None:
|
|
"""Require full and Contextual-global calls to describe the whole canvas."""
|
|
|
|
incomplete = SpatialView(kind, 1, 0, 7, 8, 7, 8)
|
|
with pytest.raises(ValueError, match="complete canvas"):
|
|
SpatialBatchLayout(8, 8, (incomplete,), 1)
|
|
|
|
complete = SpatialView(kind, 0, 0, 8, 8, 8, 8)
|
|
with pytest.raises(ValueError, match="requires one view"):
|
|
SpatialBatchLayout(8, 8, (complete, complete), 1)
|
|
|
|
|
|
def test_layout_rejects_expanded_indices_outside_either_axis() -> None:
|
|
"""Fail explicitly for invalid view or source-batch indices."""
|
|
|
|
layout = SpatialBatchLayout(8, 8, (_tile(),), 2)
|
|
|
|
with pytest.raises(IndexError, match="view index"):
|
|
layout.expanded_index(1, 0)
|
|
with pytest.raises(IndexError, match="batch index"):
|
|
layout.expanded_index(0, 2)
|
|
|
|
|
|
def _tile(
|
|
*,
|
|
x: int = 0,
|
|
y: int = 0,
|
|
width: int = 8,
|
|
height: int = 8,
|
|
model_width: int = 8,
|
|
model_height: int = 8,
|
|
source_x: int | None = None,
|
|
source_y: int | None = None,
|
|
source_width: int | None = None,
|
|
source_height: int | None = None,
|
|
) -> SpatialView:
|
|
"""Return one tile view with selectively overridden source fields."""
|
|
|
|
return SpatialView(
|
|
kind=SpatialViewKind.TILE,
|
|
source_x=x if source_x is None else source_x,
|
|
source_y=y if source_y is None else source_y,
|
|
source_width=width if source_width is None else source_width,
|
|
source_height=height if source_height is None else source_height,
|
|
model_width=model_width,
|
|
model_height=model_height,
|
|
)
|