Some checks failed
Create Examples / build (ubuntu-22.04, 3.7) (push) Has been cancelled
Create Examples / build (ubuntu-22.04, 3.8) (push) Has been cancelled
Create Examples / build (ubuntu-latest, 3.10) (push) Has been cancelled
Create Examples / build (ubuntu-latest, 3.11) (push) Has been cancelled
Create Examples / build (ubuntu-latest, 3.12) (push) Has been cancelled
Create Examples / build (ubuntu-latest, 3.9) (push) Has been cancelled
Add `url` field to Component, WireClass, and Cable with per-wire URL support for bundles. URLs produce clickable href attributes on GraphViz nodes/edges and appear as a linked column in HTML BOM output.
315 lines
10 KiB
Python
315 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
"""Tests for URL/href support (port of upstream PR #168).
|
|
|
|
Covers:
|
|
- url field on Connector, Cable, AdditionalBomItem, WireClass
|
|
- BOM hash differentiation by URL
|
|
- BOM list URL column presence/absence
|
|
- GraphViz href on nodes and edges
|
|
- HTML output <a> tag wrapping
|
|
"""
|
|
|
|
import re
|
|
|
|
import pytest
|
|
|
|
from wireviz.wv_bom import BomHash, bom_list
|
|
from wireviz.wv_dataclasses import (
|
|
AdditionalBomItem,
|
|
Cable,
|
|
Connector,
|
|
Metadata,
|
|
Options,
|
|
Tweak,
|
|
)
|
|
|
|
|
|
def make_connector(**kwargs):
|
|
args = {"designator": "X1", "pins": [1, 2, 3]}
|
|
args.update(kwargs)
|
|
return Connector(**args)
|
|
|
|
|
|
def make_cable(**kwargs):
|
|
args = {"designator": "W1", "wirecount": 3, "colors": ["BK", "RD", "GN"]}
|
|
args.update(kwargs)
|
|
return Cable(**args)
|
|
|
|
|
|
def make_harness():
|
|
from wireviz.wv_harness import Harness
|
|
|
|
return Harness(
|
|
metadata=Metadata({}),
|
|
options=Options(),
|
|
tweak=Tweak(),
|
|
)
|
|
|
|
|
|
# --- Data model ---
|
|
|
|
|
|
class TestUrlDataModel:
|
|
def test_connector_url_default_none(self):
|
|
c = make_connector()
|
|
assert c.url is None
|
|
|
|
def test_connector_url_scalar(self):
|
|
c = make_connector(url="https://example.com/connector")
|
|
assert c.url == "https://example.com/connector"
|
|
|
|
def test_cable_url_default_none(self):
|
|
c = make_cable()
|
|
assert c.url is None
|
|
|
|
def test_cable_url_scalar(self):
|
|
c = make_cable(url="https://example.com/cable")
|
|
assert c.url == "https://example.com/cable"
|
|
|
|
def test_bundle_url_list(self):
|
|
urls = [
|
|
"https://example.com/wire1",
|
|
"https://example.com/wire2",
|
|
"https://example.com/wire3",
|
|
]
|
|
c = make_cable(category="bundle", url=urls)
|
|
assert c.url == urls
|
|
# per-wire URLs should be propagated to WireClass objects
|
|
for i, wire in enumerate(c.wire_objects.values()):
|
|
if hasattr(wire, "url"):
|
|
assert wire.url == urls[i]
|
|
|
|
def test_bundle_url_scalar_applies_to_all_wires(self):
|
|
c = make_cable(category="bundle", url="https://example.com/all")
|
|
for wire in c.wire_objects.values():
|
|
if hasattr(wire, "url"):
|
|
assert wire.url == "https://example.com/all"
|
|
|
|
def test_bundle_url_list_wrong_length_raises(self):
|
|
with pytest.raises(Exception, match="URL list length must match wirecount"):
|
|
make_cable(category="bundle", url=["a", "b"]) # wirecount=3
|
|
|
|
def test_non_bundle_url_list_raises(self):
|
|
with pytest.raises(Exception, match="URL lists are only supported for bundles"):
|
|
make_cable(url=["a", "b", "c"]) # not a bundle
|
|
|
|
def test_additional_bom_item_url(self):
|
|
item = AdditionalBomItem(
|
|
type="Heatshrink",
|
|
url="https://example.com/heatshrink",
|
|
)
|
|
assert item.url == "https://example.com/heatshrink"
|
|
|
|
|
|
# --- BOM hashing ---
|
|
|
|
|
|
class TestUrlBomHash:
|
|
def test_different_urls_different_hashes(self):
|
|
c1 = make_connector(url="https://example.com/a")
|
|
c2 = make_connector(url="https://example.com/b")
|
|
assert c1.bom_hash != c2.bom_hash
|
|
|
|
def test_same_urls_same_hashes(self):
|
|
c1 = make_connector(url="https://example.com/same")
|
|
c2 = make_connector(url="https://example.com/same")
|
|
assert c1.bom_hash == c2.bom_hash
|
|
|
|
def test_url_vs_no_url_different_hashes(self):
|
|
c1 = make_connector(url="https://example.com/a")
|
|
c2 = make_connector()
|
|
assert c1.bom_hash != c2.bom_hash
|
|
|
|
def test_wire_class_url_in_hash(self):
|
|
c1 = make_cable(
|
|
category="bundle",
|
|
url=["https://a.com", "https://b.com", "https://c.com"],
|
|
)
|
|
c2 = make_cable(
|
|
category="bundle",
|
|
url=["https://x.com", "https://b.com", "https://c.com"],
|
|
)
|
|
# First wire has different URL, so first wire's hash should differ
|
|
wire1_c1 = list(c1.wire_objects.values())[0]
|
|
wire1_c2 = list(c2.wire_objects.values())[0]
|
|
assert wire1_c1.bom_hash != wire1_c2.bom_hash
|
|
# Second wire has same URL, should match
|
|
wire2_c1 = list(c1.wire_objects.values())[1]
|
|
wire2_c2 = list(c2.wire_objects.values())[1]
|
|
assert wire2_c1.bom_hash == wire2_c2.bom_hash
|
|
|
|
|
|
# --- BOM list ---
|
|
|
|
|
|
class TestUrlBomList:
|
|
def _make_bom_with_url(self, url=None):
|
|
harness = make_harness()
|
|
harness.add_connector("X1", pins=[1], url=url)
|
|
harness.add_cable("W1", wirecount=1, colors=["BK"])
|
|
harness.connect("X1", 1, "W1", 1, None, None)
|
|
harness.populate_bom()
|
|
return bom_list(harness.bom)
|
|
|
|
def test_url_column_present_when_used(self):
|
|
rows = self._make_bom_with_url("https://example.com")
|
|
headers = rows[0]
|
|
assert "URL" in headers
|
|
url_idx = headers.index("URL")
|
|
# at least one row should have the URL value
|
|
assert any(row[url_idx] == "https://example.com" for row in rows[1:])
|
|
|
|
def test_url_column_absent_when_unused(self):
|
|
rows = self._make_bom_with_url(None)
|
|
headers = rows[0]
|
|
assert "URL" not in headers
|
|
|
|
|
|
# --- GraphViz href ---
|
|
|
|
|
|
class TestUrlGraphviz:
|
|
def test_connector_node_href(self):
|
|
harness = make_harness()
|
|
harness.add_connector("X1", pins=[1], url="https://example.com/x1")
|
|
harness.add_cable("W1", wirecount=1, colors=["BK"])
|
|
harness.connect("X1", 1, "W1", 1, None, None)
|
|
graph = harness.create_graph()
|
|
gv = graph.source
|
|
assert 'href="https://example.com/x1"' in gv
|
|
|
|
def test_cable_node_href(self):
|
|
harness = make_harness()
|
|
harness.add_connector("X1", pins=[1])
|
|
harness.add_cable("W1", wirecount=1, colors=["BK"], url="https://example.com/w1")
|
|
harness.connect("X1", 1, "W1", 1, None, None)
|
|
graph = harness.create_graph()
|
|
gv = graph.source
|
|
assert 'href="https://example.com/w1"' in gv
|
|
|
|
def test_no_url_produces_empty_href(self):
|
|
harness = make_harness()
|
|
harness.add_connector("X1", pins=[1])
|
|
harness.add_cable("W1", wirecount=1, colors=["BK"])
|
|
harness.connect("X1", 1, "W1", 1, None, None)
|
|
graph = harness.create_graph()
|
|
gv = graph.source
|
|
# all href attributes should be empty (no real URLs in href values)
|
|
hrefs = re.findall(r'href="([^"]*)"', gv)
|
|
assert all(h == '' for h in hrefs), f"Expected all empty hrefs, got: {hrefs}"
|
|
|
|
def test_wire_edge_href_scalar(self):
|
|
harness = make_harness()
|
|
harness.add_connector("X1", pins=[1])
|
|
harness.add_cable("W1", wirecount=1, colors=["BK"], url="https://example.com/wire")
|
|
harness.connect("X1", 1, "W1", 1, None, None)
|
|
graph = harness.create_graph()
|
|
gv = graph.source
|
|
# The wire edge should have the cable URL as href
|
|
# Look for href in edge attributes
|
|
edge_hrefs = re.findall(r'href="(https://example\.com/wire)"', gv)
|
|
assert len(edge_hrefs) >= 1
|
|
|
|
def test_loop_edges_clear_href(self):
|
|
harness = make_harness()
|
|
harness.add_connector(
|
|
"X1",
|
|
pins=[1, 2, 3],
|
|
url="https://example.com/x1",
|
|
loops=[[1, 2]],
|
|
)
|
|
harness.add_cable("W1", wirecount=1, colors=["BK"])
|
|
harness.connect("X1", 3, "W1", 1, None, None)
|
|
graph = harness.create_graph()
|
|
gv = graph.source
|
|
# Loop edge attr should clear href
|
|
# The connector node should have the real href
|
|
assert 'href="https://example.com/x1"' in gv
|
|
|
|
def test_mate_edges_clear_href(self):
|
|
harness = make_harness()
|
|
harness.add_connector("X1", pins=[1], url="https://example.com/x1")
|
|
harness.add_connector("X2", pins=[1], url="https://example.com/x2")
|
|
harness.add_mate_component("X1", "X2", "->")
|
|
graph = harness.create_graph()
|
|
gv = graph.source
|
|
# Both connectors should have hrefs, mate edge should not leak them
|
|
assert 'href="https://example.com/x1"' in gv
|
|
assert 'href="https://example.com/x2"' in gv
|
|
|
|
|
|
# --- HTML output ---
|
|
|
|
|
|
class TestUrlHtmlOutput:
|
|
def test_url_wrapped_in_anchor(self):
|
|
"""URL values in BOM should be wrapped in <a> tags in HTML."""
|
|
harness = make_harness()
|
|
harness.add_connector("X1", pins=[1], url="https://example.com/part")
|
|
harness.add_cable("W1", wirecount=1, colors=["BK"])
|
|
harness.connect("X1", 1, "W1", 1, None, None)
|
|
harness.populate_bom()
|
|
bomlist = bom_list(harness.bom)
|
|
|
|
# Find URL column index
|
|
headers = bomlist[0]
|
|
assert "URL" in headers
|
|
url_idx = headers.index("URL")
|
|
|
|
# The connector row should have the URL
|
|
url_values = [row[url_idx] for row in bomlist[1:] if row[url_idx]]
|
|
assert "https://example.com/part" in url_values
|
|
|
|
def test_empty_url_not_wrapped(self):
|
|
"""Rows without URLs should not get anchor tags."""
|
|
harness = make_harness()
|
|
harness.add_connector("X1", pins=[1], url="https://example.com/part")
|
|
harness.add_connector("X2", pins=[1]) # no URL
|
|
harness.add_cable("W1", wirecount=1, colors=["BK"])
|
|
harness.connect("X1", 1, "W1", 1, "X2", 1)
|
|
harness.populate_bom()
|
|
bomlist = bom_list(harness.bom)
|
|
|
|
headers = bomlist[0]
|
|
if "URL" in headers:
|
|
url_idx = headers.index("URL")
|
|
# At least one row should have None/empty URL
|
|
null_urls = [row[url_idx] for row in bomlist[1:] if not row[url_idx]]
|
|
assert len(null_urls) >= 1
|
|
|
|
|
|
# --- Backward compatibility ---
|
|
|
|
|
|
class TestBackwardCompatibility:
|
|
def test_connector_without_url(self):
|
|
c = make_connector()
|
|
assert c.bom_hash is not None
|
|
assert c.url is None
|
|
|
|
def test_cable_without_url(self):
|
|
c = make_cable()
|
|
assert c.bom_hash is not None
|
|
assert c.url is None
|
|
|
|
def test_bundle_without_url(self):
|
|
c = make_cable(category="bundle")
|
|
for wire in c.wire_objects.values():
|
|
assert wire.bom_hash is not None
|
|
if hasattr(wire, "url"):
|
|
assert wire.url is None
|
|
|
|
def test_full_harness_without_urls(self):
|
|
harness = make_harness()
|
|
harness.add_connector("X1", pins=[1, 2])
|
|
harness.add_connector("X2", pins=[1, 2])
|
|
harness.add_cable("W1", wirecount=2, colors=["BK", "RD"])
|
|
harness.connect("X1", 1, "W1", 1, "X2", 1)
|
|
harness.connect("X1", 2, "W1", 2, "X2", 2)
|
|
harness.populate_bom()
|
|
bomlist = bom_list(harness.bom)
|
|
headers = bomlist[0]
|
|
# URL column should be auto-removed when no URLs are used
|
|
assert "URL" not in headers
|