Skip to content

Commit aca1a11

Browse files
[ui] Attribute/Node: Add tests for validators and refacto some methods
1 parent 73b180e commit aca1a11

File tree

4 files changed

+170
-10
lines changed

4 files changed

+170
-10
lines changed

meshroom/core/attribute.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -446,13 +446,13 @@ def getPrimitiveValue(self, exportDefault=True):
446446
def updateInternals(self):
447447
# Emit if the enable status has changed
448448
self.setEnabled(self.getEnabled())
449-
449+
450450
def getErrorMessages(self) -> list[str]:
451451
""" Execute the validators and aggregate the eventual error messages"""
452452

453453
result = []
454454

455-
for validator in self.desc._validators:
455+
for validator in self.desc.validators:
456456
isValid, errorMessages = validator(self.node, self)
457457

458458
if isValid:
@@ -463,6 +463,18 @@ def getErrorMessages(self) -> list[str]:
463463

464464
return result
465465

466+
def _isValid(self) -> bool:
467+
""" Check the validation and return False if any validator return (False, erorrs)
468+
"""
469+
470+
for validator in self.desc.validators:
471+
isValid, _ = validator(self.node, self)
472+
473+
if not isValid:
474+
return False
475+
476+
return True
477+
466478
def _isMandatory(self) -> bool:
467479
""" An attribute is considered as mandatory it contain a NotEmptyValidator """
468480

@@ -519,7 +531,7 @@ def _isMandatory(self) -> bool:
519531
errorMessageChanged = Signal()
520532
errorMessages = Property(Variant, lambda self: self.getErrorMessages(), notify=errorMessageChanged)
521533
isMandatory = Property(bool, _isMandatory, constant=True )
522-
534+
isValid = Property(bool, _isValid, constant=True)
523535

524536
def raiseIfLink(func):
525537
""" If Attribute instance is a link, raise a RuntimeError."""

meshroom/core/desc/validators.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77

88
SuccessResponse = (True, [])
9-
Number = TypeVar("Number", int, float)
109

1110

1211
class AttributeValidator(object):
@@ -26,15 +25,11 @@ def __call__(self, node: "Node", attribute: "Attribute") -> tuple[bool, list[str
2625

2726
class RangeValidator(AttributeValidator):
2827

29-
def __init__(self, min:Number, max:Number):
28+
def __init__(self, min, max):
3029
self._min = min
3130
self._max = max
3231

3332
def __call__(self, node:"Node", attribute: "Attribute") -> tuple[bool, list[str]]:
34-
35-
if not isinstance(attribute, Number):
36-
return (False, ["Attribute value should be a number"])
37-
3833

3934
if attribute.value < self._min or attribute.value > self._max:
4035
return (False, [f"Value should be greater than {self._min} and less than {self._max}",

tests/nodes/test/validableNode.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from meshroom.core import desc
2+
from meshroom.core.desc.validators import NotEmptyValidator, RangeValidator
3+
4+
5+
class ValidableNode(desc.CommandLineNode):
6+
7+
inputs = [
8+
desc.StringParam(
9+
name='mandatory',
10+
label='Mandatory input',
11+
description='''''',
12+
value='',
13+
validators= [
14+
NotEmptyValidator()
15+
]
16+
),
17+
desc.FloatParam(
18+
name='floatRange',
19+
label='range input',
20+
description='''''',
21+
value=0.0,
22+
validators=[
23+
RangeValidator(min=0.0, max=1.0)
24+
]
25+
),
26+
desc.IntParam(
27+
name='intRange',
28+
label='range input',
29+
description='''''',
30+
value=0.0,
31+
),
32+
33+
]
34+
35+
outputs = [
36+
desc.File(
37+
name='output',
38+
label='Output',
39+
description='''''',
40+
value='{nodeCacheFolder}/appendText.txt',
41+
)
42+
]
43+

tests/test_attributes.py

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
from meshroom.core.graph import Graph
2+
from tests.nodes.test.validableNode import ValidableNode
3+
import pytest
4+
5+
import logging
6+
logger = logging.getLogger('test')
7+
8+
valid3DExtensionFiles = [(f'test.{ext}', True) for ext in ('obj', 'stl', 'fbx', 'gltf', 'abc', 'ply')]
9+
invalid3DExtensionFiles = [(f'test.{ext}', False) for ext in ('', 'exe', 'jpg', 'png', 'py')]
10+
11+
valid2DSemantics= [(semantic, True) for semantic in ('image', 'imageList', 'sequence')]
12+
invalid2DSemantics = [(semantic, False) for semantic in ('3d', '', 'multiline', 'color/hue')]
213

314

415
def test_attribute_retrieve_linked_input_and_output_attributes():
@@ -36,4 +47,103 @@ def test_attribute_retrieve_linked_input_and_output_attributes():
3647
assert not n0.output.hasOutputConnections
3748
assert len(n0.input.getLinkedInAttributes()) == 0
3849
assert len(n0.output.getLinkedOutAttributes()) == 0
39-
50+
51+
@pytest.mark.parametrize("givenFile,expected", valid3DExtensionFiles + invalid3DExtensionFiles)
52+
def test_attribute_is3D_file_extensions(givenFile, expected):
53+
"""
54+
Check what makes an attribute a valid 3d media
55+
"""
56+
57+
g = Graph('')
58+
n0 = g.addNewNode('Ls', input='')
59+
60+
# Given
61+
assert not n0.input.is3D
62+
63+
# When
64+
n0.input.value = givenFile
65+
66+
# Then
67+
assert n0.input.is3D == expected
68+
69+
70+
def test_attribute_i3D_by_description_semantic():
71+
""" """
72+
73+
# Given
74+
g = Graph('')
75+
n0 = g.addNewNode('Ls', input='')
76+
77+
assert not n0.output.is3D
78+
79+
# When
80+
n0.output.desc._semantic = "3d"
81+
82+
# Then
83+
assert n0.output.is3D
84+
85+
@pytest.mark.parametrize("givenSemantic,expected", valid2DSemantics + invalid2DSemantics)
86+
def test_attribute_is2D_file_semantic(givenSemantic, expected):
87+
"""
88+
Check what makes an attribute a valid 2d media
89+
"""
90+
91+
g = Graph('')
92+
n0 = g.addNewNode('Ls', input='')
93+
94+
# Given
95+
n0.input.desc._semantic = ""
96+
assert not n0.input.is2D
97+
98+
# When
99+
n0.input.desc._semantic = givenSemantic
100+
101+
# Then
102+
assert n0.input.is2D == expected
103+
104+
def test_attribute_notEmpty_validation():
105+
106+
# Given
107+
g = Graph('')
108+
node = g.addNewNode('ValidableNode')
109+
110+
# When
111+
node.mandatory.value = ''
112+
113+
# Then
114+
assert not node.mandatory.isValid
115+
assert len(node.mandatory.getErrorMessages()) == 1
116+
assert node.mandatory.isMandatory is True
117+
assert node.hasInvalidAttribute
118+
119+
# When
120+
node.mandatory.value = 'test'
121+
122+
# Then
123+
assert node.mandatory.isValid
124+
assert len(node.mandatory.getErrorMessages()) == 0
125+
assert not node.hasInvalidAttribute
126+
127+
def test_attribute_range_validation():
128+
129+
# Given
130+
g = Graph('')
131+
node = g.addNewNode('ValidableNode')
132+
node.mandatory.value = 'test'
133+
134+
# When
135+
node.floatRange.value = 2.0
136+
137+
# Then
138+
assert not node.floatRange.isValid
139+
assert len(node.floatRange.getErrorMessages()) == 2
140+
assert node.mandatory.isMandatory is True
141+
assert node.hasInvalidAttribute
142+
143+
# When
144+
node.floatRange.value = 0.25
145+
146+
# Then
147+
assert node.floatRange.isValid
148+
assert len(node.mandatory.getErrorMessages()) == 0
149+
assert not node.hasInvalidAttribute

0 commit comments

Comments
 (0)