diff --git a/scripts/gen_rack.py b/scripts/gen_rack.py index 1b01fc7..a9d592a 100644 --- a/scripts/gen_rack.py +++ b/scripts/gen_rack.py @@ -115,3 +115,123 @@ def check_overlaps(items: list[dict]) -> None: f"U{uu} {f}: {name} overlaps {occupied[key]}" ) occupied[key] = name + + +def _esc(s: object) -> str: + return str(s).replace("&", "&").replace("<", "<").replace(">", ">") + + +def _sorted_items(items: list[dict]) -> list[dict]: + """Deterministic order: faced items by U then hostname, 0U items last.""" + return sorted( + items, + key=lambda i: ( + 0 if i.get("rack_face") not in ZERO_U_FACES else 1, + i.get("rack_u", 0) if isinstance(i.get("rack_u"), int) else 0, + i.get("hostname", ""), + ), + ) + + +def render_svg(rack: str, items: list[dict]) -> str: + U_H = 20 + COL_W = 240 + LABEL_W = 30 + RAIL_W = 16 + PAD = 12 + GAP = 50 + TITLE_H = 28 + + items = _sorted_items(items) + left_items = [i for i in items if i.get("rack_face") == "left"] + right_items = [i for i in items if i.get("rack_face") == "right"] + + body_h = RACK_UNITS * U_H + height = PAD + TITLE_H + body_h + PAD + front_x = PAD + len(left_items) * RAIL_W + LABEL_W + rear_x = front_x + COL_W + GAP + width = rear_x + COL_W + len(right_items) * RAIL_W + PAD + top = PAD + TITLE_H + + def u_y(u: int) -> int: + # U1 at the top; U numbers increase downward. + return top + (u - 1) * U_H + + p: list[str] = [] + p.append( + f'' + ) + p.append(f'') + p.append( + f'Rack {_esc(rack)}' + ) + + for col_x, col_label in ((front_x, "front"), (rear_x, "rear")): + p.append( + f'{col_label}' + ) + for u in range(1, RACK_UNITS + 1): + y = u_y(u) + p.append( + f'' + ) + + # U numbers in the gutter left of the front column. + for u in range(1, RACK_UNITS + 1): + y = u_y(u) + p.append( + f'{u}' + ) + + def draw_device(fm: dict, col_x: int) -> None: + u = fm["rack_u"] + h = fm["u_height"] + y = u_y(u) + box_h = h * U_H + color = KIND_COLORS.get(fm.get("kind", ""), DEFAULT_COLOR) + name = fm.get("hostname", "?") + urange = f"U{u}" if h == 1 else f"U{u}–U{u + h - 1}" + p.append( + f'' + ) + p.append( + f'' + f'{_esc(name)} ({urange})' + ) + + for fm in items: + face = fm.get("rack_face") + if face in ("front", "both"): + draw_device(fm, front_x) + if face in ("rear", "both"): + draw_device(fm, rear_x) + + def draw_rail(fm: dict, x: int) -> None: + color = KIND_COLORS.get(fm.get("kind", ""), DEFAULT_COLOR) + name = fm.get("hostname", "?") + cx = x + RAIL_W // 2 + cy = top + body_h // 2 + p.append( + f'' + ) + p.append( + f'{_esc(name)}' + ) + + for idx, fm in enumerate(left_items): + draw_rail(fm, PAD + idx * RAIL_W) + for idx, fm in enumerate(right_items): + draw_rail(fm, rear_x + COL_W + idx * RAIL_W) + + p.append("") + return "\n".join(p) + "\n" diff --git a/tests/test_gen_rack.py b/tests/test_gen_rack.py index 3829512..4c073f1 100644 --- a/tests/test_gen_rack.py +++ b/tests/test_gen_rack.py @@ -74,3 +74,28 @@ def test_overlaps_ignores_zero_u_rails(): item(hostname="p2", rack_face="left"), ] gen_rack.check_overlaps(items) # no raise + + +def test_render_svg_has_two_columns_of_48_slots(): + svg = gen_rack.render_svg("rack01", []) + # one faint slot rect per U per column (front + rear) + assert svg.count('fill="#f5f5f5"') == 2 * gen_rack.RACK_UNITS + assert svg.startswith("") + + +def test_render_svg_includes_device_label(): + items = [item(hostname="mf00", rack_u=1, u_height=2, rack_face="front")] + svg = gen_rack.render_svg("rack01", items) + assert "mf00" in svg + assert "U1" in svg + + +def test_render_svg_is_deterministic(): + items = [ + item(hostname="b", rack_u=3, u_height=1, rack_face="front"), + item(hostname="a", rack_u=1, u_height=1, rack_face="rear"), + ] + assert gen_rack.render_svg("rack01", items) == gen_rack.render_svg( + "rack01", list(reversed(items)) + )