mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-05-02 11:46:45 +02:00
New test suite that focuses on testing the behavior of the attribute changes callback system when such an attribute is connected to an upstream dynamic output value.
389 lines
13 KiB
Python
389 lines
13 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)
|
|
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
|
|
nodeB.affectedInput.value = 2
|
|
|
|
graph.save()
|
|
|
|
loadedGraph = loadGraph(graph.filepath)
|
|
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
|