Files
limbicnation 20d5149d02 fix: gate _last_seed mutation to dynamic modes
- random_seed_generator.py: only update the shared _last_seed counter for
  random/increment/decrement modes. fixed mode must be side-effect-free so
  interleaved fixed reads do not clobber the counter used by other
  AdvancedSeedGenerator nodes in the same workflow.
- tests: add regression coverage for the new gating (fixed leaves _last_seed
  alone; random and increment write through). Rewrite reset_state test to
  set state directly instead of relying on the removed fixed side effect.
- pyproject.toml: rewrite description to drop the stale
  "cross-library synchronization" claim (feature removed in 2.3.0).
- README: replace the unrealistic "ComfyUI Python console" reset advice
  with three actionable options (restart, random-then-increment, and
  reset_state for scripts).
2026-05-10 19:47:38 +02:00

148 lines
4.6 KiB
Python

"""Pytest suite for AdvancedSeedGenerator.
Covers the four modes (fixed, random, increment, decrement), wrap behavior at
both 64-bit boundaries, IS_CHANGED contract, and reset_state.
"""
import sys
import time
from pathlib import Path
import pytest
# Allow `import random_seed_generator` when pytest is run from the repo root.
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from random_seed_generator import ( # noqa: E402
MAX_SEED,
MIN_SEED,
NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS,
AdvancedSeedGenerator,
)
@pytest.fixture(autouse=True)
def _reset_class_state():
"""`_last_seed` is class-level shared state — reset before every test."""
AdvancedSeedGenerator.reset_state()
yield
AdvancedSeedGenerator.reset_state()
@pytest.fixture
def gen():
return AdvancedSeedGenerator()
def test_fixed_returns_input_seed(gen):
assert gen.generate_seed("fixed", 42) == (42,)
def test_fixed_does_not_mutate_last_seed(gen):
"""Regression: fixed mode must not touch the shared increment/decrement counter.
Before this guard, calling `fixed` clobbered `_last_seed`, which silently
altered other AdvancedSeedGenerator nodes running increment/decrement in the
same workflow.
"""
AdvancedSeedGenerator._last_seed = 500
assert gen.generate_seed("fixed", 12345) == (12345,)
assert AdvancedSeedGenerator._last_seed == 500
def test_random_updates_last_seed(gen):
AdvancedSeedGenerator._last_seed = 0
(result,) = gen.generate_seed("random", 0)
assert AdvancedSeedGenerator._last_seed == result
def test_increment_updates_last_seed(gen):
AdvancedSeedGenerator._last_seed = 100
gen.generate_seed("increment", 0)
assert AdvancedSeedGenerator._last_seed == 101
def test_random_within_bounds(gen):
(result,) = gen.generate_seed("random", 0)
assert isinstance(result, int)
assert MIN_SEED <= result <= MAX_SEED
def test_random_produces_different_values(gen):
samples = {gen.generate_seed("random", 0)[0] for _ in range(20)}
# 20 draws from a 2^64 space colliding 19 times would be a miracle.
assert len(samples) > 1
def test_increment_advances_last_seed(gen):
AdvancedSeedGenerator._last_seed = 100
assert gen.generate_seed("increment", 0) == (101,)
assert gen.generate_seed("increment", 0) == (102,)
def test_decrement_decreases_last_seed(gen):
AdvancedSeedGenerator._last_seed = 100
assert gen.generate_seed("decrement", 0) == (99,)
assert gen.generate_seed("decrement", 0) == (98,)
def test_increment_wraps_at_max(gen):
AdvancedSeedGenerator._last_seed = MAX_SEED
assert gen.generate_seed("increment", 0) == (MIN_SEED,)
def test_decrement_wraps_at_min(gen):
AdvancedSeedGenerator._last_seed = MIN_SEED
assert gen.generate_seed("decrement", 0) == (MAX_SEED,)
def test_unknown_mode_raises_value_error(gen):
with pytest.raises(ValueError, match="Unknown mode"):
gen.generate_seed("teleport", 0)
def test_is_changed_dynamic_modes_return_timestamp():
for mode in ("random", "increment", "decrement"):
before = time.time()
result = AdvancedSeedGenerator.IS_CHANGED(mode, 0)
after = time.time()
assert isinstance(result, float)
assert before <= result <= after
def test_is_changed_fixed_returns_stable_key():
assert AdvancedSeedGenerator.IS_CHANGED("fixed", 99) == "fixed_99"
assert AdvancedSeedGenerator.IS_CHANGED("fixed", 99) == "fixed_99"
def test_reset_state_clears_last_seed(gen):
AdvancedSeedGenerator._last_seed = 7777
assert AdvancedSeedGenerator._last_seed == 7777
AdvancedSeedGenerator.reset_state()
assert AdvancedSeedGenerator._last_seed == 0
def test_node_registration_mappings():
assert NODE_CLASS_MAPPINGS == {"AdvancedSeedGenerator": AdvancedSeedGenerator}
assert "AdvancedSeedGenerator" in NODE_DISPLAY_NAME_MAPPINGS
def test_input_types_schema_shape():
schema = AdvancedSeedGenerator.INPUT_TYPES()
assert "required" in schema
assert set(schema["required"].keys()) == {"mode", "seed"}
mode_choices, *_ = schema["required"]["mode"]
assert set(mode_choices) == {"fixed", "random", "increment", "decrement"}
seed_type, seed_meta = schema["required"]["seed"]
assert seed_type == "INT"
assert seed_meta["min"] == MIN_SEED
assert seed_meta["max"] == MAX_SEED
def test_comfyui_class_attributes():
assert AdvancedSeedGenerator.RETURN_TYPES == ("INT",)
assert AdvancedSeedGenerator.RETURN_NAMES == ("seed",)
assert AdvancedSeedGenerator.FUNCTION == "generate_seed"
assert AdvancedSeedGenerator.CATEGORY == "utils"