From 039b1212b91d2e6bea5415fcb338771906e295a0 Mon Sep 17 00:00:00 2001 From: sjat Date: Wed, 24 Jun 2026 13:51:52 +0200 Subject: [PATCH] feat(rack): render page and orchestrate generation --- scripts/gen_rack.py | 84 ++++++++++++++++++++++++++++++++++++++++++ tests/test_gen_rack.py | 52 ++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) diff --git a/scripts/gen_rack.py b/scripts/gen_rack.py index a9d592a..a627a23 100644 --- a/scripts/gen_rack.py +++ b/scripts/gen_rack.py @@ -235,3 +235,87 @@ def render_svg(rack: str, items: list[dict]) -> str: p.append("") return "\n".join(p) + "\n" + + +def render_page(rack: str, items: list[dict]) -> str: + items = _sorted_items(items) + lines: list[str] = [] + lines.append(f"# Rack {rack}") + lines.append("") + lines.append( + f"_Auto-generated from `docs/hardware/*.md` (items with `rack: {rack}`) " + f"— do not edit by hand. Run `make docs-index` after changing a " + f"source file._" + ) + lines.append("") + lines.append("## Elevation") + lines.append("") + lines.append(f"![Rack {rack} elevation]({rack}-elevation.svg)") + lines.append("") + lines.append("## Occupancy") + lines.append("") + lines.append("| U | Device | Kind | Face | Status |") + lines.append("|---|---|---|---|---|") + for fm in items: + name = fm.get("hostname", "?") + link = f"[{name}](../../hardware/{name}.md)" + face = fm.get("rack_face", "") + if face in ZERO_U_FACES: + urange = "0U" + else: + u = fm["rack_u"] + h = fm["u_height"] + urange = f"U{u}" if h == 1 else f"U{u}–U{u + h - 1}" + lines.append( + f"| {urange} | {link} | {fm.get('kind', '')} | {face} " + f"| {fm.get('status', '')} |" + ) + lines.append("") + return "\n".join(lines).rstrip() + "\n" + + +def generate(hardware_dir: Path, output_dir: Path) -> int: + items = load_rack_items(hardware_dir) + + errors: list[str] = [] + for fm in items: + try: + validate_item(fm) + except SchemaError as e: + errors.append(str(e)) + + racks: dict[str, list[dict]] = {} + for fm in items: + racks.setdefault(fm["rack"], []).append(fm) + + if not errors: # only check overlaps once placements are individually valid + for rack, ritems in racks.items(): + try: + check_overlaps(ritems) + except SchemaError as e: + errors.append(f"{rack}: {e}") + + if errors: + for err in errors: + print(f"ERROR: {err}", file=sys.stderr) + return 1 + + output_dir.mkdir(parents=True, exist_ok=True) + for rack in sorted(racks): + ritems = racks[rack] + (output_dir / f"{rack}-elevation.svg").write_text( + render_svg(rack, ritems), encoding="utf-8" + ) + (output_dir / f"{rack}.md").write_text( + render_page(rack, ritems), encoding="utf-8" + ) + print(f"Wrote {rack}.md + {rack}-elevation.svg ({len(ritems)} item(s))") + return 0 + + +def main() -> int: + return generate(HARDWARE_DIR, OUTPUT_DIR) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_gen_rack.py b/tests/test_gen_rack.py index 4c073f1..cdd44f1 100644 --- a/tests/test_gen_rack.py +++ b/tests/test_gen_rack.py @@ -99,3 +99,55 @@ def test_render_svg_is_deterministic(): assert gen_rack.render_svg("rack01", items) == gen_rack.render_svg( "rack01", list(reversed(items)) ) + + +def test_render_page_has_banner_image_and_table(): + items = [item(hostname="mf00", rack_u=1, u_height=2, rack_face="front")] + page = gen_rack.render_page("rack01", items) + assert "do not edit by hand" in page + assert "![Rack rack01 elevation](rack01-elevation.svg)" in page + assert "../../hardware/mf00.md" in page + assert "U1–U2" in page + + +def _write_item(d, name, body): + (d / f"{name}.md").write_text(body, encoding="utf-8") + + +def test_generate_writes_artifacts(tmp_path): + hw = tmp_path / "hardware" + out = tmp_path / "out" + hw.mkdir() + _write_item( + hw, + "mf00", + "---\nhostname: mf00\nkind: server\nstatus: in-use\n" + "rack: rack01\nrack_u: 1\nu_height: 1\nrack_face: front\n---\n", + ) + # a non-rack file must be ignored + _write_item(hw, "cloud", "---\nhostname: cloud\nkind: server\nstatus: in-use\n---\n") + + rc = gen_rack.generate(hw, out) + + assert rc == 0 + assert (out / "rack01.md").exists() + assert (out / "rack01-elevation.svg").exists() + assert "mf00" in (out / "rack01-elevation.svg").read_text() + + +def test_generate_returns_1_on_overlap(tmp_path): + hw = tmp_path / "hardware" + out = tmp_path / "out" + hw.mkdir() + for n, u in (("a", 1), ("b", 1)): + _write_item( + hw, + n, + f"---\nhostname: {n}\nkind: server\nstatus: in-use\n" + f"rack: rack01\nrack_u: {u}\nu_height: 1\nrack_face: front\n---\n", + ) + + rc = gen_rack.generate(hw, out) + + assert rc == 1 + assert not (out / "rack01.md").exists()