diff --git a/android-core/src/test/java/com/mparticle/testutils/RandomUtilsTest.java b/android-core/src/test/java/com/mparticle/testutils/RandomUtilsTest.java
new file mode 100644
index 000000000..cf5456259
--- /dev/null
+++ b/android-core/src/test/java/com/mparticle/testutils/RandomUtilsTest.java
@@ -0,0 +1,65 @@
+package com.mparticle.testutils;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.mparticle.MParticle;
+
+import org.junit.Test;
+
+import java.util.Map;
+
+/**
+ * Guards the invariants that random identity generation must hold for the identity tests to be
+ * deterministic.
+ *
+ *
An empty identity map is not an innocuous edge case: {@code MParticleIdentityClientImpl.modify()}
+ * short-circuits and returns 200 without issuing an HTTP request when there are no identity
+ * changes, so a test that awaits the modify request hangs and then fails on an unrelated
+ * assertion. That was the cause of the intermittent
+ * {@code MParticleIdentityClientImplTest.testModifyMessage} failures.
+ */
+public class RandomUtilsTest {
+ private static final int ITERATIONS = 1000;
+
+ private final RandomUtils randomUtils = new RandomUtils();
+
+ @Test
+ public void testRandomUserIdentitiesNeverEmpty() {
+ for (int i = 0; i < ITERATIONS; i++) {
+ assertNonEmptyAndAliasFree(randomUtils.getRandomUserIdentities());
+ }
+ }
+
+ @Test
+ public void testBoundedRandomUserIdentitiesNeverEmpty() {
+ int poolSize = MParticle.IdentityType.values().length;
+ for (int max = 1; max <= poolSize + 1; max++) {
+ // Fewer iterations per bound: the pre-fix failure rate here was 1-in-22, so this is
+ // still overwhelmingly likely to catch a regression.
+ for (int i = 0; i < ITERATIONS / 10; i++) {
+ Map identities =
+ randomUtils.getRandomUserIdentities(max);
+ assertNonEmptyAndAliasFree(identities);
+ assertTrue(
+ "expected at most " + max + " identities but got " + identities.size(),
+ identities.size() <= max);
+ }
+ }
+ }
+
+ @Test
+ public void testMockRandomUserIdentitiesNeverEmpty() {
+ for (int i = 0; i < ITERATIONS; i++) {
+ assertNonEmptyAndAliasFree(
+ com.mparticle.mock.utils.RandomUtils.getInstance().getRandomUserIdentities());
+ }
+ }
+
+ private void assertNonEmptyAndAliasFree(Map identities) {
+ assertFalse("random user identities must never be empty", identities.isEmpty());
+ assertFalse(
+ "Alias is not a settable user identity and must never be generated",
+ identities.containsKey(MParticle.IdentityType.Alias));
+ }
+}
diff --git a/testutils/src/main/java/com/mparticle/mock/utils/RandomUtils.java b/testutils/src/main/java/com/mparticle/mock/utils/RandomUtils.java
index 6e3e2b439..ca396930d 100644
--- a/testutils/src/main/java/com/mparticle/mock/utils/RandomUtils.java
+++ b/testutils/src/main/java/com/mparticle/mock/utils/RandomUtils.java
@@ -7,6 +7,8 @@
import org.junit.Test;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -24,6 +26,21 @@ public class RandomUtils {
private static RandomUtils instance;
+ /**
+ * Every IdentityType that can legitimately appear in a user identity map. Alias is excluded
+ * from the pool rather than removed after the draw -- removing it afterwards can leave the
+ * map empty, which silently changes the meaning of any request built from it.
+ */
+ private static final List ASSIGNABLE_IDENTITY_TYPES =
+ assignableIdentityTypes();
+
+ private static List assignableIdentityTypes() {
+ List types =
+ new ArrayList(Arrays.asList(MParticle.IdentityType.values()));
+ types.remove(MParticle.IdentityType.Alias);
+ return Collections.unmodifiableList(types);
+ }
+
public static RandomUtils getInstance() {
if (instance == null) {
instance = new RandomUtils();
@@ -31,16 +48,19 @@ public static RandomUtils getInstance() {
return instance;
}
+ /**
+ * @return between 1 and the number of assignable IdentityTypes distinct random user
+ * identities. Never empty.
+ */
public Map getRandomUserIdentities() {
- Map randomIdentities = new HashMap();
+ int poolSize = ASSIGNABLE_IDENTITY_TYPES.size();
+ int numIdentities = randomInt(1, poolSize + 1);
- int identityTypeLength = MParticle.IdentityType.values().length;
- int numIdentities = randomInt(1, identityTypeLength);
- Set identityIndices = randomIntSet(0, identityTypeLength, numIdentities);
+ Map randomIdentities = new HashMap();
+ Set identityIndices = randomIntSet(0, poolSize, numIdentities);
for (Integer identityIndex : identityIndices) {
- randomIdentities.put(MParticle.IdentityType.values()[identityIndex], getAlphaNumericString(randomInt(1, 55)));
+ randomIdentities.put(ASSIGNABLE_IDENTITY_TYPES.get(identityIndex), getAlphaNumericString(randomInt(1, 55)));
}
- randomIdentities.remove(MParticle.IdentityType.Alias);
return randomIdentities;
}
diff --git a/testutils/src/main/java/com/mparticle/testutils/RandomUtils.java b/testutils/src/main/java/com/mparticle/testutils/RandomUtils.java
index 6714e2c61..b90a4ec59 100644
--- a/testutils/src/main/java/com/mparticle/testutils/RandomUtils.java
+++ b/testutils/src/main/java/com/mparticle/testutils/RandomUtils.java
@@ -2,7 +2,11 @@
import com.mparticle.MParticle;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
@@ -16,20 +20,39 @@ public class RandomUtils {
private static final String sNumbers = "0123456789";
private static final String sCharacters = " ,.";
+ /**
+ * Every IdentityType that can legitimately appear in a user identity map. Alias is excluded
+ * from the pool rather than removed after the draw -- removing it afterwards can leave the
+ * map empty, which silently changes the meaning of any request built from it.
+ */
+ private static final List ASSIGNABLE_IDENTITY_TYPES =
+ assignableIdentityTypes();
+
+ private static List assignableIdentityTypes() {
+ List types =
+ new ArrayList(Arrays.asList(MParticle.IdentityType.values()));
+ types.remove(MParticle.IdentityType.Alias);
+ return Collections.unmodifiableList(types);
+ }
+
public Map getRandomUserIdentities() {
return getRandomUserIdentities(null);
}
+ /**
+ * @param max the most identities to return, or null for no limit
+ * @return between 1 and {@code max} distinct random user identities. Never empty.
+ */
public Map getRandomUserIdentities(Integer max) {
- Map randomIdentities = new HashMap();
+ int poolSize = ASSIGNABLE_IDENTITY_TYPES.size();
+ int upperBound = (max != null && max < poolSize) ? Math.max(1, max) : poolSize;
+ int numIdentities = randomInt(1, upperBound + 1);
- int identityTypeLength = MParticle.IdentityType.values().length;
- int numIdentities = randomInt(1, (max != null && max < identityTypeLength) ? max : identityTypeLength);
- Set identityIndices = randomIntSet(0, identityTypeLength, numIdentities);
+ Map randomIdentities = new HashMap();
+ Set identityIndices = randomIntSet(0, poolSize, numIdentities);
for (Integer identityIndex : identityIndices) {
- randomIdentities.put(MParticle.IdentityType.values()[identityIndex], getAlphaNumericString(randomInt(1, 55)));
+ randomIdentities.put(ASSIGNABLE_IDENTITY_TYPES.get(identityIndex), getAlphaNumericString(randomInt(1, 55)));
}
- randomIdentities.remove(MParticle.IdentityType.Alias);
return randomIdentities;
}