270 lines
10 KiB
Python
270 lines
10 KiB
Python
"""Unit tests for the Random Person Description shared core.
|
|
|
|
Pure logic, no ComfyUI required: reads the bundled data/ JSON only.
|
|
Run: python -m unittest test_random_person -v
|
|
"""
|
|
|
|
import unittest
|
|
|
|
import nodes as core
|
|
|
|
|
|
def cat_args(**overrides):
|
|
"""All categories on 'random' by default; override with key=(mode, allow, fixed)."""
|
|
args = {key: ("random", "", "(none)") for key, _, _ in core.CATEGORY_SPECS}
|
|
args.update(overrides)
|
|
return args
|
|
|
|
|
|
def run(seed=123, randomize=False, sex="female", age_mode="random",
|
|
age_min=0, age_max=0, age_fixed=0, extra="", **cat):
|
|
return core.generate_person(seed, randomize, sex, cat_args(**cat),
|
|
age_mode, age_min, age_max, age_fixed, extra)
|
|
|
|
|
|
class TestSelection(unittest.TestCase):
|
|
|
|
def test_seed_reproducible(self):
|
|
a = run(seed=42)
|
|
b = run(seed=42)
|
|
self.assertEqual(a, b)
|
|
|
|
def test_different_seed_differs(self):
|
|
# Not guaranteed in theory, but with this data the full string should differ.
|
|
self.assertNotEqual(run(seed=1)[0], run(seed=999)[0])
|
|
|
|
def test_randomize_returns_used_seed(self):
|
|
out = run(randomize=True)
|
|
used = out[-1]
|
|
self.assertIsInstance(used, int)
|
|
self.assertNotEqual(used, 0)
|
|
# Re-running with that seed and randomize off reproduces the person.
|
|
self.assertEqual(run(seed=used)[0], out[0])
|
|
|
|
def test_off_mode_omits_body(self):
|
|
out = run(body_type=("off", "", "(none)"))
|
|
self.assertEqual(out[8], "") # body_type pin empty
|
|
self.assertNotIn("build", out[0].lower())
|
|
|
|
def test_fixed_nationality(self):
|
|
out = run(nationality=("fixed", "", "Japanese"))
|
|
self.assertIn("japanese", out[3].lower())
|
|
|
|
def test_allow_list_custom_value(self):
|
|
# A token with no field overlap with any JSON entry is used as a literal.
|
|
out = run(hair_color=("allow_list", "ultraviolet", "(none)"))
|
|
self.assertIn("ultraviolet", out[6]) # hair pin carries the custom value
|
|
|
|
def test_custom_nationality_not_dropped(self):
|
|
# Regression F-NAT: a custom nationality (no "descriptor" key) must still
|
|
# reach the description opener and the nationality pin.
|
|
out = run(nationality=("allow_list", "Atlantean", "(none)"))
|
|
self.assertEqual(out[3], "Atlantean") # nationality pin
|
|
self.assertIn("Atlantean", out[0]) # description opener
|
|
|
|
def test_sex_resolved_from_random(self):
|
|
self.assertIn(run(sex="random")[1], ("male", "female"))
|
|
|
|
def test_flair_categories_default_off(self):
|
|
# Optional categories must ship with mode default "off"; identity
|
|
# categories must default "random".
|
|
req = core.RandomPersonNode.INPUT_TYPES()["required"]
|
|
for key in core.DEFAULT_OFF:
|
|
self.assertEqual(req[f"{key}_mode"][1]["default"], "off", key)
|
|
for key in ("nationality", "complexion", "eyes", "hair_color", "body_type"):
|
|
self.assertEqual(req[f"{key}_mode"][1]["default"], "random", key)
|
|
|
|
def test_female_never_grows_facial_hair(self):
|
|
for seed in range(40):
|
|
self.assertEqual(run(seed=seed, sex="female")[7], "") # facial_hair pin empty
|
|
|
|
def test_male_makeup_empty(self):
|
|
for seed in range(40):
|
|
self.assertEqual(run(seed=seed, sex="male")[11], "") # makeup pin empty
|
|
|
|
def test_skin_texture_in_complexion_pin(self):
|
|
out = run(skin_texture=("fixed", "", "freckled"),
|
|
complexion=("fixed", "", "tan"))
|
|
self.assertIn("tan", out[4])
|
|
self.assertIn("freckled", out[4])
|
|
|
|
def test_eye_shape_merges_into_eyes(self):
|
|
# eye_shape sits in front of the eye colour, single "eyes" suffix, and
|
|
# surfaces in the face pin.
|
|
out = run(eye_shape=("fixed", "", "almond"),
|
|
eyes=("fixed", "", "green"))
|
|
self.assertIn("almond", out[5]) # face pin
|
|
self.assertIn("eyes", out[5])
|
|
self.assertNotIn("eyes eyes", out[0])
|
|
|
|
def test_eye_shape_alone_gets_suffix(self):
|
|
out = run(eye_shape=("fixed", "", "hooded"), eyes=("off", "", "(none)"))
|
|
self.assertIn("hooded eyes", out[5])
|
|
|
|
def test_eyebrows_pin_and_off(self):
|
|
on = run(eyebrows=("fixed", "", "thick"))
|
|
self.assertIn("thick", on[5]) # face pin carries eyebrows
|
|
off = run(eyebrows=("off", "", "(none)"))
|
|
self.assertNotIn("eyebrow", off[5].lower())
|
|
|
|
def test_eyebrows_default_off(self):
|
|
req = core.RandomPersonNode.INPUT_TYPES()["required"]
|
|
self.assertEqual(req["eyebrows_mode"][1]["default"], "off")
|
|
|
|
def test_expression_pin_and_off(self):
|
|
on = run(expression=("fixed", "", "serious"))
|
|
self.assertIn("serious", on[9])
|
|
off = run(expression=("off", "", "(none)"))
|
|
self.assertEqual(off[9], "")
|
|
|
|
|
|
class TestAge(unittest.TestCase):
|
|
|
|
def test_age_off(self):
|
|
out = run(age_mode="off")
|
|
self.assertEqual(out[2], "")
|
|
self.assertNotIn("year old", out[0])
|
|
|
|
def test_age_fixed_clamped_to_min(self):
|
|
out = run(age_mode="fixed", age_fixed=15)
|
|
self.assertGreaterEqual(int(out[2]), 21)
|
|
|
|
def test_age_range_respected(self):
|
|
ages = core.load_shared("age.json")
|
|
for seed in range(30):
|
|
picked = core.resolve_age("random", 30, 40, 0, ages)
|
|
self.assertTrue(30 <= picked["age"] <= 40)
|
|
|
|
|
|
class TestBuildDescription(unittest.TestCase):
|
|
|
|
def test_empty_when_all_none(self):
|
|
self.assertEqual(core.build_description(*( [""] + [None] * 17 )), "")
|
|
|
|
def test_extra_attributes_appended(self):
|
|
out = run(extra="wearing glasses, gold earrings")
|
|
self.assertIn("wearing glasses", out[0])
|
|
self.assertIn("gold earrings", out[0])
|
|
|
|
def test_eyes_no_double_suffix(self):
|
|
# An eyes description already containing "eyes" must not get " eyes" appended.
|
|
args = [""] + [None] * 17
|
|
args[6] = {"description": "muted natural green eyes"} # eyes positional slot
|
|
s = core.build_description(*args)
|
|
self.assertNotIn("eyes eyes", s)
|
|
|
|
def test_opener_omits_bare_sex(self):
|
|
# sex present but no age and no nationality -> no leading "female".
|
|
args = [""] + [None] * 17
|
|
args[0] = "female" # sex positional slot
|
|
args[4] = {"description": "oval"} # face_shape slot -> "oval face"
|
|
s = core.build_description(*args)
|
|
self.assertTrue(s.startswith("oval face"), s)
|
|
self.assertNotIn("female", s)
|
|
|
|
def test_opener_keeps_sex_with_age(self):
|
|
# age + sex -> "<n> year old female ..."
|
|
s = core.build_description(
|
|
"female", None, {"age": 30}, *( [None] * 15 ))
|
|
self.assertIn("year old female", s)
|
|
|
|
|
|
class TestNodeHelpers(unittest.TestCase):
|
|
|
|
def test_collect_args_restricts_keys(self):
|
|
kwargs = {}
|
|
for key, _, _ in core.CATEGORY_SPECS:
|
|
kwargs[f"{key}_mode"] = "random"
|
|
kwargs[f"{key}_allow_list"] = ""
|
|
kwargs[f"{key}_fixed"] = "(none)"
|
|
args = core._collect_category_args(kwargs, keys=["eyes"])
|
|
self.assertEqual(args["eyes"][0], "random")
|
|
self.assertEqual(args["hair_color"], ("off", "", "(none)"))
|
|
|
|
def test_person_dict_maps_names(self):
|
|
result = run(seed=5, sex="female")
|
|
dmap = core.person_dict(result)
|
|
self.assertEqual(set(dmap.keys()), set(core.RETURN_NAMES))
|
|
self.assertEqual(dmap["seed"], result[-1])
|
|
|
|
|
|
class TestBodyData(unittest.TestCase):
|
|
|
|
BODY_KEYS = ("shoulders", "chest", "bust_size", "bust_shape")
|
|
|
|
def test_all_body_files_load(self):
|
|
for sex in ("male", "female"):
|
|
for key in self.BODY_KEYS:
|
|
data = core.load_sex(sex, f"{key}.json")
|
|
self.assertTrue(len(data) >= 1, f"{sex}/{key}")
|
|
for item in data:
|
|
self.assertIn("label", item)
|
|
self.assertIn("description", item)
|
|
|
|
def test_male_bust_is_stub(self):
|
|
for key in ("bust_size", "bust_shape"):
|
|
labels = [i["label"] for i in core.load_sex("male", f"{key}.json")]
|
|
self.assertEqual(labels, ["none"], key)
|
|
|
|
def test_female_chest_is_stub(self):
|
|
labels = [i["label"] for i in core.load_sex("female", "chest.json")]
|
|
self.assertEqual(labels, ["none"])
|
|
|
|
def test_shoulders_real_for_both(self):
|
|
for sex in ("male", "female"):
|
|
labels = [i["label"] for i in core.load_sex(sex, "shoulders.json")]
|
|
self.assertGreater(len(labels), 1, sex)
|
|
self.assertNotIn("none", labels)
|
|
|
|
def test_body_categories_default_off(self):
|
|
req = core.RandomPersonNode.INPUT_TYPES()["required"]
|
|
for key in self.BODY_KEYS:
|
|
self.assertIn(f"{key}_mode", req, key)
|
|
self.assertEqual(req[f"{key}_mode"][1]["default"], "off", key)
|
|
|
|
def test_body_categories_are_sex_gated(self):
|
|
for key in self.BODY_KEYS:
|
|
self.assertIn(key, core.SEX_GATED, key)
|
|
|
|
def test_chest_in_description_male(self):
|
|
out = run(sex="male", chest=("fixed", "", "broad"))
|
|
self.assertIn("a broad chest", out[0])
|
|
|
|
def test_bust_combines_size_and_shape_female(self):
|
|
out = run(sex="female",
|
|
bust_size=("fixed", "", "full"),
|
|
bust_shape=("fixed", "", "round"))
|
|
self.assertIn("a full, round bust", out[0])
|
|
|
|
def test_bust_size_only_female(self):
|
|
out = run(sex="female", bust_size=("fixed", "", "petite"),
|
|
bust_shape=("off", "", "(none)"))
|
|
self.assertIn("a petite bust", out[0])
|
|
|
|
def test_shoulders_pin_present(self):
|
|
names = core.RETURN_NAMES
|
|
self.assertIn("shoulders", names)
|
|
self.assertIn("chest", names)
|
|
self.assertIn("bust", names)
|
|
out = run(sex="male", shoulders=("fixed", "", "broad"))
|
|
idx = names.index("shoulders")
|
|
self.assertEqual(out[idx], "broad shoulders")
|
|
|
|
def test_male_never_grows_bust(self):
|
|
for seed in range(30):
|
|
out = run(seed=seed, sex="male",
|
|
bust_size=("random", "", "(none)"),
|
|
bust_shape=("random", "", "(none)"))
|
|
bust_idx = core.RETURN_NAMES.index("bust")
|
|
self.assertEqual(out[bust_idx], "", seed)
|
|
|
|
def test_female_never_grows_chest(self):
|
|
for seed in range(30):
|
|
out = run(seed=seed, sex="female", chest=("random", "", "(none)"))
|
|
chest_idx = core.RETURN_NAMES.index("chest")
|
|
self.assertEqual(out[chest_idx], "", seed)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|