import shutil import tempfile import unittest from pathlib import Path from mapgen import config as C from mapgen.testing import FIXTURE_TOML TECT = """ [[plate]] id = "a" seed = [0.0, 0.0] kind = "continental" motion = [90.0, 3.0] [[plate]] id = "b" seed = [0.0, 90.0] kind = "oceanic" motion = [270.0, 3.0] """ class ConfigTest(unittest.TestCase): def setUp(self): self.tmp = Path(tempfile.mkdtemp()) (self.tmp / "config").mkdir() shutil.copy(FIXTURE_TOML, self.tmp / "config" / "world.toml") (self.tmp / "config" / "tectonics.toml").write_text(TECT) def tearDown(self): shutil.rmtree(self.tmp) def test_loads_repo_world(self): cfg, tect = C.load(self.tmp) self.assertEqual(cfg["planet"]["radius_km"], 12742.0) self.assertEqual(len(tect["plate"]), 2) def test_out_of_range_is_error(self): p = self.tmp / "config" / "world.toml" p.write_text(p.read_text().replace("tilt_deg = 20.0", "tilt_deg = 120.0")) with self.assertRaisesRegex(C.ConfigError, "tilt_deg"): C.load(self.tmp) def test_missing_key_is_error(self): p = self.tmp / "config" / "world.toml" p.write_text(p.read_text().replace("seed = 1296\n", "")) with self.assertRaisesRegex(C.ConfigError, "seed"): C.load(self.tmp) def test_params_merge_and_typo_guard(self): cfg = {"erosion": {"k": 0.5}} self.assertEqual(C.params(cfg, "erosion", {"k": 0.1, "m": 0.5}), {"k": 0.5, "m": 0.5}) with self.assertRaisesRegex(C.ConfigError, "kk"): C.params({"erosion": {"kk": 1}}, "erosion", {"k": 0.1}) def test_tectonics_validation(self): (self.tmp / "config" / "tectonics.toml").write_text(TECT.replace('kind = "oceanic"', 'kind = "lava"')) with self.assertRaisesRegex(C.ConfigError, "kind"): C.load(self.tmp) (self.tmp / "config" / "tectonics.toml").write_text(TECT.replace('id = "b"', 'id = "a"')) with self.assertRaisesRegex(C.ConfigError, "duplicate"): C.load(self.tmp) REV = """ [[plateau]] name = "p1" center = [-40.0, -18.0] area_km2 = 3.0e6 elongation = 1.9 azimuth_deg = 30.0 top_m = [1500.0, 3000.0] [[plateau]] name = "p2" center = [0.0, 176.0] area_km2 = 1.0e6 top_m = [1000.0, 1500.0] islands = true [[land_patch]] name = "fill" center = [-10.0, 102.0] radius_km = 2850.0 edge_noise = 0.4 [[zone]] name = "R1" field = "o2" center = [-5.6, -118.8] radius_km = 3200.0 v = 1.0 """ ERAS = """ [[event]] name = "cut" kind = "disintegrate" center = [-10.0, 102.0] radius_km = 3000.0 depth_m = 3000.0 [[event]] name = "aura" kind = "zone" shape = "landmass" seed = [-12.7, 121.5] reach_km = [500.0, 1000.0] edge_km = 5.0 fields = { gravity_g = 0.35, pressure_bar = 2.0, o2_fraction = 0.35, fire_reactivity = 0.5 } [eras] order = ["before", "after"] default = "after" [eras.before] label = "Before" events = [] [eras.after] label = "After" events = ["cut", "aura"] """ class RevisionConfigTest(unittest.TestCase): def setUp(self): self.tmp = Path(tempfile.mkdtemp()) (self.tmp / "config").mkdir() shutil.copy(FIXTURE_TOML, self.tmp / "config" / "world.toml") self._set() def tearDown(self): shutil.rmtree(self.tmp) def _set(self, rev=REV, eras=ERAS): (self.tmp / "config" / "tectonics.toml").write_text(TECT + rev) (self.tmp / "config" / "eras.toml").write_text(eras) def test_valid_revision_config_loads(self): _, t = C.load(self.tmp) self.assertEqual([p["name"] for p in t["plateau"]], ["p1", "p2"]) self.assertEqual([e["name"] for e in C.era_events(t, "after")], ["cut", "aura"]) self.assertEqual(C.era_events(t, "before"), []) def test_era_events_are_cumulative(self): eras = ERAS.replace('order = ["before", "after"]', 'order = ["before", "mid", "after"]').replace( '[eras.after]\nlabel = "After"\nevents = ["cut", "aura"]', '[eras.mid]\nlabel = "Mid"\nevents = ["aura"]\n[eras.after]\nlabel = "After"\nevents = ["cut"]') self._set(REV, eras) _, t = C.load(self.tmp) self.assertEqual([e["name"] for e in C.era_events(t, "mid")], ["aura"]) self.assertEqual([e["name"] for e in C.era_events(t, "after")], ["aura", "cut"]) with self.assertRaises(C.ConfigError): C.era_events(t, "never") def test_errors_name_the_problem(self): dup = '[[plateau]]\nname = "p1"\ncenter = [60.0, 60.0]\narea_km2 = 1e6\ntop_m = [1000.0, 2000.0]\n' cases = [ (REV.replace("top_m = [1500.0, 3000.0]", "top_m = [3000.0, 1500.0]"), ERAS, "top_m"), (REV.replace("center = [0.0, 176.0]", "center = [-41.0, -10.0]"), ERAS, "apart"), (REV + dup, ERAS, "duplicate"), (REV.replace('field = "o2"', 'field = "heat"'), ERAS, "field"), (REV, ERAS.replace("fire_reactivity = 0.5", "sparkle = 1.0"), "sparkle"), (REV, ERAS.replace('default = "after"', 'default = "later"'), "default"), (REV, ERAS.replace('events = ["cut", "aura"]', 'events = ["cut", "nope"]'), "nope"), (REV, ERAS.replace('kind = "disintegrate"', 'kind = "flood"'), "kind"), (REV, ERAS.replace("edge_km = 5.0\n", ""), "edge_km"), ] for rev, eras, needle in cases: with self.subTest(needle=needle): self._set(rev, eras) with self.assertRaises(C.ConfigError) as cm: C.load(self.tmp) self.assertIn(needle, str(cm.exception)) def test_events_belong_in_eras_toml(self): (self.tmp / "config" / "tectonics.toml").write_text(TECT + REV + ERAS) with self.assertRaises(C.ConfigError) as cm: C.load(self.tmp) self.assertIn("eras.toml", str(cm.exception)) def test_eras_toml_is_not_part_of_the_base_inputs_key(self): from mapgen import pipeline as P k = P.inputs_key(self.tmp, 1) self._set(REV, ERAS.replace("fire_reactivity = 0.5", "fire_reactivity = 0.6")) self.assertEqual(P.inputs_key(self.tmp, 1), k, "tuning an event must not rebuild the base world") self._set(REV.replace("v = 1.0", "v = 0.9"), ERAS) self.assertNotEqual(P.inputs_key(self.tmp, 1), k)