"""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", "", "dewy"), complexion=("fixed", "", "tan")) self.assertIn("tan", out[4]) self.assertIn("dewy", 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 -> " 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) def test_bust_average_uses_an(self): out = run(sex="female", bust_size=("fixed", "", "average"), bust_shape=("off", "", "(none)")) self.assertIn("an average bust", out[0]) def test_bust_full_uses_a(self): out = run(sex="female", bust_size=("fixed", "", "full"), bust_shape=("off", "", "(none)")) self.assertIn("a full bust", out[0]) class TestSegmentNodes(unittest.TestCase): def _node(self, node_id): return core.NODE_CLASS_MAPPINGS[node_id] def test_all_nodes_registered(self): for node_id in ("RandomPersonNode", "RandomPersonIdentity", "RandomPersonFace", "RandomPersonHair", "RandomPersonBody", "RandomPersonStyle"): self.assertIn(node_id, core.NODE_CLASS_MAPPINGS, node_id) def test_all_nodes_in_random_person_category(self): for cls in core.NODE_CLASS_MAPPINGS.values(): self.assertEqual(cls.CATEGORY, "Random Person") def test_first_pin_relabelled_per_node(self): # Display label of the first pin is node-specific; internal value key stays "description". expect = { "RandomPersonNode": "full_description", "RandomPersonIdentity": "full_identity_description", "RandomPersonFace": "full_face_description", "RandomPersonHair": "full_hair_description", "RandomPersonBody": "full_body_description", "RandomPersonStyle": "full_style_description", } for node_id, label in expect.items(): self.assertEqual(core.NODE_CLASS_MAPPINGS[node_id].RETURN_NAMES[0], label, node_id) # Module RETURN_NAMES (the value-lookup keys) is unchanged, so the first # pin still resolves to the description value. self.assertEqual(core.RETURN_NAMES[0], "description") def test_identity_exposes_only_its_widgets(self): req = self._node("RandomPersonIdentity").INPUT_TYPES()["required"] self.assertIn("nationality_mode", req) self.assertIn("age_mode", req) self.assertNotIn("eyes_mode", req) self.assertNotIn("hair_color_mode", req) def test_face_node_has_no_age(self): req = self._node("RandomPersonFace").INPUT_TYPES()["required"] self.assertIn("eyes_mode", req) self.assertNotIn("age_mode", req) def test_face_fragment_excludes_other_groups(self): cls = self._node("RandomPersonFace") kwargs = {} req = cls.INPUT_TYPES()["required"] for name, (typ, opts) in req.items(): if name.endswith("_mode"): kwargs[name] = "random" elif name.endswith("_allow_list"): kwargs[name] = "" elif name.endswith("_fixed"): kwargs[name] = "(none)" out = cls().generate(seed=3, randomize=False, sex="female", **kwargs) desc = out[0] # first pin is always the full description self.assertNotIn("build", desc) # body excluded self.assertNotIn("hair", desc) # hair excluded self.assertNotIn("year old", desc) # age excluded def test_body_node_outputs_bust_pin(self): cls = self._node("RandomPersonBody") self.assertIn("bust", cls.RETURN_NAMES) self.assertIn("shoulders", cls.RETURN_NAMES) self.assertNotIn("hair", cls.RETURN_NAMES) def test_node_specs_cover_all_ids(self): ids = [s[0] for s in core.NODE_SPECS] self.assertEqual(len(ids), len(set(ids))) self.assertEqual(ids[0], "RandomPersonNode") self.assertEqual(len(ids), 6) def test_every_output_name_is_a_known_pin(self): for _id, _name, _keys, _age, outs in core.NODE_SPECS: for name in outs: self.assertIn(name, core.RETURN_NAMES, f"{_id}:{name}") class TestClothing(unittest.TestCase): def test_clothing_defaults_off(self): req = core.RandomPersonNode.INPUT_TYPES()["required"] self.assertEqual(req["clothing_mode"][1]["default"], "off") def test_clothing_is_sex_gated(self): self.assertIn("clothing", core.SEX_GATED) def test_clothing_pin_carries_value(self): out = run(sex="female", clothing=("fixed", "", "leather jacket")) idx = core.RETURN_NAMES.index("clothing") self.assertEqual(out[idx], "a leather jacket") self.assertIn("a leather jacket", out[0]) def test_male_never_gets_female_only_garment(self): female_only_descs = { "a blouse", "a camisole", "a cardigan", "a cocktail dress", "a jumpsuit", "a knit sweater", "a maxi dress", "an off-shoulder top", "a peplum top", "a sundress", "a wool coat", "a wrap dress", } for seed in range(30): out = run(seed=seed, sex="male", clothing=("random", "", "(none)")) idx = core.RETURN_NAMES.index("clothing") val = out[idx] if val: self.assertNotIn(val, female_only_descs, f"seed={seed}: male got female-only '{val}'") def test_female_never_gets_male_only_garment(self): male_only_descs = { "a flannel shirt", "a polo shirt", "a henley shirt", "a work shirt", "a bomber jacket", "a crew-neck sweater", "a parka", "a three-piece suit", "a tracksuit", "a wool overcoat", } for seed in range(30): out = run(seed=seed, sex="female", clothing=("random", "", "(none)")) idx = core.RETURN_NAMES.index("clothing") val = out[idx] if val: self.assertNotIn(val, male_only_descs, f"seed={seed}: female got male-only '{val}'") def test_style_node_exposes_clothing(self): req = core.NODE_CLASS_MAPPINGS["RandomPersonStyle"].INPUT_TYPES()["required"] self.assertIn("clothing_mode", req) self.assertIn("clothing", core.NODE_CLASS_MAPPINGS["RandomPersonStyle"].RETURN_NAMES) class TestFootwear(unittest.TestCase): def test_footwear_defaults_off(self): req = core.RandomPersonNode.INPUT_TYPES()["required"] self.assertEqual(req["footwear_mode"][1]["default"], "off") def test_footwear_is_sex_gated(self): self.assertIn("footwear", core.SEX_GATED) def test_footwear_pin_carries_value(self): out = run(sex="female", clothing=("fixed", "", "leather jacket"), footwear=("fixed", "", "high heels")) idx = core.RETURN_NAMES.index("footwear") self.assertEqual(out[idx], "high heels") self.assertIn("high heels", out[0]) def test_male_never_gets_female_only_shoe(self): female_only_shoes = { "high heels", "stilettos", "kitten heels", "platform heels", "strappy heels", "wedges", "ballet flats", "mary janes", "knee-high boots", } for seed in range(30): out = run(seed=seed, sex="male", footwear=("random", "", "(none)")) idx = core.RETURN_NAMES.index("footwear") val = out[idx] if val: self.assertNotIn(val, female_only_shoes, f"seed={seed}: male got female-only shoe '{val}'") def test_style_node_exposes_footwear(self): req = core.NODE_CLASS_MAPPINGS["RandomPersonStyle"].INPUT_TYPES()["required"] self.assertIn("footwear_mode", req) self.assertIn("footwear", core.NODE_CLASS_MAPPINGS["RandomPersonStyle"].RETURN_NAMES) if __name__ == "__main__": unittest.main()