Skip to content

Commit e159f29

Browse files
fix: handle None from fullmatch reference regex
- this code branch is for `is_pyxform_reference` but it didn't handle the case where there is no match - in which case the iteration over `outer_matches` would fail and throw an error like "None has no group" - added test coverage for relevant usages of is_pyxform_reference: - repeat_count, now with tests showing common usage pattern outcomes - seed param, which had alternate usages already (constant/single ref)
1 parent 093a058 commit e159f29

4 files changed

Lines changed: 269 additions & 41 deletions

File tree

pyxform/validators/pyxform/pyxform_reference.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@ def _parse(
5959
return None
6060

6161
if match_full:
62-
outer_matches = (RE_PYXFORM_REF_OUTER.fullmatch(value),)
62+
outer_matches = RE_PYXFORM_REF_OUTER.fullmatch(value)
63+
if not outer_matches:
64+
# Expression may contain a reference but has other characters e.g. func call.
65+
return None
66+
outer_matches = (outer_matches,)
6367
else:
6468
outer_matches = RE_PYXFORM_REF_OUTER.finditer(value)
6569

tests/test_randomize_itemsets.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,26 @@ def test_randomized_seeded_select_one_nameset_seed(self):
6363
],
6464
)
6565

66+
def test_randomized_seeded_select_one_nameset_seed_expression__error(self):
67+
md = """
68+
| survey |
69+
| | type | name | label | parameters | calculation |
70+
| | calculate | seed | | | once(decimal-date-time(now())) |
71+
| | select_one choices | select | Select | randomize=true,seed=int(${seed}) | |
72+
73+
| choices |
74+
| | list_name | name | label |
75+
| | choices | a | opt_a |
76+
| | choices | b | opt_b |
77+
"""
78+
self.assertPyxformXform(
79+
md=md,
80+
errored=True,
81+
error__contains=[
82+
"seed value must be a number or a reference to another field."
83+
],
84+
)
85+
6686
def test_randomized_seeded_filtered_select_one(self):
6787
self.assertPyxformXform(
6888
name="data",

tests/test_repeat.py

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
Test reapeat structure.
2+
Test repeat structure.
33
"""
44

55
from tests.pyxform_test_case import PyxformTestCase
@@ -943,42 +943,3 @@ def test_repeat_with_reference_path_after_instance_not_in_predicate_not_using_cu
943943
"""<bind calculate="concat(instance('item')/root/item[index = current()/../pos5 ]/label, ../pos5 + 1)" nodeset="/test_name/rep5/item5" type="string"/>""", # pylint: disable=line-too-long
944944
],
945945
)
946-
947-
def test_repeat_count_item_with_same_suffix_as_repeat_is_ok(self):
948-
"""Should not have a name clash, the referenced item should be used directly."""
949-
md = """
950-
| survey | | | | |
951-
| | type | name | label | repeat_count |
952-
| | integer | a_count | 1 | |
953-
| | begin repeat | a | 2 | ${a_count} |
954-
| | text | b | 3 | |
955-
| | end repeat | a | | |
956-
"""
957-
self.assertPyxformXform(
958-
md=md,
959-
xml__xpath_match=[
960-
# repeat references existing count element directly.
961-
"""
962-
/h:html/h:body/x:group[@ref='/test_name/a']/x:repeat[
963-
@jr:count=' /test_name/a_count '
964-
and @nodeset='/test_name/a'
965-
and ./x:input[@ref='/test_name/a/b']
966-
]
967-
""",
968-
# binding for existing count element but no calculate binding.
969-
"""
970-
/h:html/h:head/x:model[
971-
./x:bind[@nodeset='/test_name/a_count' and @type='int']
972-
and not(./x:bind[
973-
@calculate=' /test_name/a_count'
974-
and @nodeset='/test_name/a_count'
975-
and @readonly='true()'
976-
])
977-
]
978-
""",
979-
# no duplicated element in the instance.
980-
"""
981-
/h:html/h:head/x:model/x:instance/x:test_name/x:a_count
982-
""",
983-
],
984-
)

tests/test_repeat_count.py

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
from tests.pyxform_test_case import PyxformTestCase
2+
from tests.xpath_helpers.questions import xpq
3+
4+
5+
class TestRepeatCount(PyxformTestCase):
6+
"""
7+
Test usages of the survey repeat_count column.
8+
"""
9+
10+
def test_single_reference__generated_element_same_name__ok(self):
11+
"""Should not have a name clash, the referenced item should be used directly."""
12+
md = """
13+
| survey |
14+
| | type | name | label | repeat_count |
15+
| | integer | q1_count | Q1 | |
16+
| | begin repeat | r1 | R1 | ${q1_count} |
17+
| | text | q2 | Q2 | |
18+
| | end repeat | | | |
19+
"""
20+
self.assertPyxformXform(
21+
md=md,
22+
xml__xpath_match=[
23+
# repeat_count target output as normal
24+
xpq.model_instance_item("q1_count"),
25+
xpq.model_instance_bind("q1_count", "int"),
26+
xpq.body_control("q1_count", "input"),
27+
# no instance element for generated *_count item
28+
"""
29+
/h:html/h:head/x:model/x:instance/x:test_name[not(./x:r1_count)]
30+
""",
31+
# no binding for generated *_count item
32+
"""
33+
/h:html/h:head/x:model[not(./x:bind[@nodeset='/test_name/r1_count'])]
34+
""",
35+
# repeat references existing count element directly.
36+
"""
37+
/h:html/h:body/x:group[@ref='/test_name/r1']/x:repeat[
38+
@nodeset='/test_name/r1'
39+
and @jr:count=' /test_name/q1_count '
40+
]
41+
""",
42+
],
43+
)
44+
45+
def test_single_reference__generated_element_different_name__ok(self):
46+
"""Should find that a {repeat_name}_count element is generated for the calculation."""
47+
md = """
48+
| survey |
49+
| | type | name | label | repeat_count |
50+
| | integer | q1 | Q1 | |
51+
| | begin repeat | r1 | R1 | ${q1} |
52+
| | text | q2 | Q2 | |
53+
| | end repeat | | | |
54+
"""
55+
self.assertPyxformXform(
56+
md=md,
57+
xml__xpath_match=[
58+
# repeat_count target output as normal
59+
xpq.model_instance_item("q1"),
60+
xpq.model_instance_bind("q1", "int"),
61+
xpq.body_control("q1", "input"),
62+
# no instance element for generated *_count item
63+
"""
64+
/h:html/h:head/x:model/x:instance/x:test_name[not(./x:r1_count)]
65+
""",
66+
# no binding for generated *_count item
67+
"""
68+
/h:html/h:head/x:model[not(./x:bind[@nodeset='/test_name/r1_count'])]
69+
""",
70+
# repeat references existing count element directly.
71+
"""
72+
/h:html/h:body/x:group[@ref='/test_name/r1']/x:repeat[
73+
@nodeset='/test_name/r1'
74+
and @jr:count=' /test_name/q1 '
75+
]
76+
""",
77+
],
78+
)
79+
80+
def test_expression__generated_element_same_name__error(self):
81+
"""Should find that a duplicate {repeat_name}_count element raises an error."""
82+
md = """
83+
| survey |
84+
| | type | name | label | repeat_count |
85+
| | select_multiple l1 | r1_count | Q1 | |
86+
| | begin_repeat | r1 | R1 | count-selected(${r1_count}) |
87+
| | text | q2 | Q2 | |
88+
| | end_repeat | | | |
89+
90+
| choices |
91+
| | list_name | name | label |
92+
| | l1 | c1 | C1 |
93+
| | l1 | c2 | C2 |
94+
"""
95+
self.assertPyxformXform(
96+
md=md,
97+
errored=True,
98+
error__contains=[
99+
"There are more than one survey elements named 'r1_count' "
100+
"(case-insensitive) in the section named 'test_name'."
101+
],
102+
)
103+
104+
def test_expression__generated_element_different_name__ok(self):
105+
"""Should find that a {repeat_name}_count element is generated for the calculation."""
106+
# repro for pyxform 781
107+
md = """
108+
| survey |
109+
| | type | name | label | repeat_count |
110+
| | select_multiple l1 | q1 | Q1 | |
111+
| | begin_repeat | r1 | R1 | count-selected(${q1}) |
112+
| | text | q2 | Q2 | |
113+
| | end_repeat | | | |
114+
115+
| choices |
116+
| | list_name | name | label |
117+
| | l1 | c1 | C1 |
118+
| | l1 | c2 | C2 |
119+
"""
120+
self.assertPyxformXform(
121+
md=md,
122+
xml__xpath_match=[
123+
xpq.model_instance_item("r1_count"),
124+
xpq.model_instance_bind("r1_count", "string"),
125+
xpq.model_instance_bind_attr(
126+
"r1_count", "calculate", "count-selected( /test_name/q1 )"
127+
),
128+
xpq.model_instance_bind_attr("r1_count", "readonly", "true()"),
129+
# repeat references generated repeat_count element.
130+
"""
131+
/h:html/h:body/x:group[@ref='/test_name/r1']/x:repeat[
132+
@nodeset='/test_name/r1'
133+
and @jr:count=' /test_name/r1_count '
134+
]
135+
""",
136+
],
137+
)
138+
139+
def test_manual_xpath__generated_element_same_name__error(self):
140+
"""Should find that a duplicate {repeat_name}_count element raises an error."""
141+
md = """
142+
| survey |
143+
| | type | name | label | repeat_count |
144+
| | select_multiple l1 | r1_count | Q1 | |
145+
| | begin_repeat | r1 | R1 | count-selected( /test_name/r1_count ) |
146+
| | text | q2 | Q2 | |
147+
| | end_repeat | | | |
148+
149+
| choices |
150+
| | list_name | name | label |
151+
| | l1 | c1 | C1 |
152+
| | l1 | c2 | C2 |
153+
"""
154+
self.assertPyxformXform(
155+
md=md,
156+
errored=True,
157+
error__contains=[
158+
"There are more than one survey elements named 'r1_count' "
159+
"(case-insensitive) in the section named 'test_name'."
160+
],
161+
)
162+
163+
def test_manual_xpath__generated_element_different_name__ok(self):
164+
"""Should find that a {repeat_name}_count element is generated for the calculation."""
165+
md = """
166+
| survey |
167+
| | type | name | label | repeat_count |
168+
| | select_multiple l1 | q1 | Q1 | |
169+
| | begin_repeat | r1 | R1 | count-selected( /test_name/q1 ) |
170+
| | text | q2 | Q2 | |
171+
| | end_repeat | | | |
172+
173+
| choices |
174+
| | list_name | name | label |
175+
| | l1 | c1 | C1 |
176+
| | l1 | c2 | C2 |
177+
"""
178+
self.assertPyxformXform(
179+
md=md,
180+
xml__xpath_match=[
181+
xpq.model_instance_item("r1_count"),
182+
xpq.model_instance_bind("r1_count", "string"),
183+
xpq.model_instance_bind_attr(
184+
"r1_count", "calculate", "count-selected( /test_name/q1 )"
185+
),
186+
xpq.model_instance_bind_attr("r1_count", "readonly", "true()"),
187+
# repeat references generated repeat_count element.
188+
"""
189+
/h:html/h:body/x:group[@ref='/test_name/r1']/x:repeat[
190+
@nodeset='/test_name/r1'
191+
and @jr:count=' /test_name/r1_count '
192+
]
193+
""",
194+
],
195+
)
196+
197+
def test_constant_integer__generated_element_same_name__error(self):
198+
"""Should find that a duplicate {repeat_name}_count element raises an error."""
199+
# Seems strange, but according to pyxform 435 it's a javarosa limitation.
200+
md = """
201+
| survey |
202+
| | type | name | label | repeat_count |
203+
| | integer | r1_count | Q1 | |
204+
| | begin_repeat | r1 | R1 | 2 |
205+
| | text | q2 | Q2 | |
206+
| | end_repeat | | | |
207+
"""
208+
self.assertPyxformXform(
209+
md=md,
210+
errored=True,
211+
error__contains=[
212+
"There are more than one survey elements named 'r1_count' "
213+
"(case-insensitive) in the section named 'test_name'."
214+
],
215+
)
216+
217+
def test_constant_integer__generated_element_different_name__ok(self):
218+
"""Should find that a {repeat_name}_count element is generated for the calculation."""
219+
# Seems strange, but according to pyxform 435 it's a javarosa limitation.
220+
md = """
221+
| survey |
222+
| | type | name | label | repeat_count |
223+
| | integer | q1 | Q1 | |
224+
| | begin_repeat | r1 | R1 | 2 |
225+
| | text | q2 | Q2 | |
226+
| | end_repeat | | | |
227+
"""
228+
self.assertPyxformXform(
229+
md=md,
230+
xml__xpath_match=[
231+
xpq.model_instance_item("r1_count"),
232+
xpq.model_instance_bind("r1_count", "string"),
233+
xpq.model_instance_bind_attr("r1_count", "calculate", "2"),
234+
xpq.model_instance_bind_attr("r1_count", "readonly", "true()"),
235+
# repeat references generated repeat_count element.
236+
"""
237+
/h:html/h:body/x:group[@ref='/test_name/r1']/x:repeat[
238+
@nodeset='/test_name/r1'
239+
and @jr:count=' /test_name/r1_count '
240+
]
241+
""",
242+
],
243+
)

0 commit comments

Comments
 (0)