package org.commcare.android.tests.processing;
import org.commcare.CommCareTestApplication;
import org.commcare.android.CommCareTestRunner;
import org.commcare.android.database.user.models.FormRecord;
import org.commcare.android.resource.installers.XFormAndroidInstaller;
import org.commcare.android.util.TestUtils;
import org.commcare.models.AndroidClassHasher;
import org.commcare.models.AndroidPrototypeFactory;
import org.javarosa.core.model.FormDef;
import org.javarosa.core.util.externalizable.DeserializationException;
import org.javarosa.core.util.externalizable.ExtUtil;
import org.javarosa.core.util.externalizable.PrototypeFactory;
import org.javarosa.xform.util.XFormUtils;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* Tests for the serializaiton and deserialzation of XForms.
*
* @author ctsims
*/
@Config(application = CommCareTestApplication.class)
@RunWith(CommCareTestRunner.class)
public class FormStorageTest {
private boolean noSerializiationExceptions;
// Contains the names of all externalizable classes that have ever existed in CommCare, so as
// to ensure that users on any prior version of CommCare will be able to load their saved
// forms upon upgrading. When a class is migrated, it should NOT be removed from this list,
// but should be moved to the bottom with a comment as to what version it was migrated in
private static final List<String> completeHistoryOfExternalizableClasses = Arrays.asList(
// current class names:
"org.commcare.android.database.app.models.ResourceModelUpdater"
, "org.commcare.android.database.app.models.UserKeyRecord"
, "org.commcare.android.database.app.models.UserKeyRecordV1"
, "org.commcare.android.database.global.models.AndroidSharedKeyRecord"
, "org.commcare.android.database.global.models.ApplicationRecord"
, "org.commcare.android.database.global.models.ApplicationRecordV1"
, "org.commcare.android.database.user.models.ACase"
, "org.commcare.android.database.user.models.ACasePreV6Model$CaseIndexUpdater"
, "org.commcare.android.database.user.models.ACasePreV6Model"
, "org.commcare.android.database.user.models.AUser"
, "org.commcare.android.database.user.models.FormRecord"
, "org.commcare.android.database.user.models.FormRecordV1"
, "org.commcare.android.database.user.models.FormRecordV2"
, "org.commcare.android.database.user.models.GeocodeCacheModel"
, "org.commcare.android.database.user.models.SessionStateDescriptor"
, "org.commcare.android.javarosa.AndroidLogEntry"
, "org.commcare.android.javarosa.AndroidXFormExtensions"
, "org.commcare.android.javarosa.IntentCallout"
, "org.commcare.android.javarosa.PollSensorAction"
, "org.commcare.android.javarosa.DeviceReportRecord"
, "org.commcare.android.logging.ForceCloseLogEntry"
, "org.commcare.android.resource.installers.LocaleAndroidInstaller"
, "org.commcare.android.resource.installers.MediaFileAndroidInstaller"
, "org.commcare.android.resource.installers.OfflineUserRestoreAndroidInstaller"
, "org.commcare.android.resource.installers.ProfileAndroidInstaller"
, "org.commcare.android.resource.installers.SuiteAndroidInstaller"
, "org.commcare.android.resource.installers.XFormAndroidInstaller"
, "org.commcare.android.storage.framework.Persisted"
, "org.commcare.cases.instance.CaseDataInstance"
, "org.commcare.cases.ledger.Ledger"
, "org.commcare.cases.model.Case"
, "org.commcare.cases.model.CaseIndex"
, "org.commcare.cases.model.StorageIndexedTreeElementModel"
, "org.commcare.logging.XPathErrorEntry"
, "org.commcare.resources.model.Resource"
, "org.commcare.resources.model.ResourceLocation"
, "org.commcare.resources.model.installers.BasicInstaller"
, "org.commcare.resources.model.installers.CacheInstaller"
, "org.commcare.resources.model.installers.LocaleFileInstaller"
, "org.commcare.resources.model.installers.LoginImageInstaller"
, "org.commcare.resources.model.installers.MediaInstaller"
, "org.commcare.resources.model.installers.OfflineUserRestoreInstaller"
, "org.commcare.resources.model.installers.ProfileInstaller"
, "org.commcare.resources.model.installers.SuiteInstaller"
, "org.commcare.resources.model.installers.XFormInstaller"
, "org.commcare.session.SessionFrame"
, "org.commcare.suite.model.Action"
, "org.commcare.suite.model.AssertionSet"
, "org.commcare.suite.model.Callout"
, "org.commcare.suite.model.ComputedDatum"
, "org.commcare.suite.model.Detail"
, "org.commcare.suite.model.DetailField"
, "org.commcare.suite.model.DisplayUnit"
, "org.commcare.suite.model.EntityDatum"
, "org.commcare.suite.model.Entry"
, "org.commcare.suite.model.FormEntry"
, "org.commcare.suite.model.FormIdDatum"
, "org.commcare.suite.model.Menu"
, "org.commcare.suite.model.OfflineUserRestore"
, "org.commcare.suite.model.Profile"
, "org.commcare.suite.model.PropertySetter"
, "org.commcare.suite.model.RemoteQueryDatum"
, "org.commcare.suite.model.SessionDatum"
, "org.commcare.suite.model.StackFrameStep"
, "org.commcare.suite.model.StackOperation"
, "org.commcare.suite.model.Suite"
, "org.commcare.suite.model.RemoteRequestEntry"
, "org.commcare.suite.model.PostRequest"
, "org.commcare.suite.model.Text"
, "org.commcare.suite.model.ViewEntry"
, "org.commcare.suite.model.graph.Annotation"
, "org.commcare.suite.model.graph.BubbleSeries"
, "org.commcare.suite.model.graph.Graph"
, "org.commcare.suite.model.graph.XYSeries"
, "org.commcare.xml.DummyGraphParser$DummyGraphDetailTemplate"
, "org.javarosa.core.log.LogEntry"
, "org.javarosa.core.model.FormDef"
, "org.javarosa.core.model.GroupDef"
, "org.javarosa.core.model.ItemsetBinding"
, "org.javarosa.core.model.QuestionDef"
, "org.javarosa.core.model.QuestionString"
, "org.javarosa.core.model.SelectChoice"
, "org.javarosa.core.model.SubmissionProfile"
, "org.javarosa.core.model.UploadQuestionExtension"
, "org.javarosa.core.model.User"
, "org.javarosa.core.model.actions.Action"
, "org.javarosa.core.model.actions.ActionController"
, "org.javarosa.core.model.actions.SetValueAction"
, "org.javarosa.core.model.condition.Condition"
, "org.javarosa.core.model.condition.Constraint"
, "org.javarosa.core.model.condition.Recalculate"
, "org.javarosa.core.model.condition.Triggerable"
, "org.javarosa.core.model.data.BooleanData"
, "org.javarosa.core.model.data.DateData"
, "org.javarosa.core.model.data.DateTimeData"
, "org.javarosa.core.model.data.DecimalData"
, "org.javarosa.core.model.data.GeoPointData"
, "org.javarosa.core.model.data.IntegerData"
, "org.javarosa.core.model.data.LongData"
, "org.javarosa.core.model.data.PointerAnswerData"
, "org.javarosa.core.model.data.SelectMultiData"
, "org.javarosa.core.model.data.SelectOneData"
, "org.javarosa.core.model.data.StringData"
, "org.javarosa.core.model.data.TimeData"
, "org.javarosa.core.model.data.UncastData"
, "org.javarosa.core.model.data.helper.Selection"
, "org.javarosa.core.model.instance.DataInstance"
, "org.javarosa.core.model.instance.ExternalDataInstance"
, "org.javarosa.core.model.instance.FormInstance"
, "org.javarosa.core.model.instance.TreeElement"
, "org.javarosa.core.model.instance.TreeReference"
, "org.javarosa.core.model.instance.TreeReferenceLevel"
, "org.javarosa.core.reference.ReferenceDataSource"
, "org.javarosa.core.reference.RootTranslator"
, "org.javarosa.core.services.locale.Localizer"
, "org.javarosa.core.services.locale.TableLocaleSource"
, "org.javarosa.core.services.properties.Property"
, "org.javarosa.core.services.transport.payload.ByteArrayPayload"
, "org.javarosa.core.services.transport.payload.DataPointerPayload"
, "org.javarosa.core.services.transport.payload.MultiMessagePayload"
, "org.javarosa.core.util.SortedIntSet"
, "org.javarosa.core.util.externalizable.ExtWrapIntEncoding"
, "org.javarosa.core.util.externalizable.ExtWrapIntEncodingSmall"
, "org.javarosa.core.util.externalizable.ExtWrapIntEncodingUniform"
, "org.javarosa.core.util.externalizable.ExtWrapList"
, "org.javarosa.core.util.externalizable.ExtWrapListPoly"
, "org.javarosa.core.util.externalizable.ExtWrapMap"
, "org.javarosa.core.util.externalizable.ExtWrapMapPoly"
, "org.javarosa.core.util.externalizable.ExtWrapNullable"
, "org.javarosa.core.util.externalizable.ExtWrapTagged"
, "org.javarosa.core.util.externalizable.ExternalizableWrapper"
, "org.javarosa.form.api.FormEntryAction"
, "org.javarosa.form.api.FormEntrySession"
, "org.javarosa.model.xform.XPathReference"
, "org.javarosa.xpath.XPathConditional"
, "org.javarosa.xpath.expr.XPathArithExpr"
, "org.javarosa.xpath.expr.XPathBinaryOpExpr"
, "org.javarosa.xpath.expr.XPathBoolExpr"
, "org.javarosa.xpath.expr.XPathCmpExpr"
, "org.javarosa.xpath.expr.XPathEqExpr"
, "org.javarosa.xpath.expr.XPathExpression"
, "org.javarosa.xpath.expr.XPathFilterExpr"
, "org.javarosa.xpath.expr.XPathFuncExpr"
, "org.javarosa.xpath.expr.XPathNumNegExpr"
, "org.javarosa.xpath.expr.XPathNumericLiteral"
, "org.javarosa.xpath.expr.XPathOpExpr"
, "org.javarosa.xpath.expr.XPathPathExpr"
, "org.javarosa.xpath.expr.XPathQName"
, "org.javarosa.xpath.expr.XPathStep"
, "org.javarosa.xpath.expr.XPathStringLiteral"
, "org.javarosa.xpath.expr.XPathUnaryOpExpr"
, "org.javarosa.xpath.expr.XPathUnionExpr"
, "org.javarosa.xpath.expr.XPathVariableReference"
, "org.javarosa.xpath.expr.XPathAbsFunc"
, "org.javarosa.xpath.expr.XPathAcosFunc"
, "org.javarosa.xpath.expr.XPathAsinFunc"
, "org.javarosa.xpath.expr.XPathAtanFunc"
, "org.javarosa.xpath.expr.XPathAtanTwoFunc"
, "org.javarosa.xpath.expr.XPathBooleanFromStringFunc"
, "org.javarosa.xpath.expr.XPathBooleanFunc"
, "org.javarosa.xpath.expr.XPathCeilingFunc"
, "org.javarosa.xpath.expr.XPathChecklistFunc"
, "org.javarosa.xpath.expr.XPathConcatFunc"
, "org.javarosa.xpath.expr.XPathCondFunc"
, "org.javarosa.xpath.expr.XPathContainsFunc"
, "org.javarosa.xpath.expr.XPathCosFunc"
, "org.javarosa.xpath.expr.XPathCountFunc"
, "org.javarosa.xpath.expr.XPathCountSelectedFunc"
, "org.javarosa.xpath.expr.XPathCustomRuntimeFunc"
, "org.javarosa.xpath.expr.XPathDateFunc"
, "org.javarosa.xpath.expr.XPathDependFunc"
, "org.javarosa.xpath.expr.XPathDistanceFunc"
, "org.javarosa.xpath.expr.XPathDoubleFunc"
, "org.javarosa.xpath.expr.XPathEndsWithFunc"
, "org.javarosa.xpath.expr.XPathExpFunc"
, "org.javarosa.xpath.expr.XPathExpression"
, "org.javarosa.xpath.expr.XPathFalseFunc"
, "org.javarosa.xpath.expr.XPathFloorFunc"
, "org.javarosa.xpath.expr.XPathFormatDateForCalendarFunc"
, "org.javarosa.xpath.expr.XPathFormatDateFunc"
, "org.javarosa.xpath.expr.XPathFuncExpr"
, "org.javarosa.xpath.expr.XPathIfFunc"
, "org.javarosa.xpath.expr.XPathIntFunc"
, "org.javarosa.xpath.expr.XPathJoinFunc"
, "org.javarosa.xpath.expr.XPathLogFunc"
, "org.javarosa.xpath.expr.XPathLogTenFunc"
, "org.javarosa.xpath.expr.XPathLowerCaseFunc"
, "org.javarosa.xpath.expr.XPathMaxFunc"
, "org.javarosa.xpath.expr.XPathMinFunc"
, "org.javarosa.xpath.expr.XPathNotFunc"
, "org.javarosa.xpath.expr.XPathNowFunc"
, "org.javarosa.xpath.expr.XPathNumberFunc"
, "org.javarosa.xpath.expr.XPathPathExpr"
, "org.javarosa.xpath.expr.XPathPiFunc"
, "org.javarosa.xpath.expr.XPathPositionFunc"
, "org.javarosa.xpath.expr.XPathPowFunc"
, "org.javarosa.xpath.expr.XPathRandomFunc"
, "org.javarosa.xpath.expr.XPathRegexFunc"
, "org.javarosa.xpath.expr.XPathReplaceFunc"
, "org.javarosa.xpath.expr.XPathRoundFunc"
, "org.javarosa.xpath.expr.XPathSelectedAtFunc"
, "org.javarosa.xpath.expr.XPathSelectedFunc"
, "org.javarosa.xpath.expr.XPathSinFunc"
, "org.javarosa.xpath.expr.XPathSqrtFunc"
, "org.javarosa.xpath.expr.XPathStartsWithFunc"
, "org.javarosa.xpath.expr.XPathStringFunc"
, "org.javarosa.xpath.expr.XPathStringLengthFunc"
, "org.javarosa.xpath.expr.XPathSubstrFunc"
, "org.javarosa.xpath.expr.XPathSubstringAfterFunc"
, "org.javarosa.xpath.expr.XPathSubstringBeforeFunc"
, "org.javarosa.xpath.expr.XPathSumFunc"
, "org.javarosa.xpath.expr.XPathTanFunc"
, "org.javarosa.xpath.expr.XPathTodayFunc"
, "org.javarosa.xpath.expr.XPathTranslateFunc"
, "org.javarosa.xpath.expr.XPathTrueFunc"
, "org.javarosa.xpath.expr.XPathUpperCaseFunc"
, "org.javarosa.xpath.expr.XPathUuidFunc"
, "org.javarosa.xpath.expr.XPathWeightedChecklistFunc"
, "org.javarosa.xpath.expr.XpathCoalesceFunc"
// Migrated in 2.28
, "org.odk.collect.android.jr.extensions.AndroidXFormExtensions"
, "org.odk.collect.android.jr.extensions.IntentCallout"
, "org.odk.collect.android.jr.extensions.PollSensorAction"
// Removed in 2.32, but doesn't require migration because it was
// never actually serialized
//, "org.javarosa.core.model.DataBinding"
// Added in 2.35
, "org.javarosa.xpath.expr.XPathJoinChunkFunc"
, "org.javarosa.xpath.expr.XPathIdCompressFunc"
// Added in 2.36
, "org.commcare.heartbeat.UpdateToPrompt"
, "org.commcare.android.database.global.models.AppAvailableToInstall"
);
@Before
public void setup() {
XFormAndroidInstaller.registerAndroidLevelFormParsers();
}
@Test
public void testAllExternalizablesInPrototypeFactory() {
PrototypeFactory pf = TestUtils.getStaticPrototypeFactory();
List<String> extClassesInPF =
CommCareTestApplication.getTestPrototypeFactoryClasses();
// Ensure all externalizable classes are present in list of classes.
// Enforcing this keeps the list up-to-date, which is crucial for the loop check below
for (String className : extClassesInPF) {
// Should fail if a new class implementing externalizable is added
// without updating the list used by this test.
assertTrue(
"Please keep test list up-to-date by adding '" + className + "' to list",
completeHistoryOfExternalizableClasses.contains(className));
}
// Ensure that any renamed externalizable classes are properly migrated
for (String className : completeHistoryOfExternalizableClasses) {
Assert.assertNotNull(
"The class '" + className + "' wasn't properly migrated in the prototype factory. " +
"A migration strategy for this class should be added in AndroidPrototypeFactory.",
pf.getClass(AndroidClassHasher.getInstance().getClassnameHash(className)));
}
// For completeness, make sure that migrated classes are present in the test class list used
for (String className : AndroidPrototypeFactory.getMigratedClassNames()) {
assertTrue("The migrated class '" + className + "' isn't represented in the test list",
completeHistoryOfExternalizableClasses.contains(className));
}
}
@Test
public void testRegressionXFormSerializations() {
FormDef def = XFormUtils.getFormFromResource("/forms/placeholder.xml");
try {
ExtUtil.deserialize(ExtUtil.serialize(def), FormDef.class,
TestUtils.getStaticPrototypeFactory());
} catch (IOException | DeserializationException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
/**
* Ensure that a form that has intent callouts can be serialized and deserialized
*/
@Test
public void testCalloutSerializations() {
FormDef def =
XFormUtils.getFormFromResource("/forms/intent_callout_serialization_test.xml");
try {
ExtUtil.deserialize(ExtUtil.serialize(def), FormDef.class, TestUtils.getStaticPrototypeFactory());
} catch (IOException | DeserializationException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
@Test
public void parallelFormRecordSerializationTest() {
noSerializiationExceptions = true;
// Make sure that Persited externalization works in a mult-thread setting
// Important because setting field accessibility can lead to to throws of IllegalAccessException
Thread t1 = new BulkFormRecordSerializer();
Thread t2 = new BulkFormRecordSerializer();
Thread t3 = new BulkFormRecordSerializer();
t1.start();
t2.start();
t3.start();
try {
t1.join();
t2.join();
t3.join();
} catch (InterruptedException e) {
fail(e.getMessage());
}
assertTrue(noSerializiationExceptions);
}
private class BulkFormRecordSerializer extends Thread {
@Override
public void run() {
int i = 10;
while (i-- > 0) {
try {
serializeFormRecord();
} catch (Exception e) {
e.printStackTrace();
noSerializiationExceptions = false;
}
}
}
}
private static void serializeFormRecord() throws IOException, DeserializationException {
FormRecord r = new FormRecord("", FormRecord.STATUS_UNSTARTED, "some form",
new byte[]{1, 2, 3}, null, new Date(0), "some app id");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
r.writeExternal(new DataOutputStream(bos));
FormRecord newRecord = new FormRecord();
ByteArrayInputStream inputStream = new ByteArrayInputStream(bos.toByteArray());
newRecord.readExternal(new DataInputStream(inputStream), TestUtils.getStaticPrototypeFactory());
}
}