Files
bradsec-ComfyUI_ResolutionS…/test_resolution_selector.py
T
BRADSEC 02f84ccb23 Fix invalid presets, validate preset path, sync JS resolution table (review 1, 2)
Qwen Image 1140x1472 (and mirror) and SD 1.5 512x682 were not divisible
by 8, so the generated latent did not match the reported width/height;
removed the stray Qwen pair (official list has 1104x1472) and corrected
682 to 680. The preset branch now runs _validate_dimensions before the
multiplier, so API-built workflows cannot pair a model with another
model's incompatible preset. Regenerated the hand-copied JS table from
nodes.py (it had drifted by 14 entries) and added tests that fail on
future drift or non-conforming presets. Bump to 2.1.6.
2026-07-03 10:56:23 +10:00

243 lines
9.3 KiB
Python

#!/usr/bin/env python3
"""
Test script for ResolutionSelector enhancements
"""
import sys
import unittest
sys.path.insert(0, '.')
# Mock torch module for testing without ComfyUI environment
class MockTorch:
class device:
def __init__(self, name):
self.name = name
sys.modules['torch'] = MockTorch()
sys.modules['comfy'] = type('module', (), {'model_management': None})()
from nodes import (
calculate_aspect_ratio,
format_resolution,
get_resolution_list,
get_all_resolutions,
get_latent_channels,
parse_resolution_string,
MODEL_RESOLUTIONS
)
def test_aspect_ratio():
"""Test aspect ratio calculation"""
print("\nTesting aspect ratio calculation:")
assert calculate_aspect_ratio(1920, 1080) == "16:9", "1920x1080 should be 16:9"
assert calculate_aspect_ratio(1024, 1024) == "1:1", "1024x1024 should be 1:1"
assert calculate_aspect_ratio(1280, 720) == "16:9", "1280x720 should be 16:9"
assert calculate_aspect_ratio(1536, 1024) == "3:2", "1536x1024 should be 3:2"
print(" ✓ Aspect ratio tests passed")
def test_format_resolution():
"""Test resolution formatting"""
print("\nTesting resolution formatting:")
result = format_resolution(1920, 1080)
print(f" 1920x1080 → '{result}'")
# With padding, should be "1920x1080 (16:9 Landscape)" - 13 chars total for resolution part
assert "(16:9 Landscape)" in result, f"Should contain aspect ratio and orientation"
assert result.startswith("1920x1080"), f"Should start with resolution"
result = format_resolution(1024, 1024)
print(f" 1024x1024 → '{result}'")
assert "(1:1 Square)" in result, f"Should contain aspect ratio and orientation"
assert result.startswith("1024x1024"), f"Should start with resolution"
result = format_resolution(1080, 1920)
print(f" 1080x1920 → '{result}'")
assert "(9:16 Portrait)" in result, f"Should contain aspect ratio and orientation"
assert result.startswith("1080x1920"), f"Should start with resolution"
print(" ✓ Format resolution tests passed")
def test_parse_resolution():
"""Test resolution string parsing"""
print("\nTesting resolution string parsing:")
# Test with padding (as it will be in the actual dropdown)
width, height = parse_resolution_string("1920x1080 (16:9 Landscape)")
assert width == 1920 and height == 1080, "Should parse 1920x1080 with padding"
width, height = parse_resolution_string("1024x1024 (1:1 Square)")
assert width == 1024 and height == 1024, "Should parse 1024x1024 with padding"
# Test without padding (backward compatibility)
width, height = parse_resolution_string("1920x1080 (16:9 Landscape)")
assert width == 1920 and height == 1080, "Should parse 1920x1080 without padding"
print(" ✓ Parse resolution tests passed")
def test_model_resolutions():
"""Test model resolution lists"""
print("\nTesting model resolution lists:")
# Test individual model
flux_res = get_resolution_list("Flux")
print(f" Flux has {len(flux_res)} resolutions")
assert len(flux_res) > 0, "Flux should have resolutions"
# Flux uses 16-pixel divisibility, so 1920x1080 becomes 1920x1088
assert any("1920x1088" in r for r in flux_res), "Flux should have 1920x1088 (Full HD adapted to 16px divisibility)"
# Test All model
all_res = get_resolution_list("All")
print(f" 'All' has {len(all_res)} unique resolutions")
assert len(all_res) > len(flux_res), "'All' should have more resolutions than individual models"
# Verify no duplicates in All
assert len(all_res) == len(set(all_res)), "'All' should have no duplicates"
print(" ✓ Model resolution tests passed")
def test_new_resolutions():
"""Test that new resolutions were added"""
print("\nTesting new resolutions up to 1920x1080:")
for model_name, model_data in MODEL_RESOLUTIONS.items():
resolutions = get_resolution_list(model_name)
has_1080p = any("1920x1080" in r or "1080x1920" in r for r in resolutions)
print(f" {model_name}: {len(resolutions)} resolutions, has 1080p: {has_1080p}")
print(" ✓ New resolutions verified")
def test_all_resolutions_unique():
"""Test that 'All' model returns unique resolutions"""
print("\nTesting 'All' model uniqueness:")
all_res = get_all_resolutions()
# Count occurrences
dimensions = []
for res in all_res:
width, height = parse_resolution_string(res)
dimensions.append((width, height))
# Check for duplicates
unique_dimensions = set(dimensions)
assert len(dimensions) == len(unique_dimensions), "Should have no duplicate dimensions"
print(f" ✓ All {len(all_res)} resolutions are unique")
def test_latent_channels():
"""R1: latent channel count must match the model (4 SD-based, 16 Flux/Qwen/Z-Image)"""
print("\nTesting latent channel mapping:")
expected = {"Flux": 16, "Qwen Image": 16, "Z-Image": 16, "SD 1.5": 4, "SDXL": 4, "All": 4}
for model, channels in expected.items():
got = get_latent_channels(model)
assert got == channels, f"{model}: expected {channels} channels, got {got}"
print(f" ✓ Channel mapping correct: {expected}")
def test_qwen_official_resolutions():
"""Official Qwen-Image sizes must be present in all 16-channel models"""
print("\nTesting official Qwen resolutions in 16-channel models:")
want = [(1328, 1328), (1664, 928), (928, 1664), (1472, 1104),
(1104, 1472), (1584, 1056), (1056, 1584)]
for model in ["Flux", "Qwen Image", "Z-Image"]:
resolutions = get_resolution_list(model)
for w, h in want:
s = format_resolution(w, h)
assert s in resolutions, f"{model} missing {w}x{h}"
print(f" ✓ All {len(want)} resolutions present in Flux, Qwen Image, Z-Image")
def test_presets_satisfy_constraints():
"""Every preset must satisfy its own model's divisibility/min/max, or the
reported width/height will not match the generated latent dimensions."""
print("\nTesting preset dimensions against model constraints:")
for model, data in MODEL_RESOLUTIONS.items():
c = data["constraints"]
for category in ("square", "portrait", "landscape"):
for w, h in data[category]:
assert w % c["divisible_by"] == 0 and h % c["divisible_by"] == 0, \
f"{model} {w}x{h} not divisible by {c['divisible_by']}"
assert c["min"] <= w <= c["max"] and c["min"] <= h <= c["max"], \
f"{model} {w}x{h} outside {c['min']}-{c['max']}"
print(" ✓ All presets satisfy their model constraints")
def test_js_table_matches_python():
"""js/resolution_selector.js hand-mirrors MODEL_RESOLUTIONS; catch drift."""
print("\nTesting JS resolution table matches Python:")
import json, os, re
js_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"js", "resolution_selector.js")
with open(js_path, encoding="utf-8") as f:
src = f.read()
match = re.search(r"const MODEL_RESOLUTIONS = (\{.*?\n\});", src, re.DOTALL)
assert match, "MODEL_RESOLUTIONS table not found in resolution_selector.js"
js_table = json.loads(re.sub(r",(\s*[}\]])", r"\1", match.group(1)))
py_table = {model: {cat: [list(p) for p in data[cat]]
for cat in ("square", "portrait", "landscape")}
for model, data in MODEL_RESOLUTIONS.items()}
assert js_table == py_table, "JS MODEL_RESOLUTIONS differs from nodes.py"
print(" ✓ JS table in sync with nodes.py")
class TestResolutionSelector(unittest.TestCase):
"""unittest wrapper so `python -m unittest` discovers these tests (R5).
Each method delegates to the existing function-style test; an assertion
failure inside propagates and fails the unittest case.
"""
def test_aspect_ratio(self):
test_aspect_ratio()
def test_format_resolution(self):
test_format_resolution()
def test_parse_resolution(self):
test_parse_resolution()
def test_model_resolutions(self):
test_model_resolutions()
def test_new_resolutions(self):
test_new_resolutions()
def test_all_resolutions_unique(self):
test_all_resolutions_unique()
def test_latent_channels(self):
test_latent_channels()
def test_qwen_official_resolutions(self):
test_qwen_official_resolutions()
def test_presets_satisfy_constraints(self):
test_presets_satisfy_constraints()
def test_js_table_matches_python(self):
test_js_table_matches_python()
if __name__ == "__main__":
print("=" * 60)
print("ResolutionSelector Enhancement Tests")
print("=" * 60)
try:
test_aspect_ratio()
test_format_resolution()
test_parse_resolution()
test_model_resolutions()
test_new_resolutions()
test_all_resolutions_unique()
test_latent_channels()
test_qwen_official_resolutions()
test_presets_satisfy_constraints()
test_js_table_matches_python()
print("\n" + "=" * 60)
print("✓ ALL TESTS PASSED!")
print("=" * 60)
except AssertionError as e:
print(f"\n✗ TEST FAILED: {e}")
sys.exit(1)
except Exception as e:
print(f"\n✗ ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)