"""fill_res_config.json loader: parsing, validation, precedence, save.""" import json import pytest from fill_resistance import config, configfile from fill_resistance.configfile import (ConfigError, dialog_defaults, load_config, rect_terminals_json, save_classic_config, save_pdn_config, strip_comment_lines, updated_terminals_json) from fill_resistance.dialog import PdnTerminalRow, Selection CLASSIC = """\ // Fill Resistance run configuration - classic mode. // Full-line comments like this one are allowed; keys starting with "_" // are ignored everywhere. { "version": 1, "mode": "classic", "run": { "net": "VOUT+", "layers": ["F.Cu", "In1.Cu", "B.Cu"], "include_tracks": true, "vias_capped": true, "cap_max_drill_mm": 0.5, "adaptive": true, "cell_um": null, "freq_hz": "142k", "contact_model": "uniform", "include_buildup": false, "extra_cu_um": 0.0, "push_overlays": false, "trim": {"enabled": false, "mode": "pct", "value": 10.0} }, "classic": { "current_a": 40.0, "contact1": "auto", "contact2": "auto", "_comment": "pos/neg: terminals fully specified by the file", "pos": ["J1.1"], "neg": ["J2.1", "J2.2"] }, "physics": { "via_plating_um": 25.0 } } """ PDN = """\ // PDN study of the 3.3 V rail. { "version": 1, "mode": "pdn", "run": {"net": "VCC_3V3", "freq_hz": 0, "adaptive": true, "v_nominal": 3.30}, "terminals": [ {"name": "buck", "role": "supply", "parts": ["U1.SW2", "U1.SW3"], "r_out_ohm": 0.004}, {"name": "ldo", "role": "supply", "parts": ["U2.OUT"], "r_out_ohm": 0.050, "v_oc": 3.28}, {"name": "mcu", "role": "load", "parts": ["U7"], "i_draw_a": 1.8}, {"name": "cam", "role": "load", "parts": ["rect:CAM_ZONE"], "i_draw_a": 0.35, "contact": "F.Cu"}, {"name": "heater", "role": "load", "parts": [{"rect_mm": [112.0, 40.5, 118.0, 44.0], "contact": "B.Cu"}, {"via_mm": [115.2, 42.1]}], "i_draw_a": 2.5} ], "markers": {"pdn_layer": "User.3"} } """ def _write(tmp_path, text, name="fill_res_config.json"): p = tmp_path / name p.write_text(text, encoding="utf-8") return p def test_classic_example_loads(tmp_path): cfg = load_config(_write(tmp_path, CLASSIC)) assert cfg.mode == "classic" assert cfg.net == "VOUT+" assert cfg.layers == ["F.Cu", "In1.Cu", "B.Cu"] assert cfg.freq_hz == pytest.approx(142_000.0) assert cfg.cell_um_given and cfg.cell_um is None assert cfg.current_a == pytest.approx(40.0) assert cfg.trim_enabled is False assert cfg.trim_value == pytest.approx(10.0) assert [p.kind for p in cfg.pos_parts] == ["pad"] assert (cfg.neg_parts[0].ref, cfg.neg_parts[0].pad) == ("J2", "1") assert cfg.physics == {"via_plating_um": 25.0} assert cfg.terminals == [] def test_pdn_example_loads(tmp_path): cfg = load_config(_write(tmp_path, PDN)) assert cfg.mode == "pdn" assert cfg.net == "VCC_3V3" assert cfg.v_nominal == pytest.approx(3.30) assert [t.name for t in cfg.terminals] == ["buck", "ldo", "mcu", "cam", "heater"] buck, ldo, mcu, cam, heater = cfg.terminals assert buck.role == "supply" and buck.r_out_ohm == pytest.approx(0.004) assert buck.v_oc is None assert ldo.v_oc == pytest.approx(3.28) assert mcu.parts[0].kind == "footprint" and mcu.parts[0].ref == "U7" assert cam.parts[0].kind == "rect_label" assert cam.parts[0].label == "CAM_ZONE" assert cam.contact == "F.Cu" assert heater.parts[0].kind == "rect_mm" assert heater.parts[0].rect_mm == (112.0, 40.5, 118.0, 44.0) assert heater.parts[0].contact == "B.Cu" assert heater.parts[1].kind == "via_mm" assert cfg.markers == {"pdn_layer": "User.3"} def test_comment_stripping_keeps_line_numbers(tmp_path): # the syntax error sits on line 4 of the file; the two stripped # comment lines above it must not shift the reported position text = "// one\n// two\n{\n \"version\": oops\n}\n" with pytest.raises(ConfigError, match="line 4"): load_config(_write(tmp_path, text)) assert strip_comment_lines(text).count("\n") == text.count("\n") def test_unknown_keys_warn_but_load(tmp_path, capsys): text = json.dumps({"version": 1, "run": {"nett": "X", "net": "Y"}}) cfg = load_config(_write(tmp_path, text)) out = capsys.readouterr().out assert "run.nett" in out assert cfg.net == "Y" def test_underscore_keys_are_silent(tmp_path, capsys): text = json.dumps({"version": 1, "_note": "hi", "run": {"_x": 1, "net": "Y"}}) load_config(_write(tmp_path, text)) assert "warning" not in capsys.readouterr().out @pytest.mark.parametrize("mutate, match", [ (lambda d: d.pop("version"), "version"), (lambda d: d.update(version=2), "newer"), (lambda d: d.update(mode="pdn"), "no.*terminals|terminals"), (lambda d: d["run"].update(contact_model="bonded"), "contact_model"), (lambda d: d["run"].update(freq_hz="-5"), "freq_hz"), (lambda d: d["run"].update(cell_um=-1), "cell_um"), (lambda d: d["classic"].update(current_a=0), "current_a"), (lambda d: d["classic"].pop("neg"), "pos and neg together"), ]) def test_classic_validation_failures(tmp_path, mutate, match): d = json.loads(strip_comment_lines(CLASSIC)) mutate(d) with pytest.raises(ConfigError, match=match): load_config(_write(tmp_path, json.dumps(d))) @pytest.mark.parametrize("mutate, match", [ (lambda d: d["terminals"][2].update(name="buck"), "duplicates"), (lambda d: d["terminals"][2].pop("i_draw_a"), "i_draw_a.*required"), (lambda d: d["terminals"][0].update(i_draw_a=1.0), "supply.*i_draw_a"), (lambda d: d["terminals"][2].update(r_out_ohm=1.0), "load.*r_out_ohm"), (lambda d: d["terminals"][0].update(r_out_ohm=-1), "r_out_ohm"), (lambda d: d["terminals"][2].update(i_draw_a=-1), "i_draw_a"), (lambda d: d["terminals"][0].update(role="source"), "role"), (lambda d: d["terminals"][2].update(parts=["U7."]), "not a valid"), (lambda d: d["terminals"][3].update(parts=["rect:"]), "empty rect"), (lambda d: [d["terminals"].pop(0), d["terminals"].pop(0)], "at least one active supply"), (lambda d: [t.update(active=False) for t in d["terminals"] if t["role"] == "load"], "at least one active load"), (lambda d: d["terminals"][0].update(active="yes"), "active"), (lambda d: d["terminals"][0].update(comment=3), "comment"), (lambda d: d["run"].pop("net"), "run.net"), ]) def test_pdn_validation_failures(tmp_path, mutate, match): d = json.loads(strip_comment_lines(PDN)) mutate(d) with pytest.raises(ConfigError, match=match): load_config(_write(tmp_path, json.dumps(d))) def test_mode_inferred_from_terminals(tmp_path): d = json.loads(strip_comment_lines(PDN)) del d["mode"] cfg = load_config(_write(tmp_path, json.dumps(d))) assert cfg.mode == "pdn" c = json.loads(strip_comment_lines(CLASSIC)) del c["mode"] cfg = load_config(_write(tmp_path, json.dumps(c), name="c.json")) assert cfg.mode == "classic" def test_pad_number_with_dot_splits_on_first(tmp_path): d = {"version": 1, "run": {"net": "V"}, "terminals": [ {"name": "s", "role": "supply", "parts": ["U1.A.1"], "r_out_ohm": 0}, {"name": "l", "role": "load", "parts": ["U2.1"], "i_draw_a": 1}]} cfg = load_config(_write(tmp_path, json.dumps(d))) ref = cfg.terminals[0].parts[0] assert (ref.ref, ref.pad) == ("U1", "A.1") def test_find_config_prefers_board_stem(tmp_path): shared = _write(tmp_path, CLASSIC) specific = _write(tmp_path, CLASSIC, name="myboard.fill_res_config.json") assert configfile.find_config(tmp_path, "myboard.kicad_pcb") == specific assert configfile.find_config(tmp_path, "other.kicad_pcb") == shared assert configfile.find_config(tmp_path / "empty", "x.kicad_pcb") is None def test_find_config_loads_the_config_named_default(tmp_path): plain = _write(tmp_path, CLASSIC) named = _write(tmp_path, CLASSIC, name="fill_res_config.default.json") # "default" beats the plain legacy filename, board-specific beats both assert configfile.find_config(tmp_path, "x.kicad_pcb") == named specific = _write(tmp_path, CLASSIC, name="x.fill_res_config.json") assert configfile.find_config(tmp_path, "x.kicad_pcb") == specific named.unlink() specific.unlink() assert configfile.find_config(tmp_path, "x.kicad_pcb") == plain def test_named_config_filename_scheme(): assert (configfile.named_config_filename("pdn_test") == "fill_res_config.pdn_test.json") def test_dialog_defaults_without_config_match_constants(): d = dialog_defaults(None) assert d.include_tracks == config.INCLUDE_TRACKS assert d.vias_capped == config.VIAS_CAPPED assert d.adaptive == config.ADAPTIVE_CELLS assert d.contact_model == config.CONTACT_MODEL assert d.current_a == config.TEST_CURRENT_A assert d.trim_mode == config.TRIM_MODE assert d.net is None and d.layers is None assert d.cell_um is None and d.trim_value is None def test_dialog_defaults_overlay_config(tmp_path): cfg = load_config(_write(tmp_path, CLASSIC)) d = dialog_defaults(cfg) assert d.net == "VOUT+" assert d.current_a == pytest.approx(40.0) assert d.freq_hz == pytest.approx(142_000.0) assert d.layers == ["F.Cu", "In1.Cu", "B.Cu"] assert d.contact1 == "auto" def test_apply_physics_mutates_config_module(tmp_path, monkeypatch): monkeypatch.setattr(config, "VIA_PLATING_UM", 18.0) monkeypatch.setattr(config, "ELECTRODE_PDN_LAYER", "User.3") d = {"version": 1, "physics": {"via_plating_um": 30.0}, "markers": {"pdn_layer": "User.4"}} cfg = load_config(_write(tmp_path, json.dumps(d))) configfile.apply_physics(cfg) assert config.VIA_PLATING_UM == 30.0 assert config.ELECTRODE_PDN_LAYER == "User.4" def _selection(): return Selection(net="VOUT+", layers=["F.Cu", "B.Cu"], contact1="auto", contact2="all", current_a=12.5, cell_um=80.0, freq_hz=0.0, contact_model="equipotential", include_buildup=True, extra_cu_um=100.0, include_tracks=False, vias_capped=False, cap_max_drill_mm=0.6, adaptive=False, push_overlays=True, trim_enabled=True, trim_mode="abs", trim_value=2.0) def test_save_then_load_roundtrip(tmp_path): path = tmp_path / "fill_res_config.json" save_classic_config(path, _selection()) cfg = load_config(path) assert cfg.mode == "classic" assert cfg.net == "VOUT+" assert cfg.layers == ["F.Cu", "B.Cu"] assert cfg.current_a == pytest.approx(12.5) assert cfg.cell_um == pytest.approx(80.0) assert cfg.contact_model == "equipotential" assert cfg.contact2 == "all" assert cfg.include_tracks is False assert cfg.trim_enabled is True and cfg.trim_mode == "abs" assert cfg.trim_value == pytest.approx(2.0) def test_save_preserves_foreign_sections(tmp_path): path = _write(tmp_path, CLASSIC) save_classic_config(path, _selection()) cfg = load_config(path) assert cfg.physics == {"via_plating_um": 25.0} # kept from the old file assert [p.describe() for p in cfg.pos_parts] == ["J1.1"] assert cfg.current_a == pytest.approx(12.5) # new dialog value def test_classic_mode_may_carry_terminals(tmp_path): # mode is only the STARTING mode: a classic config keeps a # terminals section (the dialog's PDN mode offers it) d = json.loads(strip_comment_lines(PDN)) d["mode"] = "classic" cfg = load_config(_write(tmp_path, json.dumps(d))) assert cfg.mode == "classic" assert len(cfg.terminals) > 0 def test_save_classic_over_pdn_keeps_the_terminals(tmp_path): path = _write(tmp_path, PDN) old = load_config(path) save_classic_config(path, _selection()) cfg = load_config(path) assert cfg.mode == "classic" # starting mode flipped assert cfg.current_a == pytest.approx(12.5) assert [t.name for t in cfg.terminals] == [t.name for t in old.terminals] def test_save_refuses_broken_config(tmp_path): path = _write(tmp_path, "{ not json") with pytest.raises(ConfigError): save_classic_config(path, _selection()) assert path.read_text(encoding="utf-8") == "{ not json" # untouched def test_docs_examples_load(): """The copyable examples in docs/ must always parse.""" from pathlib import Path root = Path(__file__).resolve().parent.parent / "docs" classic = load_config(root / "fill_res_config.example.json") assert classic.mode == "classic" pdn = load_config(root / "fill_res_config.pdn.example.json") assert pdn.mode == "pdn" and len(pdn.terminals) == 5 # --- PDN save (dialog editor) ------------------------------------------------ def _pdn_selection(): sel = _selection() sel.mode = "pdn" sel.net = "VCC_3V3" sel.v_nominal = 3.3 return sel def test_rect_terminals_json_shapes(): rows = [PdnTerminalRow(name="VIN", role="supply", resolved="", r_out_ohm=0.01), PdnTerminalRow(name="L1", role="load", resolved="", i_draw_a=2.0)] infos = [(True, (0.0, 0.0, 1.0, 1.0)), (False, (10.0, 20.5, 12.25, 22.0))] tj = rect_terminals_json(rows, infos) assert tj[0]["parts"] == ["rect:VIN"] # labeled: live ref assert tj[0]["r_out_ohm"] == pytest.approx(0.01) assert "v_oc" not in tj[0] # None -> key omitted assert tj[1]["parts"] == [{"rect_mm": [10.0, 20.5, 12.25, 22.0]}] assert tj[1]["i_draw_a"] == pytest.approx(2.0) def test_updated_terminals_json_preserves_raw(): raw = [{"name": "buck", "role": "supply", "parts": ["U1.SW2"], "r_out_ohm": 0.004, "v_oc": 3.3, "_comment": "keep me"}, {"name": "mcu", "role": "load", "parts": ["U7"], "i_draw_a": 1.8}] rows = [PdnTerminalRow(name="buck", role="supply", resolved="", r_out_ohm=0.007, v_oc=None), PdnTerminalRow(name="mcu", role="load", resolved="", i_draw_a=2.2)] tj = updated_terminals_json(raw, rows) assert tj[0]["parts"] == ["U1.SW2"] assert tj[0]["_comment"] == "keep me" # verbatim carry-over assert tj[0]["r_out_ohm"] == pytest.approx(0.007) assert "v_oc" not in tj[0] # None removes the key assert tj[1]["i_draw_a"] == pytest.approx(2.2) assert raw[0]["r_out_ohm"] == pytest.approx(0.004) # deep-copied def test_rect_terminals_json_contact_layer(): rows = [PdnTerminalRow(name="VIN", role="supply", resolved="", r_out_ohm=0.01, contact="F.Cu"), PdnTerminalRow(name="L1", role="load", resolved="", i_draw_a=2.0, contact="all")] infos = [(True, (0.0, 0.0, 1.0, 1.0)), (True, (2.0, 0.0, 3.0, 1.0))] tj = rect_terminals_json(rows, infos) assert tj[0]["contact"] == "F.Cu" assert "contact" not in tj[1] # "all" is a rect's natural scope def test_updated_terminals_json_moves_the_contact_scope(): raw = [{"name": "buck", "role": "supply", "parts": ["U1.SW2"], "r_out_ohm": 0.004, "contact": "F.Cu"}, {"name": "mcu", "role": "load", "parts": ["U7"], "i_draw_a": 1.8}] rows = [PdnTerminalRow(name="buck", role="supply", resolved="", r_out_ohm=0.004, contact="auto"), PdnTerminalRow(name="mcu", role="load", resolved="", i_draw_a=1.8, contact="B.Cu")] tj = updated_terminals_json(raw, rows) assert "contact" not in tj[0] # "auto" restores the default assert tj[1]["contact"] == "B.Cu" def test_numbers_accept_si_suffix_strings(tmp_path): d = {"version": 1, "mode": "pdn", "run": {"net": "V", "v_nominal": "3300m"}, "terminals": [ {"name": "s", "role": "supply", "parts": ["U1"], "r_out_ohm": "50m"}, {"name": "l", "role": "load", "parts": ["U2"], "i_draw_a": "150m"}]} cfg = load_config(_write(tmp_path, json.dumps(d))) assert cfg.v_nominal == pytest.approx(3.3) assert cfg.terminals[0].r_out_ohm == pytest.approx(0.05) assert cfg.terminals[1].i_draw_a == pytest.approx(0.15) d["terminals"][0]["r_out_ohm"] = "5k5" # RKM style: rejected with pytest.raises(ConfigError, match="cannot parse number"): load_config(_write(tmp_path, json.dumps(d))) def test_inactive_terminal_may_omit_its_value(tmp_path): d = {"version": 1, "mode": "pdn", "run": {"net": "VCC"}, "terminals": [ {"name": "s", "role": "supply", "parts": ["U1"], "r_out_ohm": 0.01}, {"name": "l1", "role": "load", "parts": ["U2"], "i_draw_a": 1.0}, {"name": "l2", "role": "load", "parts": ["U3"], "active": False, "comment": "not fitted"}]} cfg = load_config(_write(tmp_path, json.dumps(d))) l2 = cfg.terminals[2] assert l2.active is False and l2.i_draw_a is None assert l2.comment == "not fitted" assert cfg.terminals[0].active is True and cfg.terminals[0].comment == "" def test_active_and_comment_in_the_save_builders(): rows = [PdnTerminalRow(name="VIN", role="supply", resolved="", r_out_ohm=0.01, comment="buck"), PdnTerminalRow(name="L1", role="load", resolved="", active=False, i_draw_a=None)] infos = [(True, (0.0, 0.0, 1.0, 1.0)), (True, (2.0, 0.0, 3.0, 1.0))] tj = rect_terminals_json(rows, infos) assert "active" not in tj[0] and tj[0]["comment"] == "buck" assert tj[1]["active"] is False assert "i_draw_a" not in tj[1] and "comment" not in tj[1] raw = [{"name": "VIN", "role": "supply", "parts": ["U1"], "r_out_ohm": 0.01, "comment": "old"}, {"name": "L1", "role": "load", "parts": ["U2"], "active": False}] rows[0].comment = "" # cleared -> key removed rows[1].active = True # re-enabled -> default again rows[1].i_draw_a = 2.0 uj = updated_terminals_json(raw, rows) assert "comment" not in uj[0] assert "active" not in uj[1] and uj[1]["i_draw_a"] == 2.0 def test_updated_terminals_json_moves_the_bonded_flag(): raw = [{"name": "pkg", "role": "load", "parts": ["U7"], "i_draw_a": 1.8, "bonded": True}, {"name": "heat", "role": "load", "parts": ["U9"], "i_draw_a": 2.0}] rows = [PdnTerminalRow(name="pkg", role="load", resolved="", i_draw_a=1.8, bonded=False), PdnTerminalRow(name="heat", role="load", resolved="", i_draw_a=2.0, bonded=True)] uj = updated_terminals_json(raw, rows) assert "bonded" not in uj[0] # unchecked removes the key assert uj[1]["bonded"] is True def test_inactive_row_saves_and_reloads(tmp_path): path = tmp_path / "fill_res_config.json" rows = [PdnTerminalRow(name="VIN", role="supply", resolved="", r_out_ohm=0.01), PdnTerminalRow(name="L1", role="load", resolved="", i_draw_a=2.0, comment="main draw"), PdnTerminalRow(name="L2", role="load", resolved="", active=False)] # blank value: still valid infos = [(True, (0.0, 0.0, 1.0, 1.0)), (True, (2.0, 0.0, 3.0, 1.0)), (True, (4.0, 0.0, 5.0, 1.0))] save_pdn_config(path, _pdn_selection(), rect_terminals_json(rows, infos)) cfg = load_config(path) assert cfg.terminals[2].active is False assert cfg.terminals[2].i_draw_a is None assert cfg.terminals[1].comment == "main draw" def test_save_pdn_config_roundtrip(tmp_path): path = tmp_path / "fill_res_config.json" rows = [PdnTerminalRow(name="VIN", role="supply", resolved="", r_out_ohm=0.01, v_oc=3.28), PdnTerminalRow(name="L1", role="load", resolved="", i_draw_a=2.0)] infos = [(True, (0.0, 0.0, 1.0, 1.0)), (False, (5.0, 5.0, 6.0, 6.0))] save_pdn_config(path, _pdn_selection(), rect_terminals_json(rows, infos)) cfg = load_config(path) assert cfg.mode == "pdn" assert cfg.net == "VCC_3V3" assert cfg.v_nominal == pytest.approx(3.3) vin, l1 = cfg.terminals assert vin.parts[0].kind == "rect_label" assert vin.parts[0].label == "VIN" assert vin.r_out_ohm == pytest.approx(0.01) assert vin.v_oc == pytest.approx(3.28) assert l1.parts[0].kind == "rect_mm" assert l1.i_draw_a == pytest.approx(2.0) def test_save_pdn_into_classic_file_preserves_sections(tmp_path): path = _write(tmp_path, CLASSIC) rows = [PdnTerminalRow(name="S1", role="supply", resolved="", r_out_ohm=0.0), PdnTerminalRow(name="L1", role="load", resolved="", i_draw_a=1.0)] infos = [(False, (0.0, 0.0, 1.0, 1.0)), (False, (2.0, 0.0, 3.0, 1.0))] save_pdn_config(path, _pdn_selection(), rect_terminals_json(rows, infos)) cfg = load_config(path) assert cfg.mode == "pdn" and len(cfg.terminals) == 2 # the classic section survived for a later hand-edit back assert cfg.current_a == pytest.approx(40.0) assert [p.describe() for p in cfg.pos_parts] == ["J1.1"] assert cfg.physics == {"via_plating_um": 25.0} def test_save_pdn_into_pdn_file_updates_values(tmp_path): path = _write(tmp_path, PDN) old = load_config(path) rows = [] for spec in old.terminals: rows.append(PdnTerminalRow( name=spec.name, role=spec.role, resolved="", i_draw_a=(spec.i_draw_a + 1.0 if spec.role == "load" else None), r_out_ohm=(spec.r_out_ohm * 2 if spec.role == "supply" else None), v_oc=spec.v_oc)) save_pdn_config(path, _pdn_selection(), updated_terminals_json(old.raw["terminals"], rows)) cfg = load_config(path) assert [t.name for t in cfg.terminals] == [t.name for t in old.terminals] # partrefs are byte-identical carry-overs, only values moved assert cfg.terminals[0].parts[0].describe() == "U1.SW2" assert cfg.terminals[0].r_out_ohm == pytest.approx(0.008) assert cfg.terminals[2].i_draw_a == pytest.approx(2.8) assert cfg.terminals[3].parts[0].label == "CAM_ZONE" def test_save_pdn_refuses_broken_file(tmp_path): path = _write(tmp_path, "{ not json") with pytest.raises(ConfigError): save_pdn_config(path, _pdn_selection(), []) assert path.read_text(encoding="utf-8") == "{ not json" def test_save_pdn_self_validates_before_writing(tmp_path): path = tmp_path / "fill_res_config.json" # a load without a draw is invalid - the save must refuse instead # of writing a config the next launch rejects bad = [{"name": "L1", "role": "load", "parts": ["U1"]}] with pytest.raises(ConfigError, match="i_draw_a"): save_pdn_config(path, _pdn_selection(), bad) assert not path.exists() def test_classic_save_still_omits_v_nominal(tmp_path): path = tmp_path / "fill_res_config.json" save_classic_config(path, _selection()) # v_nominal stays None raw = json.loads(path.read_text(encoding="utf-8")) assert "v_nominal" not in raw["run"] def test_bonded_key_parses_and_validates(tmp_path): d = {"version": 1, "run": {"net": "V"}, "terminals": [ {"name": "s", "role": "supply", "parts": ["U1"], "r_out_ohm": 0}, {"name": "pkg", "role": "load", "parts": ["U2"], "i_draw_a": 3.0, "bonded": True}]} cfg = load_config(_write(tmp_path, json.dumps(d))) assert cfg.terminals[0].bonded is False assert cfg.terminals[1].bonded is True d["terminals"][1]["bonded"] = "yes" with pytest.raises(ConfigError, match="bonded"): load_config(_write(tmp_path, json.dumps(d), name="b.json")) def test_bonded_survives_the_save_roundtrip(tmp_path): path = tmp_path / "fill_res_config.json" rows = [PdnTerminalRow(name="VIN", role="supply", resolved="", r_out_ohm=0.01), PdnTerminalRow(name="PKG", role="load", resolved="", i_draw_a=2.0, bonded=True)] infos = [(True, (0.0, 0.0, 1.0, 1.0)), (True, (5.0, 5.0, 6.0, 6.0))] tj = rect_terminals_json(rows, infos) assert "bonded" not in tj[0] # false -> key omitted assert tj[1]["bonded"] is True save_pdn_config(path, _pdn_selection(), tj) cfg = load_config(path) assert cfg.terminals[0].bonded is False assert cfg.terminals[1].bonded is True