raw ยท 8510 bytes
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | 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]) |