test: infamy: Allow tests to refine the topology matching process

To run LAG tests on hardware, we have to make sure that the chosen
links are running at the same speed.  We do not want to require a
specific link speed from the logical topology, as the real requirement
is merely that the two (or however many) links are of the _same_
speed, not any _particular_ speed.

Rather than having to complicate the topology matcher, let it take
care of the common pattern of matching requirements to provided
features, then let tests pass custom node/edge matchers for the
complicated cases.
This commit is contained in:
Tobias Waldekranz
2025-02-12 23:22:09 +01:00
parent a6b79857db
commit da39855cec
2 changed files with 25 additions and 20 deletions
+6 -2
View File
@@ -65,7 +65,9 @@ def test_argument(option, **kwargs):
class Env(object):
def __init__(self, ltop=None, args=None, argv=sys.argv[1::], environ=os.environ):
def __init__(self, ltop=None, args=None, argv=sys.argv[1::], environ=os.environ,
nodes_compatible=topology.compatible,
edge_mappings=topology.edge_mappings):
if "INFAMY_ARGS" in environ:
argv = shlex.split(environ["INFAMY_ARGS"]) + argv
@@ -89,7 +91,9 @@ class Env(object):
ldot = pydot.graph_from_dot_file(top_path)[0]
self.ltop = topology.Topology(ldot)
if not self.ltop.map_to(self.ptop):
if not self.ltop.map_to(self.ptop,
nodes_compatible=nodes_compatible,
edge_mappings=edge_mappings):
raise tap.TestSkip()
print(repr(self.ltop))
+19 -18
View File
@@ -14,25 +14,17 @@ def _qstrip(text):
return text
def compatible(physical, logical):
provides_set = set(physical.get("provides", "").split())
requires_set = set(logical.get("requires", "").split())
return logical["requires"].issubset(physical["provides"])
return requires_set.issubset(provides_set)
def map_edges(les, pes):
def edge_mappings(les, pes):
les = les.values()
pes = pes.values()
for perm in permutations(pes, len(les)):
candidate = tuple(zip(les, perm))
if all(map(lambda pair: compatible(pair[1], pair[0]), candidate)):
return candidate
yield candidate
def match_node(pn, ln):
return compatible(pn, ln)
def match_edge(pes, les):
return map_edges(les, pes) is not None
class Topology:
def __init__(self, dotg):
@@ -46,6 +38,9 @@ class Topology:
repr(n.get_attributes())
attrs = { _qstrip(k): _qstrip(v) for k, v in n.get_attributes().items() if k != "label" }
for attr in ("requires", "provides"):
attrs[attr] = set(attrs.get(attr, "").split())
self.g.add_node(name, **attrs)
for e in self.dotg.get_edges():
@@ -55,6 +50,10 @@ class Topology:
attrs = {_qstrip(k): _qstrip(v) for k, v in e.get_attributes().items()}
attrs[sn] = sp
attrs[dn] = dp
for attr in ("requires", "provides"):
attrs[attr] = set(attrs.get(attr, "").split())
self.g.add_edge(sn, dn, **attrs)
def __repr__(self):
@@ -73,13 +72,15 @@ class Topology:
return out
def map_to(self, phy):
def map_to(self, phy,
nodes_compatible=compatible, edge_mappings=edge_mappings):
mapper = isomorphism.MultiGraphMatcher(phy.g, self.g,
edge_match=match_edge,
node_match=match_node)
edge_match=lambda pes, les: any(edge_mappings(les, pes)),
node_match=nodes_compatible)
if not mapper.subgraph_is_monomorphic():
return False
# breakpoint()
self.phy = phy
self.mapping = {}
@@ -93,7 +94,7 @@ class Topology:
les = self.g.get_edge_data(lsrc, ldst)
pes = self.phy.g.get_edge_data(psrc, pdst)
for le, pe in map_edges(les, pes):
for le, pe in next(edge_mappings(les, pes)):
self.mapping[lsrc][le[lsrc]] = pe[psrc]
self.mapping[ldst][le[ldst]] = pe[pdst]
@@ -139,15 +140,15 @@ class Topology:
return None
def get_mgmt_link(self, src, dst):
return self.get_link(src, dst, lambda e: compatible(e, {"requires": "mgmt"}))
return self.get_link(src, dst, lambda e: compatible(e, {"requires": {"mgmt"}}))
def get_ctrl(self):
ns = self.get_nodes(lambda _, attrs: compatible(attrs, {"requires": "controller"}))
ns = self.get_nodes(lambda _, attrs: compatible(attrs, {"requires": {"controller"}}))
assert len(ns) == 1
return ns[0]
def get_infixen(self):
return self.get_nodes(lambda _, attrs: compatible(attrs, {"requires": "infix"}))
return self.get_nodes(lambda _, attrs: compatible(attrs, {"requires": {"infix"}}))
def get_attr(self, name, default=None):