Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
588c3b9
chg: remove unused question_type xls + code
lindsay-stevens Sep 1, 2025
848117f
chg: update and expand tests on loops
lindsay-stevens Sep 3, 2025
e296c9c
add: test coverage for unmatched group begin/end
lindsay-stevens Sep 4, 2025
9002113
chg: improve error messages for unmatched groups
lindsay-stevens Sep 4, 2025
a4e05b1
add: test coverage for name uniqueness rules
lindsay-stevens Sep 9, 2025
f8fd743
chg: make name uniqueness rules more consistent
lindsay-stevens Sep 9, 2025
7a3d3cd
chg: move entities tests into folder
lindsay-stevens Sep 10, 2025
a187792
add: allow entities to be declared for a repeat
lindsay-stevens Sep 10, 2025
ba42054
add: name to unmatched control error messages
lindsay-stevens Sep 15, 2025
cdce738
chg: improve error messages
lindsay-stevens Sep 15, 2025
c736045
dev: merge branch 'master' into 'pyxform-775'
lindsay-stevens Sep 19, 2025
2caa1e2
add: set entity id when creating new repeat
lindsay-stevens Sep 19, 2025
0af166a
chg: set new entities version when repeat used
lindsay-stevens Sep 19, 2025
df6eee6
chg: improve entities xpath helper, refactor tests
lindsay-stevens Sep 19, 2025
1e481b9
chg: standardise/fix/split action node output
lindsay-stevens Oct 1, 2025
026bfe0
fix: repeat entity label binding path too low
lindsay-stevens Oct 2, 2025
0014ca9
fix: repeat entity attribute binding paths too low
lindsay-stevens Oct 7, 2025
983b990
add: error if entity save_to in undeclared repeat
lindsay-stevens Oct 7, 2025
3c8287e
fix: only emit repeat setvalue with create mode
lindsay-stevens Oct 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed tests/example_xls/another_loop.xls
Binary file not shown.
38 changes: 0 additions & 38 deletions tests/test_for_loop.py

This file was deleted.

202 changes: 194 additions & 8 deletions tests/test_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,186 @@

from pyxform.xls2xform import convert

from tests import utils
from tests.pyxform_test_case import PyxformTestCase
from tests.xpath_helpers.questions import xpq


class TestLoop(TestCase):
class TestLoop(PyxformTestCase):
"""
A 'loop' a type of group that, for each choice in the referenced choice list,
Comment thread
lindsay-stevens marked this conversation as resolved.
Outdated
generates grouped set of questions using the questions inside the loop definition.
The pattern "%(name)s" or "%(label)s" can be used to insert the choice name or label
into the question columns, e.g. to adjust the label to each choice.
"""

def test_loop(self):
"""Should find that each item in the loop is repeated for each loop choice."""
md = """
| survey |
| | type | name | label |
| | begin loop over c1 | l1 | |
| | integer | q1 | Age |
| | select_one c2 | q2 | Size of %(label)s |
| | end loop | | |

| choices |
| | list_name | name | label |
| | c1 | thing1 | Thing 1 |
| | c1 | thing2 | Thing 2 |
| | c2 | type1 | Big |
| | c2 | type2 | Small |
"""
self.assertPyxformXform(
md=md,
xml__xpath_match=[
# Instance
xpq.model_instance_item("l1/x:thing1/x:q1"),
xpq.model_instance_item("l1/x:thing1/x:q2"),
xpq.model_instance_item("l1/x:thing2/x:q1"),
xpq.model_instance_item("l1/x:thing2/x:q2"),
# Bind
xpq.model_instance_bind("l1/thing1/q1", "int"),
xpq.model_instance_bind("l1/thing1/q2", "string"),
xpq.model_instance_bind("l1/thing1/q1", "int"),
xpq.model_instance_bind("l1/thing1/q1", "int"),
# Control
"""
/h:html/h:body/x:group[@ref = '/test_name/l1']/x:group[
@ref = '/test_name/l1/thing1'
and ./x:label = 'Thing 1'
and ./x:input[
@ref = '/test_name/l1/thing1/q1'
and ./x:label = 'Age'
]
and ./x:select1[
@ref = '/test_name/l1/thing1/q2'
and ./x:label = 'Size of Thing 1'
and ./x:itemset[
@nodeset = "instance('c2')/root/item"
and ./x:value[@ref = 'name']
and ./x:label[@ref = 'label']
]
]
]
""",
"""
/h:html/h:body/x:group[@ref = '/test_name/l1']/x:group[
@ref = '/test_name/l1/thing2'
and ./x:label = 'Thing 2'
and ./x:input[
@ref = '/test_name/l1/thing2/q1'
and ./x:label = 'Age'
]
and ./x:select1[
@ref = '/test_name/l1/thing2/q2'
and ./x:label = 'Size of Thing 2'
and ./x:itemset[
@nodeset = "instance('c2')/root/item"
and ./x:value[@ref = 'name']
and ./x:label[@ref = 'label']
]
]
]
""",
],
)

def test_loop__groups_error(self):
"""Should find that using a group in a loop results in an error."""
md = """
| survey |
| | type | name | label |
| | begin loop over c1 | l1 | |
| | begin group | g1 | |
| | integer | q1 | Count %(label)s |
| | select_one c2 | q2 | Type of %(label)s |
| | end group | | |
| | end loop | | |

| choices |
| | list_name | name | label |
| | c1 | thing1 | Thing 1 |
| | c1 | thing2 | Thing 2 |
| | c2 | type1 | Big |
| | c2 | type2 | Small |
"""
self.assertPyxformXform(
md=md,
errored=True,
error__contains=["There are two sections with the name g1."],
)

def test_loop__repeats_error(self):
"""Should find that using a repeat in a loop results in an error."""
md = """
| survey |
| | type | name | label |
| | begin loop over c1 | l1 | |
| | begin repeat | r1 | |
| | integer | q1 | Count %(label)s |
| | select_one c2 | q2 | Type of %(label)s |
| | end repeat | | |
| | end loop | | |

| choices |
| | list_name | name | label |
| | c1 | thing1 | Thing 1 |
| | c1 | thing2 | Thing 2 |
| | c2 | type1 | Big |
| | c2 | type2 | Small |
"""
self.assertPyxformXform(
md=md,
errored=True,
error__contains=["There are two sections with the name r1."],
)

def test_loop__references_error(self):
"""Should find that using a reference variable in a loop results in an error."""
md = """
| survey |
| | type | name | label |
| | begin loop over c1 | l1 | |
| | integer | q1 | Count %(label)s |
| | note | q2 | Counted ${q1} |
| | end loop | | |

| choices |
| | list_name | name | label |
| | c1 | thing1 | Thing 1 |
| | c1 | thing2 | Thing 2 |
"""
self.assertPyxformXform(
md=md,
errored=True,
error__contains=[
"There has been a problem trying to replace ${q1} with the XPath to the "
"survey element named 'q1'. There are multiple survey elements with this name."
],
)


class TestLoopInternalRepresentation(TestCase):
maxDiff = None

def test_table(self):
path = utils.path_to_text_fixture("simple_loop.xls")
observed = convert(xlsform=path)._pyxform
def test_pyxform(self):
md = """
| survey |
| | type | name | label::English |
| | begin loop over my_columns | my_table | My Table |
| | integer | count | How many are there in this group? |
| | end loop | | |

| choices |
| | list name | name | label:English |
| | my_columns | col1 | Column 1 |
| | my_columns | col2 | Column 2 |

| settings |
| | id_string |
| | simple_loop |
"""
observed = convert(xlsform=md)._pyxform
expected = {
"name": "data",
"title": "simple_loop",
Expand Down Expand Up @@ -62,9 +232,25 @@ def test_table(self):
}
self.assertEqual(expected, observed)

def test_loop(self):
path = utils.path_to_text_fixture("another_loop.xls")
observed = convert(xlsform=path)._survey.to_json_dict()
def test_survey(self):
md = """
| survey |
| | type | name | label::English | label::French | constraint |
| | begin loop over types | loop_vehicle_types | | | |
| | integer | total | How many do you have? | Combien avoir? | |
| | integer | working | How many are working? | Combien marcher? | . <= ../total |
| | end loop | | | | |

| choices |
| | list_name | name | label::English | label::French |
| | types | car | Car | Voiture |
| | types | motor_cycle | Motorcycle | Moto |

| settings |
| | id_string |
| | another_loop |
"""
observed = convert(xlsform=md)._survey.to_json_dict()
observed.pop("_translations", None)
observed.pop("_xpath", None)
expected = {
Expand Down