import tempfile import tomllib import unittest from pathlib import Path from tests.helpers import make_history, make_race from worldhistory.config import ConfigError, dumps_toml, link, load_config, parse_race, resolved RACE = """ id = "stone" name = "Stone folk" colour = [140, 140, 150] [demography] density = 1.2 growth = 0.02 allee = 75 [seed] clusters = 25 heads = 200 [habitat] realm = "land" terms = [{ p = "mountain", w = 0.7 }, { p = ["land", "coal"], w = 0.5 }] gates = [{ penalty = ["above:o2_fraction:0.3", "lithology:3,4"], factor = 0.6 }] [tolerance.temperature] optimum = 10 width = 20 lo = -30 hi = 40 rate = 0.001 [special] veins = 0.002 """ SUB = """ id = "deep" [demography] density = 0.5 growth = 0.08 [habitat] realm = "sea" terms = [{ p = "vent", w = 1.0 }] qmax = 0.7 [emerge] parent = "stone" birth_min = 0.5 [tolerance.temperature] optimum = 30 width = 10 lo = 0 hi = 50 """ HISTORY = """ [run] years = 7500 seed = 3 [world] eras = ["late"] [regions.zone] kind = "changed" era = "late" fields = ["gravity_g"] [[event]] year = 0 kind = "seed" [[event]] year = 20 kind = "cull" region = "zone" share = 0.6 by_race = { stone = 0.8 } [[event]] year = 20 kind = "era_switch" era = "late" """ def write(d, name, text): p = Path(d) / name p.parent.mkdir(parents=True, exist_ok=True) p.write_text(text) class ConfigTest(unittest.TestCase): def test_load_defaults_and_inheritance(self): with tempfile.TemporaryDirectory() as d: write(d, "history.toml", HISTORY) write(d, "races/stone.toml", RACE) write(d, "races/deep.toml", SUB) h, races = load_config(d) self.assertEqual([r.id for r in races], ["stone", "deep"]) stone, deep = races self.assertEqual(h.step, 10) # default self.assertEqual(h.years, 7500) self.assertEqual(stone.founder, 75) # founder defaults to allee self.assertEqual(stone.family, "stone") self.assertEqual(deep.family, "stone") # sub-species joins the parent's family self.assertEqual(deep.tolerance["temperature"].width, 10) # own curve self.assertEqual(deep.clusters, 0) # sub-species are never seeded self.assertEqual(stone.travel["river"], -1.0) # default self.assertEqual(stone.gates[0]["factor"], 0.6) self.assertEqual(h.events[1]["by_race"], {"stone": 0.8}) def test_unknown_key(self): with self.assertRaisesRegex(ConfigError, r"test:x.*demography.*desnity"): parse_race({"id": "x", "demography": {"desnity": 1}}, "test:x") def test_unknown_predicate(self): with self.assertRaisesRegex(ConfigError, "nosuch"): make_race(habitat={"terms": [{"p": "nosuch", "w": 1}]}) def test_bad_tolerance_condition(self): with self.assertRaisesRegex(ConfigError, "wind"): make_race(tolerance={"wind": {"optimum": 0, "width": 1, "lo": 0, "hi": 1}}) def test_tolerance_needs_widths(self): with self.assertRaisesRegex(ConfigError, "width"): make_race(tolerance={"depth": {"optimum": 0, "lo": 0, "hi": 10, "width_lo": 5}}) def test_link_checks_references(self): h = make_history(event=[{"year": 20, "kind": "cull", "region": "nowhere", "share": 0.5}]) with self.assertRaisesRegex(ConfigError, "nowhere"): link(h, [make_race()]) h = make_history(event=[{"year": 20, "kind": "cull", "share": 0.5, "by_race": {"ghost": 0.1}}]) with self.assertRaisesRegex(ConfigError, "ghost"): link(h, [make_race()]) h = make_history(event=[{"year": 20, "kind": "era_switch", "era": "late"}]) with self.assertRaisesRegex(ConfigError, "late"): link(h, [make_race()]) sub = make_race("s", emerge={"parent": "p"}) with self.assertRaisesRegex(ConfigError, "p"): link(make_history(), [sub]) same = {"gravity": {"optimum": 1.0, "width": 0.5, "lo": 0.3, "hi": 1.3}} with self.assertRaisesRegex(ConfigError, "never be born"): # progress stays 0 < birth_min link(make_history(), [make_race("p", tolerance=same), make_race("s", tolerance=same, emerge={"parent": "p"})]) link(make_history(), [make_race("p", tolerance=same), # birth_min 0 or ritual: fine make_race("s", emerge={"parent": "p", "birth_min": 0.0, "birth_sure": 0.0})]) link(make_history(), [make_race("p", tolerance=same), make_race("s", emerge={"parent": "p", "mode": "ritual"})]) def test_emerge_defaults_and_merge(self): with tempfile.TemporaryDirectory() as d: write(d, "history.toml", HISTORY) write(d, "races/stone.toml", RACE.replace("[special]", "[tolerance.depth]\noptimum = 0\nwidth = 60\n" "lo = 0\nhi = 6000\n[special]")) write(d, "races/deep.toml", SUB) h, (stone, deep) = load_config(d) em = deep.emerge self.assertEqual((em["mode"], em["birth_min"], em["birth_sure"], em["birth_rate"]), ("adapt", 0.5, 0.8, 0.2)) self.assertEqual((em["return_min"], em["return_sure"]), (0.5, 0.8)) # default to the birth values self.assertEqual((em["mix_min"], em["displace"], em["spread"]), (0.3, 0.9, "contact")) self.assertEqual(deep.tolerance["temperature"].optimum, 30) # own curve self.assertEqual(deep.tolerance["depth"].width_hi, 60) # inherited condition def test_removed_keys_name_the_replacement(self): with self.assertRaisesRegex(ConfigError, "removed.*birth_min"): make_race("s", emerge={"parent": "p", "class": "deep_sea"}) with self.assertRaisesRegex(ConfigError, "removed.*birth_min"): make_race("s", emerge={"parent": "p", "years": [1, 2]}) with self.assertRaisesRegex(ConfigError, "exposure.*removed"): make_history(exposure={"hot": "above:T_mean:10"}) with self.assertRaisesRegex(ConfigError, "trigger"): make_race("s", emerge={"parent": "p", "trigger": 0.1}) # ritual only with self.assertRaisesRegex(ConfigError, "crisis_drop"): make_race("s", emerge={"parent": "p", "crisis_drop": 0.9}) # ritual only with self.assertRaisesRegex(ConfigError, "victims"): make_race("s", emerge={"parent": "p", "mode": "ritual", "victims": "everyone"}) with self.assertRaisesRegex(ConfigError, "crisis_drop"): make_race("s", emerge={"parent": "p", "mode": "ritual", "crisis_drop": 1.0}) self.assertEqual(make_race("s", emerge={"parent": "p", "birth_rate": 0.4}).emerge["return_rate"], 0.4) for key, bad in [("displace", 1.5), ("dominance", -0.1), ("birth_shift", 1.2), ("convert", 0.0), ("mix_min", 1.1), ("birth_rate", 2.0), ("return_rate", -1.0), ("after", -5)]: with self.assertRaisesRegex(ConfigError, key): make_race("s", emerge={"parent": "p", key: bad}) for key, bad in [("ritual_rate", -0.1), ("ritual_cost", -1), ("ritual_converts", -1), ("trigger", 1.5), ("raid_size", 0), ("raid_km", -1), ("raid_rate", -1), ("ritual_power", 0), ("backlash_years", -1), ("crisis_years", 0)]: with self.assertRaisesRegex(ConfigError, key): make_race("s", emerge={"parent": "p", "mode": "ritual", key: bad}) with self.assertRaisesRegex(ConfigError, "mode"): make_race("s", emerge={"parent": "p", "mode": "magic"}) with self.assertRaisesRegex(ConfigError, "birth_min"): make_race("s", emerge={"parent": "p", "birth_min": 0.9, "birth_sure": 0.5}) def test_dumps_roundtrip(self): h = make_history(regions={"z": {"kind": "box", "lat": [0, 1], "lon": [2, 3]}}, event=[{"year": 0, "kind": "seed"}, {"year": 20, "kind": "cull", "share": 0.3, "by_race": {"a": 0.5}}]) doc = resolved(h, [make_race()], seed=5, engine="abc") back = tomllib.loads(dumps_toml(doc)) self.assertEqual(back["seed"], 5) self.assertEqual(back["history"]["events"][1]["by_race"], {"a": 0.5}) self.assertEqual(back["race"][0]["id"], "a") self.assertEqual(back["history"]["regions"]["z"]["lat"], [0, 1])