Skip to content

Commit 0d6c2ea

Browse files
authored
Extend randomize seed tests (#805)
1 parent 9545fa3 commit 0d6c2ea

File tree

3 files changed

+158
-20
lines changed

3 files changed

+158
-20
lines changed

src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -705,12 +705,18 @@ public static Double toDouble(Object o) {
705705
}
706706

707707
/**
708-
* convert a string value to an integer by:
708+
* Convert a non-zero-length string value to an integer by:
709709
* - encoding it as utf-8
710710
* - hashing it with sha256 (available cross-platform, including via browser crypto API)
711711
* - interpreting the first 8 bytes of the hash as a long
712+
*
713+
* A zero-length string results in a 0L — this is the classic behaviour that we want
714+
* to conserve for backward compatibility with a case that is expected to be common.
715+
* In practical terms, we don't want the sort order of choice lists using an empty string as
716+
* a seed to change with the introduction of this seed derivation mechanism.
712717
*/
713718
public static long toLongHash(String sourceString) {
719+
if (sourceString.length() == 0) return 0L;
714720
byte[] hasheeBuf = sourceString.getBytes(Charset.forName("UTF-8"));
715721
SHA256Digest hasher = new SHA256Digest();
716722
hasher.update(hasheeBuf, 0, hasheeBuf.length);

src/test/java/org/javarosa/xpath/expr/RandomizeTypesTest.java

Lines changed: 150 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.junit.Test;
66

77
import java.io.IOException;
8+
import java.util.Arrays;
89

910
import static org.hamcrest.MatcherAssert.assertThat;
1011
import static org.hamcrest.Matchers.is;
@@ -30,21 +31,35 @@ public void stringNumberSeedConvertsWhenUsedInNodesetExpression() throws IOExcep
3031
title("Randomize non-numeric seed"),
3132
model(
3233
mainInstance(t("data id=\"rand-non-numeric\"",
33-
t("choice")
34+
t("choices_numeric_seed"),
35+
t("choices_stringified_numeric_seed")
3436
)),
3537
instance("choices",
3638
item("a", "A"),
37-
item("b", "B")
39+
item("b", "B"),
40+
item("c", "C"),
41+
item("d", "D"),
42+
item("e", "E"),
43+
item("f", "F"),
44+
item("g", "G"),
45+
item("h", "H")
3846
),
39-
bind("/data/choice").type("string")
47+
bind("/data/choices_numeric_seed").type("string"),
48+
bind("/data/choices_stringified_numeric_seed").type("string")
4049
)
4150
),
4251
body(
43-
select1Dynamic("/data/choice", "randomize(instance('choices')/root/item, '1')")
52+
select1Dynamic("/data/choices_numeric_seed", "randomize(instance('choices')/root/item, 1234)"),
53+
select1Dynamic("/data/choices_stringified_numeric_seed", "randomize(instance('choices')/root/item, '1234')")
4454
)
4555
));
46-
47-
assertThat(scenario.choicesOf("/data/choice").get(0).getValue(), is("b"));
56+
String[] shuffled = {"g", "f", "e", "d", "a", "h", "b", "c"};
57+
String[] nodes = {"/data/choices_numeric_seed", "/data/choices_stringified_numeric_seed"};
58+
for (int i = 0; i < shuffled.length; i++) {
59+
for (String node : nodes) {
60+
assertThat(scenario.choicesOf(node).get(i).getValue(), is(shuffled[i]));
61+
}
62+
}
4863
}
4964

5065
@Test
@@ -54,21 +69,30 @@ public void stringNumberSeedConvertsWhenUsedInCalculate() throws IOException, XF
5469
title("Randomize non-numeric seed"),
5570
model(
5671
mainInstance(t("data id=\"rand-non-numeric\"",
57-
t("choice")
72+
t("choices_numeric_seed"),
73+
t("choices_stringified_numeric_seed")
5874
)),
5975
instance("choices",
6076
item("a", "A"),
61-
item("b", "B")
77+
item("b", "B"),
78+
item("c", "C"),
79+
item("d", "D"),
80+
item("e", "E"),
81+
item("f", "F"),
82+
item("g", "G"),
83+
item("h", "H")
6284
),
63-
bind("/data/choice").type("string").calculate("selected-at(join(' ', randomize(instance('choices')/root/item/label, '1')), 0)")
85+
bind("/data/choices_numeric_seed").type("string").calculate("join('', randomize(instance('choices')/root/item/label, 1234))"),
86+
bind("/data/choices_stringified_numeric_seed").type("string").calculate("join('', randomize(instance('choices')/root/item/label, '1234'))")
6487
)
6588
),
6689
body(
67-
input("/data/choice")
90+
input("/data/choices_numeric_seed"),
91+
input("/data/choices_stringified_numeric_seed")
6892
)
6993
));
70-
71-
assertThat(scenario.answerOf("/data/choice").getDisplayText(), is("B"));
94+
assertThat(scenario.answerOf("/data/choices_numeric_seed").getDisplayText(), is(scenario.answerOf("/data/choices_stringified_numeric_seed").getDisplayText()));
95+
assertThat(scenario.answerOf("/data/choices_numeric_seed").getDisplayText(), is("GFEDAHBC"));
7296
}
7397

7498
@Test
@@ -82,7 +106,13 @@ public void stringTextSeedConvertsWhenUsedInNodesetExpression() throws IOExcepti
82106
)),
83107
instance("choices",
84108
item("a", "A"),
85-
item("b", "B")
109+
item("b", "B"),
110+
item("c", "C"),
111+
item("d", "D"),
112+
item("e", "E"),
113+
item("f", "F"),
114+
item("g", "G"),
115+
item("h", "H")
86116
),
87117
bind("/data/choice").type("string")
88118
)
@@ -91,8 +121,10 @@ public void stringTextSeedConvertsWhenUsedInNodesetExpression() throws IOExcepti
91121
select1Dynamic("/data/choice", "randomize(instance('choices')/root/item, 'foo')")
92122
)
93123
));
94-
95-
assertThat(scenario.choicesOf("/data/choice").get(0).getValue(), is("b"));
124+
String[] shuffled = {"e", "a", "d", "b", "h", "g", "c", "f"};
125+
for (int i = 0; i < shuffled.length; i++) {
126+
assertThat(scenario.choicesOf("/data/choice").get(i).getValue(), is(shuffled[i]));
127+
}
96128
}
97129

98130
@Test
@@ -106,17 +138,23 @@ public void stringTextSeedConvertsWhenUsedInCalculate() throws IOException, XFor
106138
)),
107139
instance("choices",
108140
item("a", "A"),
109-
item("b", "B")
141+
item("b", "B"),
142+
item("c", "C"),
143+
item("d", "D"),
144+
item("e", "E"),
145+
item("f", "F"),
146+
item("g", "G"),
147+
item("h", "H")
110148
),
111-
bind("/data/choice").type("string").calculate("selected-at(join(' ', randomize(instance('choices')/root/item/label, 'foo')), 0)")
149+
bind("/data/choice").type("string").calculate("join('', randomize(instance('choices')/root/item/label, 'foo'))")
112150
)
113151
),
114152
body(
115153
input("/data/choice")
116154
)
117155
));
118156

119-
assertThat(scenario.answerOf("/data/choice").getDisplayText(), is("B"));
157+
assertThat(scenario.answerOf("/data/choice").getDisplayText(), is("EADBHGCF"));
120158
}
121159

122160
@Test
@@ -155,4 +193,98 @@ public void seedInRepeatIsEvaluatedForEachInstance() throws IOException, XFormPa
155193
assertThat(scenario.choicesOf("/data/repeat[2]/choice").get(0).getValue(), is("b"));
156194
assertThat(scenario.choicesOf("/data/repeat[1]/choice").get(0).getValue(), is("a"));
157195
}
196+
197+
@Test
198+
public void seedFromArbitraryInputCanBeUsed() throws IOException, XFormParser.ParseException {
199+
Scenario scenario = Scenario.init("Randomize non-numeric seed", html(
200+
head(
201+
title("Randomize non-numeric seed"),
202+
model(
203+
mainInstance(t("data id=\"rand-non-numeric\"",
204+
t("input"),
205+
t("choice")
206+
)),
207+
instance("choices",
208+
item("a", "A"),
209+
item("b", "B"),
210+
item("c", "C"),
211+
item("d", "D"),
212+
item("e", "E"),
213+
item("f", "F"),
214+
item("g", "G"),
215+
item("h", "H")
216+
),
217+
bind("/data/input").type("geopoint"),
218+
bind("/data/choice").type("string")
219+
)
220+
),
221+
body(
222+
input("/data/input"),
223+
select1Dynamic("/data/choice", "randomize(instance('choices')/root/item, /data/input)")
224+
)
225+
));
226+
227+
scenario.answer("/data/input", "-6.8137120026589315 39.29392995851879");
228+
String[] shuffled = {"h", "b", "d", "f", "a", "g", "c", "e"};
229+
for (int i = 0; i < shuffled.length; i++) {
230+
assertThat(scenario.choicesOf("/data/choice").get(i).getValue(), is(shuffled[i]));
231+
}
232+
}
233+
234+
@Test
235+
public void seed0FromNaNs() throws IOException, XFormParser.ParseException {
236+
Scenario scenario = Scenario.init("Randomize non-numeric seed", html(
237+
head(
238+
title("Randomize non-numeric seed"),
239+
model(
240+
mainInstance(t("data id=\"rand-non-numeric\"",
241+
t("input_emptystring"),
242+
t("input_somestring"),
243+
t("input_int"),
244+
t("choice_emptystring"),
245+
t("choice_somestring"),
246+
t("choice_int")
247+
)),
248+
instance("choices",
249+
item("a", "A"),
250+
item("b", "B"),
251+
item("c", "C"),
252+
item("d", "D"),
253+
item("e", "E"),
254+
item("f", "F"),
255+
item("g", "G"),
256+
item("h", "H")
257+
),
258+
bind("/data/input_emptystring").type("string"),
259+
bind("/data/input_somestring").type("string"),
260+
bind("/data/input_int").type("int")
261+
)
262+
),
263+
body(
264+
input("/data/input_emptystring"),
265+
input("/data/input_somestring"),
266+
input("/data/input_int"),
267+
select1Dynamic("/data/choice_emptystring", "randomize(instance('choices')/root/item, /data/input_emptystring)"),
268+
select1Dynamic("/data/choice_somestring", "randomize(instance('choices')/root/item, /data/input_somestring)"),
269+
select1Dynamic("/data/choice_int", "randomize(instance('choices')/root/item, /data/input_int)")
270+
)
271+
));
272+
273+
scenario.answer("/data/input_emptystring", "");
274+
scenario.answer("/data/input_somestring", "somestring");
275+
scenario.answer("/data/input_int", "0");
276+
277+
String[] shuffled_NaN_or_0 = {"c", "b", "h", "a", "f", "d", "g", "e"};
278+
String[] shuffled_somestring = {"e", "b", "c", "g", "d", "a", "f", "h"};
279+
assertThat("somestring-seeded expected order is distinct from 0-seeded expected order", !Arrays.equals(shuffled_NaN_or_0, shuffled_somestring));
280+
String[] shuffledfields = {"/data/choice_emptystring", "/data/choice_int"};
281+
for (int i = 0; i < shuffled_NaN_or_0.length; i++) {
282+
for (String shuffledfield : shuffledfields) {
283+
assertThat(scenario.choicesOf(shuffledfield).get(i).getValue(), is(shuffled_NaN_or_0[i]));
284+
}
285+
}
286+
for (int i = 0; i < shuffled_somestring.length; i++) {
287+
assertThat(scenario.choicesOf("/data/choice_somestring").get(i).getValue(), is(shuffled_somestring[i]));
288+
}
289+
}
158290
}

src/test/java/org/javarosa/xpath/expr/XPathFuncAsSomethingTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class XPathFuncAsSomethingTest {
1515
@Test
1616
public void toLongHashHashesWell() {
1717
assertThat(toLongHash("Hello"), equalTo(1756278180214341157L));
18-
assertThat(toLongHash(""), equalTo(-2039914840885289964L));
18+
assertThat(toLongHash(""), equalTo(0L)); // the empty string would actually hash to -2039914840885289964L; but we've added this quirk for backward compatibility.
1919
}
2020

2121
@Test

0 commit comments

Comments
 (0)