Meshroom/tests/test_nodeAttributeChangedCallback.py
Yann Lanthony 4aec741a89 [core] Graph: add importGraphContent API
Extract the logic of importing the content of a graph within a graph instance from
the graph loading logic.
Add `Graph.importGraphContent` and `Graph.importGraphContentFromFile`
methods.
Use the deserialization API to load the content in another temporary graph instance,
to handle the renaming of nodes using the Graph API, rather than manipulating
entries in a raw dictionnary.
2025-02-06 16:46:04 +01:00

458 lines
15 KiB
Python

# coding:utf-8
from meshroom.core.graph import Graph, loadGraph, executeGraph
from meshroom.core import desc, registerNodeType, unregisterNodeType
from meshroom.core.node import Node
class NodeWithAttributeChangedCallback(desc.Node):
"""
A Node containing an input Attribute with an 'on{Attribute}Changed' method,
called whenever the value of this attribute is changed explicitly.
"""
inputs = [
desc.IntParam(
name="input",
label="Input",
description="Attribute with a value changed callback (onInputChanged)",
value=0,
range=None,
),
desc.IntParam(
name="affectedInput",
label="Affected Input",
description="Updated to input.value * 2 whenever 'input' is explicitly modified",
value=0,
range=None,
),
]
def onInputChanged(self, instance: Node):
instance.affectedInput.value = instance.input.value * 2
def processChunk(self, chunk):
pass # No-op.
class TestNodeWithAttributeChangedCallback:
@classmethod
def setup_class(cls):
registerNodeType(NodeWithAttributeChangedCallback)
@classmethod
def teardown_class(cls):
unregisterNodeType(NodeWithAttributeChangedCallback)
def test_assignValueTriggersCallback(self):
node = Node(NodeWithAttributeChangedCallback.__name__)
assert node.affectedInput.value == 0
node.input.value = 10
assert node.affectedInput.value == 20
def test_specifyDefaultValueDoesNotTriggerCallback(self):
node = Node(NodeWithAttributeChangedCallback.__name__, input=10)
assert node.affectedInput.value == 0
def test_assignDefaultValueDoesNotTriggerCallback(self):
node = Node(NodeWithAttributeChangedCallback.__name__, input=10)
node.input.value = 10
assert node.affectedInput.value == 0
def test_assignNonDefaultValueTriggersCallback(self):
node = Node(NodeWithAttributeChangedCallback.__name__, input=10)
node.input.value = 2
assert node.affectedInput.value == 4
class TestAttributeCallbackTriggerInGraph:
@classmethod
def setup_class(cls):
registerNodeType(NodeWithAttributeChangedCallback)
@classmethod
def teardown_class(cls):
unregisterNodeType(NodeWithAttributeChangedCallback)
def test_connectionTriggersCallback(self):
graph = Graph("")
nodeA = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
assert nodeA.affectedInput.value == nodeB.affectedInput.value == 0
nodeA.input.value = 1
graph.addEdge(nodeA.input, nodeB.input)
assert nodeA.affectedInput.value == nodeB.affectedInput.value == 2
def test_connectedValueChangeTriggersCallback(self):
graph = Graph("")
nodeA = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
assert nodeA.affectedInput.value == nodeB.affectedInput.value == 0
graph.addEdge(nodeA.input, nodeB.input)
nodeA.input.value = 1
assert nodeA.affectedInput.value == 2
assert nodeB.affectedInput.value == 2
def test_defaultValueOnlyTriggersCallbackDownstream(self):
graph = Graph("")
nodeA = graph.addNewNode(NodeWithAttributeChangedCallback.__name__, input=1)
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
assert nodeA.affectedInput.value == 0
assert nodeB.affectedInput.value == 0
graph.addEdge(nodeA.input, nodeB.input)
assert nodeA.affectedInput.value == 0
assert nodeB.affectedInput.value == 2
def test_valueChangeIsPropagatedAlongNodeChain(self):
graph = Graph("")
nodeA = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
nodeC = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
nodeD = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
graph.addEdges(
(nodeA.affectedInput, nodeB.input),
(nodeB.affectedInput, nodeC.input),
(nodeC.affectedInput, nodeD.input),
)
nodeA.input.value = 5
assert nodeA.affectedInput.value == nodeB.input.value == 10
assert nodeB.affectedInput.value == nodeC.input.value == 20
assert nodeC.affectedInput.value == nodeD.input.value == 40
assert nodeD.affectedInput.value == 80
def test_disconnectionTriggersCallback(self):
graph = Graph("")
nodeA = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
graph.addEdge(nodeA.input, nodeB.input)
nodeA.input.value = 5
assert nodeB.affectedInput.value == 10
graph.removeEdge(nodeB.input)
assert nodeB.input.value == 0
assert nodeB.affectedInput.value == 0
def test_loadingGraphDoesNotTriggerCallback(self, graphSavedOnDisk):
graph: Graph = graphSavedOnDisk
node = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
node.input.value = 5
node.affectedInput.value = 2
graph.save()
loadedGraph = loadGraph(graph.filepath, strictCompatibility=True)
loadedNode = loadedGraph.node(node.name)
assert loadedNode
assert loadedNode.affectedInput.value == 2
def test_loadingGraphDoesNotTriggerCallbackForConnectedAttributes(
self, graphSavedOnDisk
):
graph: Graph = graphSavedOnDisk
nodeA = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
graph.addEdge(nodeA.input, nodeB.input)
nodeA.input.value = 5
assert nodeB.affectedInput.value == nodeB.input.value * 2
nodeB.affectedInput.value = 2
graph.save()
loadedGraph = loadGraph(graph.filepath, strictCompatibility=True)
loadedNodeB = loadedGraph.node(nodeB.name)
assert loadedNodeB
assert loadedNodeB.affectedInput.value == 2
class NodeWithCompoundAttributes(desc.Node):
"""
A Node containing a variation of compound attributes (List/Groups),
called whenever the value of this attribute is changed explicitly.
"""
inputs = [
desc.ListAttribute(
name="listInput",
label="List Input",
description="ListAttribute of IntParams.",
elementDesc=desc.IntParam(
name="int", label="Int", description="", value=0, range=None
),
),
desc.GroupAttribute(
name="groupInput",
label="Group Input",
description="GroupAttribute with a single 'IntParam' element.",
groupDesc=[
desc.IntParam(
name="int", label="Int", description="", value=0, range=None
)
],
),
desc.ListAttribute(
name="listOfGroupsInput",
label="List of Groups input",
description="ListAttribute of GroupAttribute with a single 'IntParam' element.",
elementDesc=desc.GroupAttribute(
name="subGroup",
label="SubGroup",
description="",
groupDesc=[
desc.IntParam(
name="int", label="Int", description="", value=0, range=None
)
],
)
),
desc.GroupAttribute(
name="groupWithListInput",
label="Group with List",
description="GroupAttribute with a single 'ListAttribute of IntParam' element.",
groupDesc=[
desc.ListAttribute(
name="subList",
label="SubList",
description="",
elementDesc=desc.IntParam(
name="int", label="Int", description="", value=0, range=None
)
)
]
)
]
class TestAttributeCallbackBehaviorWithUpstreamCompoundAttributes:
@classmethod
def setup_class(cls):
registerNodeType(NodeWithAttributeChangedCallback)
registerNodeType(NodeWithCompoundAttributes)
@classmethod
def teardown_class(cls):
unregisterNodeType(NodeWithAttributeChangedCallback)
unregisterNodeType(NodeWithCompoundAttributes)
def test_connectionToListElement(self):
graph = Graph("")
nodeA = graph.addNewNode(NodeWithCompoundAttributes.__name__)
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
nodeA.listInput.append(0)
attr = nodeA.listInput.at(0)
graph.addEdge(attr, nodeB.input)
attr.value = 10
assert nodeB.input.value == 10
assert nodeB.affectedInput.value == 20
def test_connectionToGroupElement(self):
graph = Graph("")
nodeA = graph.addNewNode(NodeWithCompoundAttributes.__name__)
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
graph.addEdge(nodeA.groupInput.int, nodeB.input)
nodeA.groupInput.int.value = 10
assert nodeB.input.value == 10
assert nodeB.affectedInput.value == 20
def test_connectionToGroupElementInList(self):
graph = Graph("")
nodeA = graph.addNewNode(NodeWithCompoundAttributes.__name__)
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
nodeA.listOfGroupsInput.append({})
attr = nodeA.listOfGroupsInput.at(0)
graph.addEdge(attr.int, nodeB.input)
attr.int.value = 10
assert nodeB.input.value == 10
assert nodeB.affectedInput.value == 20
def test_connectionToListElementInGroup(self):
graph = Graph("")
nodeA = graph.addNewNode(NodeWithCompoundAttributes.__name__)
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
nodeA.groupWithListInput.subList.append(0)
attr = nodeA.groupWithListInput.subList.at(0)
graph.addEdge(attr, nodeB.input)
attr.value = 10
assert nodeB.input.value == 10
assert nodeB.affectedInput.value == 20
class NodeWithDynamicOutputValue(desc.Node):
"""
A Node containing an output attribute which value is computed dynamically during graph execution.
"""
inputs = [
desc.IntParam(
name="input",
label="Input",
description="Input used in the computation of 'output'",
value=0,
),
]
outputs = [
desc.IntParam(
name="output",
label="Output",
description="Dynamically computed output (input * 2)",
# Setting value to None makes the attribute dynamic.
value=None,
),
]
def processChunk(self, chunk):
chunk.node.output.value = chunk.node.input.value * 2
class TestAttributeCallbackBehaviorWithUpstreamDynamicOutputs:
@classmethod
def setup_class(cls):
registerNodeType(NodeWithAttributeChangedCallback)
registerNodeType(NodeWithDynamicOutputValue)
@classmethod
def teardown_class(cls):
unregisterNodeType(NodeWithAttributeChangedCallback)
unregisterNodeType(NodeWithDynamicOutputValue)
def test_connectingUncomputedDynamicOutputDoesNotTriggerDownstreamAttributeChangedCallback(
self,
):
graph = Graph("")
nodeA = graph.addNewNode(NodeWithDynamicOutputValue.__name__)
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
nodeA.input.value = 10
graph.addEdge(nodeA.output, nodeB.input)
assert nodeB.affectedInput.value == 0
def test_connectingComputedDynamicOutputTriggersDownstreamAttributeChangedCallback(
self, graphWithIsolatedCache
):
graph: Graph = graphWithIsolatedCache
nodeA = graph.addNewNode(NodeWithDynamicOutputValue.__name__)
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
nodeA.input.value = 10
executeGraph(graph)
graph.addEdge(nodeA.output, nodeB.input)
assert nodeA.output.value == nodeB.input.value == 20
assert nodeB.affectedInput.value == 40
def test_dynamicOutputValueComputeDoesNotTriggerDownstreamAttributeChangedCallback(
self, graphWithIsolatedCache
):
graph: Graph = graphWithIsolatedCache
nodeA = graph.addNewNode(NodeWithDynamicOutputValue.__name__)
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
graph.addEdge(nodeA.output, nodeB.input)
nodeA.input.value = 10
executeGraph(graph)
assert nodeB.input.value == 20
assert nodeB.affectedInput.value == 0
def test_clearingDynamicOutputValueDoesNotTriggerDownstreamAttributeChangedCallback(
self, graphWithIsolatedCache
):
graph: Graph = graphWithIsolatedCache
nodeA = graph.addNewNode(NodeWithDynamicOutputValue.__name__)
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
nodeA.input.value = 10
executeGraph(graph)
graph.addEdge(nodeA.output, nodeB.input)
expectedPreClearValue = nodeA.input.value * 2 * 2
assert nodeB.affectedInput.value == expectedPreClearValue
nodeA.clearData()
assert nodeA.output.value == nodeB.input.value is None
assert nodeB.affectedInput.value == expectedPreClearValue
def test_loadingGraphWithComputedDynamicOutputValueDoesNotTriggerDownstreamAttributeChangedCallback(
self, graphSavedOnDisk
):
graph: Graph = graphSavedOnDisk
nodeA = graph.addNewNode(NodeWithDynamicOutputValue.__name__)
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
nodeA.input.value = 10
graph.addEdge(nodeA.output, nodeB.input)
executeGraph(graph)
assert nodeA.output.value == nodeB.input.value == 20
assert nodeB.affectedInput.value == 0
graph.save()
loadGraph(graph.filepath, strictCompatibility=True)
assert nodeB.affectedInput.value == 0
class TestAttributeCallbackBehaviorOnGraphImport:
@classmethod
def setup_class(cls):
registerNodeType(NodeWithAttributeChangedCallback)
@classmethod
def teardown_class(cls):
unregisterNodeType(NodeWithAttributeChangedCallback)
def test_importingGraphDoesNotTriggerAttributeChangedCallbacks(self):
graph = Graph("")
nodeA = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
nodeB = graph.addNewNode(NodeWithAttributeChangedCallback.__name__)
graph.addEdge(nodeA.affectedInput, nodeB.input)
nodeA.input.value = 5
nodeB.affectedInput.value = 2
otherGraph = Graph("")
otherGraph.importGraphContent(graph)
assert otherGraph.node(nodeB.name).affectedInput.value == 2