diff --git a/src/main/java/org/commcare/cases/instance/CaseInstanceTreeElement.java b/src/main/java/org/commcare/cases/instance/CaseInstanceTreeElement.java index aeeea13853..34540521b0 100644 --- a/src/main/java/org/commcare/cases/instance/CaseInstanceTreeElement.java +++ b/src/main/java/org/commcare/cases/instance/CaseInstanceTreeElement.java @@ -71,4 +71,8 @@ protected Hashtable getStorageIndexMap() { return indices; } + + protected String getStorageCacheName() { + return CaseInstanceTreeElement.MODEL_NAME; + } } diff --git a/src/main/java/org/commcare/cases/instance/IndexedFixtureChildElement.java b/src/main/java/org/commcare/cases/instance/IndexedFixtureChildElement.java index 5a3b972981..fb72912ae4 100644 --- a/src/main/java/org/commcare/cases/instance/IndexedFixtureChildElement.java +++ b/src/main/java/org/commcare/cases/instance/IndexedFixtureChildElement.java @@ -2,6 +2,7 @@ import org.commcare.cases.model.StorageIndexedTreeElementModel; import org.commcare.cases.query.QueryContext; +import org.commcare.cases.query.QuerySensitive; import org.javarosa.core.model.instance.TreeElement; import org.javarosa.core.model.instance.TreeReference; @@ -12,7 +13,7 @@ * * @author Phillip Mates (pmates@dimagi.com) */ -public class IndexedFixtureChildElement extends StorageBackedChildElement { +public class IndexedFixtureChildElement extends StorageBackedChildElement implements QuerySensitive{ private TreeElement empty; protected IndexedFixtureChildElement(StorageInstanceTreeElement parent, @@ -65,4 +66,9 @@ public static IndexedFixtureChildElement buildFixtureChildTemplate(IndexedFixtur template.empty.setMult(TreeReference.INDEX_TEMPLATE); return template; } + + @Override + public void prepareForUseInCurrentContext(QueryContext queryContext) { + cache(queryContext); + } } diff --git a/src/main/java/org/commcare/cases/instance/IndexedFixtureInstanceTreeElement.java b/src/main/java/org/commcare/cases/instance/IndexedFixtureInstanceTreeElement.java index 94951dd0cd..25a916193b 100644 --- a/src/main/java/org/commcare/cases/instance/IndexedFixtureInstanceTreeElement.java +++ b/src/main/java/org/commcare/cases/instance/IndexedFixtureInstanceTreeElement.java @@ -21,11 +21,13 @@ public class IndexedFixtureInstanceTreeElement extends StorageInstanceTreeElement { private Hashtable storageIndexMap = null; + private String cacheKey; private IndexedFixtureInstanceTreeElement(AbstractTreeElement instanceRoot, IStorageUtilityIndexed storage, String modelName, String childName) { super(instanceRoot, storage, modelName, childName); + cacheKey = modelName + "|" + childName; } public static IndexedFixtureInstanceTreeElement get(UserSandbox sandbox, @@ -68,4 +70,8 @@ protected Hashtable getStorageIndexMap() { return storageIndexMap; } + + protected String getStorageCacheName() { + return cacheKey; + } } diff --git a/src/main/java/org/commcare/cases/instance/LedgerInstanceTreeElement.java b/src/main/java/org/commcare/cases/instance/LedgerInstanceTreeElement.java index b53a44e2cc..dc2a0605ea 100644 --- a/src/main/java/org/commcare/cases/instance/LedgerInstanceTreeElement.java +++ b/src/main/java/org/commcare/cases/instance/LedgerInstanceTreeElement.java @@ -44,4 +44,8 @@ protected Hashtable getStorageIndexMap() { return indices; } + + protected String getStorageCacheName() { + return MODEL_NAME; + } } diff --git a/src/main/java/org/commcare/cases/instance/StorageInstanceTreeElement.java b/src/main/java/org/commcare/cases/instance/StorageInstanceTreeElement.java index 10a101c742..36fa1983ad 100644 --- a/src/main/java/org/commcare/cases/instance/StorageInstanceTreeElement.java +++ b/src/main/java/org/commcare/cases/instance/StorageInstanceTreeElement.java @@ -2,6 +2,9 @@ import org.commcare.cases.query.QueryContext; import org.commcare.cases.util.StorageBackedTreeRoot; +import org.commcare.modern.engine.cases.RecordObjectCache; +import org.commcare.modern.engine.cases.RecordSetResultCache; +import org.commcare.modern.util.Pair; import org.javarosa.core.model.data.IAnswerData; import org.javarosa.core.model.instance.AbstractTreeElement; import org.javarosa.core.model.instance.TreeElement; @@ -14,6 +17,7 @@ import org.javarosa.core.util.Interner; import org.javarosa.core.util.externalizable.Externalizable; +import java.util.LinkedHashSet; import java.util.Vector; /** @@ -269,7 +273,60 @@ protected abstract T buildElement(StorageInstanceTreeElement storageIn int recordId, String id, int mult); protected Model getElement(int recordId, QueryContext context) { - EvaluationTrace trace = new EvaluationTrace("Model Load[" + childName+"]"); + if (context == null || getStorageCacheName() == null) { + return getElementSingular(recordId, context); + } + RecordSetResultCache recordSetCache = context.getQueryCacheOrNull(RecordSetResultCache.class); + + RecordObjectCache recordObjectCache = getRecordObjectCacheIfRelevant(context); + + if(recordObjectCache != null) { + if (recordObjectCache.isLoaded(recordId)) { + return recordObjectCache.getLoadedRecordObject(recordId); + } + + if (canLoadRecordFromGroup(recordSetCache, recordId)) { + Pair> tranche = + recordSetCache.getRecordSetForRecordId(getStorageCacheName(), recordId); + EvaluationTrace loadTrace = + new EvaluationTrace(String.format("Model [%s]: Bulk Load [%s}", + this.getStorageCacheName(),tranche.first)); + + LinkedHashSet body = tranche.second; + storage.bulkRead(body, recordObjectCache.getLoadedCaseMap()); + loadTrace.setOutcome("Loaded: " + body.size()); + context.reportTrace(loadTrace); + + return recordObjectCache.getLoadedRecordObject(recordId); + } + } + + return getElementSingular(recordId, context); + } + + private boolean canLoadRecordFromGroup(RecordSetResultCache recordSetCache, int recordId) { + return recordSetCache != null && recordSetCache.hasMatchingRecordSet(getStorageCacheName(), recordId); + } + + /** + * Get a record object cache if it's appropriate in the current context. + */ + private RecordObjectCache getRecordObjectCacheIfRelevant(QueryContext context) { + // If the query isn't currently in a bulk mode, don't force an object cache to exist unless + // it already does + if (context.getScope() < QueryContext.BULK_QUERY_THRESHOLD) { + return context.getQueryCacheOrNull(RecordObjectCache.class); + } else { + return context.getQueryCache(RecordObjectCache.class); + } + } + + /** + * Retrieves a model for the provided record ID using a guaranteed singular lookup from + * storage. This is the "Safe" fallback behavior for lookups. + */ + protected Model getElementSingular(int recordId, QueryContext context) { + EvaluationTrace trace = new EvaluationTrace(String.format("Model [%s]: Singular Load", getStorageCacheName())); Model m = storage.read(recordId); diff --git a/src/main/java/org/commcare/cases/util/StorageBackedTreeRoot.java b/src/main/java/org/commcare/cases/util/StorageBackedTreeRoot.java index f7f6bb80c9..135fd42157 100644 --- a/src/main/java/org/commcare/cases/util/StorageBackedTreeRoot.java +++ b/src/main/java/org/commcare/cases/util/StorageBackedTreeRoot.java @@ -5,6 +5,8 @@ import org.commcare.cases.query.IndexedValueLookup; import org.commcare.cases.query.PredicateProfile; import org.commcare.cases.query.handlers.BasicStorageBackedCachingQueryHandler; +import org.commcare.modern.engine.cases.RecordSetResultCache; +import org.commcare.modern.util.PerformanceTuningUtil; import org.javarosa.core.model.condition.EvaluationContext; import org.javarosa.core.model.instance.AbstractTreeElement; import org.javarosa.core.model.instance.TreeReference; @@ -20,6 +22,7 @@ import java.util.Collection; import java.util.Enumeration; import java.util.Hashtable; +import java.util.LinkedHashSet; import java.util.List; import java.util.Vector; @@ -258,24 +261,39 @@ protected Collection getNextIndexMatch(Vector profile IndexedValueLookup op = (IndexedValueLookup)profiles.elementAt(0); - EvaluationTrace trace = new EvaluationTrace("Model Index[" + op.key + "] Lookup"); //Get matches if it works - List returnValue = storage.getIDsForValue(op.key, op.value); + List ids = storage.getIDsForValue(op.key, op.value); + + if(getStorageCacheName() != null && + ids.size() > 50 && ids.size() < PerformanceTuningUtil.getMaxPrefetchCaseBlock()) { + RecordSetResultCache cue = currentQueryContext.getQueryCache(RecordSetResultCache.class); + String bulkRecordSetKey = String.format("%s|%s", op.key, op.value); + cue.reportBulkRecordSet(bulkRecordSetKey, getStorageCacheName(), new LinkedHashSet(ids)); + } + + + trace.setOutcome("results: " + ids.size()); - trace.setOutcome("results: " + returnValue.size()); if (currentQueryContext != null) { currentQueryContext.reportTrace(trace); } if(defaultCacher != null) { - defaultCacher.cacheResult(op.key, op.value, returnValue); + defaultCacher.cacheResult(op.key, op.value, ids); } //If we processed this, pop it off the queue profiles.removeElementAt(0); - return returnValue; + return ids; } + + /** + * @return A string which will provide a unique name for the storage that is used in this tree + * root. Used to differentiate the record ID's retrieved during operations on this root in + * internal caches + */ + protected abstract String getStorageCacheName(); } diff --git a/src/main/java/org/commcare/modern/engine/cases/CaseIndexQuerySetTransform.java b/src/main/java/org/commcare/modern/engine/cases/CaseIndexQuerySetTransform.java index 3dd85fc3e9..4972c4bb81 100644 --- a/src/main/java/org/commcare/modern/engine/cases/CaseIndexQuerySetTransform.java +++ b/src/main/java/org/commcare/modern/engine/cases/CaseIndexQuerySetTransform.java @@ -1,5 +1,6 @@ package org.commcare.modern.engine.cases; +import org.commcare.cases.instance.CaseInstanceTreeElement; import org.commcare.cases.query.QueryContext; import org.commcare.cases.query.queryset.CaseQuerySetLookup; import org.commcare.cases.query.queryset.DerivedCaseQueryLookup; @@ -68,7 +69,9 @@ protected ModelQuerySet loadModelQuerySet(QueryContext queryContext) { private void cacheCaseModelQuerySet(QueryContext queryContext, DualTableSingleMatchModelQuerySet ret) { int modelQueryMagnitude = ret.getSetBody().size(); if(modelQueryMagnitude > QueryContext.BULK_QUERY_THRESHOLD && modelQueryMagnitude < PerformanceTuningUtil.getMaxPrefetchCaseBlock()) { - queryContext.getQueryCache(CaseSetResultCache.class).reportBulkCaseSet(this.getCurrentQuerySetId(), ret.getSetBody()); + queryContext.getQueryCache(RecordSetResultCache.class). + reportBulkRecordSet(this.getCurrentQuerySetId(), + CaseInstanceTreeElement.MODEL_NAME, ret.getSetBody()); } } diff --git a/src/main/java/org/commcare/modern/engine/cases/CaseObjectCache.java b/src/main/java/org/commcare/modern/engine/cases/CaseObjectCache.java deleted file mode 100644 index 10039164fd..0000000000 --- a/src/main/java/org/commcare/modern/engine/cases/CaseObjectCache.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.commcare.modern.engine.cases; - -import org.commcare.cases.model.Case; -import org.commcare.cases.query.QueryCache; - -import java.util.HashMap; - -/** - * A straightforward cache object query cache. Stores cases by their record ID. - * - * Used by other optimizations to isolate doing bulk loads and ensure that they are relevant - * when they occur - * - * Created by ctsims on 6/22/2017. - */ - -public class CaseObjectCache implements QueryCache { - - private HashMap cachedCases = new HashMap<>(); - - public boolean isLoaded(int recordId) { - return cachedCases.containsKey(recordId); - } - - public HashMap getLoadedCaseMap() { - return cachedCases; - } - - public Case getLoadedCase(int recordId) { - return cachedCases.get(recordId); - } -} diff --git a/src/main/java/org/commcare/modern/engine/cases/CaseSetResultCache.java b/src/main/java/org/commcare/modern/engine/cases/CaseSetResultCache.java deleted file mode 100644 index 98032f26ef..0000000000 --- a/src/main/java/org/commcare/modern/engine/cases/CaseSetResultCache.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.commcare.modern.engine.cases; - -import org.commcare.cases.query.QueryCache; -import org.commcare.modern.util.Pair; - -import java.util.HashMap; -import java.util.LinkedHashSet; - -/** - * A case group result cache keeps track of different sets of "Bulk" cases which are - * likely to have data or operations tracked about them (IE: results of a common query which - * are likely to have further filtering applied. - * - * Since these results are often captured/reported before a context is escalated, this cache - * doesn't directly hold the resulting cached cases themselves. Rather a CaseObjectCache should - * be used to track the resulting cases. This will ensure that cache can be attached to the - * appropriate lifecycle - * - * Created by ctsims on 1/25/2017. - */ - -public class CaseSetResultCache implements QueryCache { - - private HashMap> bulkFetchBodies = new HashMap<>(); - - public void reportBulkCaseSet(String key, LinkedHashSet ids) { - if (bulkFetchBodies.containsKey(key)) { - return; - } - bulkFetchBodies.put(key, ids); - } - - public boolean hasMatchingCaseSet(int recordId) { - return getCaseSetForRecord(recordId) != null; - } - - public Pair> getCaseSetForRecord(int recordId) { - for (String key : bulkFetchBodies.keySet()) { - LinkedHashSet tranche = bulkFetchBodies.get(key); - if (tranche.contains(recordId)) { - return new Pair<>(key, tranche); - } - } - return null; - } -} diff --git a/src/main/java/org/commcare/modern/engine/cases/RecordObjectCache.java b/src/main/java/org/commcare/modern/engine/cases/RecordObjectCache.java new file mode 100644 index 0000000000..1477d25fcd --- /dev/null +++ b/src/main/java/org/commcare/modern/engine/cases/RecordObjectCache.java @@ -0,0 +1,32 @@ +package org.commcare.modern.engine.cases; + +import org.commcare.cases.model.Case; +import org.commcare.cases.query.QueryCache; + +import java.util.HashMap; + +/** + * A straightforward cache object query cache. Stores objects by their record ID. + * + * Used by other optimizations to isolate doing bulk loads and ensure that they are relevant + * when they occur + * + * Created by ctsims on 6/22/2017. + */ + +public class RecordObjectCache implements QueryCache { + + private HashMap cachedRecordObjects = new HashMap<>(); + + public boolean isLoaded(int recordId) { + return cachedRecordObjects.containsKey(recordId); + } + + public HashMap getLoadedCaseMap() { + return cachedRecordObjects; + } + + public T getLoadedRecordObject(int recordId) { + return cachedRecordObjects.get(recordId); + } +} diff --git a/src/main/java/org/commcare/modern/engine/cases/RecordSetResultCache.java b/src/main/java/org/commcare/modern/engine/cases/RecordSetResultCache.java new file mode 100644 index 0000000000..b1fa3142b6 --- /dev/null +++ b/src/main/java/org/commcare/modern/engine/cases/RecordSetResultCache.java @@ -0,0 +1,56 @@ +package org.commcare.modern.engine.cases; + +import org.commcare.cases.query.QueryCache; +import org.commcare.modern.util.Pair; + +import java.util.HashMap; +import java.util.LinkedHashSet; + +/** + * A record set result cache keeps track of different sets of "Bulk" record which are + * likely to have data or operations tracked about them (IE: results of a common query which + * are likely to have further filtering applied.) + * + * Since these results are often captured/reported before a context is escalated, this cache + * doesn't directly hold the resulting cached records themselves. Rather a RecordObjectCache should + * be used to track the resulting records. This will ensure that cache can be attached to the + * appropriate lifecycle + * + * Created by ctsims on 1/25/2017. + */ + +public class RecordSetResultCache implements QueryCache { + + private HashMap>> bulkFetchBodies = new HashMap<>(); + + /** + * Report a set of bulk records that are likely to be needed as a group. + * + * @param key A unique key for the provided record set. It is presumed that if the key is + * already in use that the id set is redundant. + * @param storageSetID The name of the Storage where the records are stored. + * @param ids The record set ID's + */ + public void reportBulkRecordSet(String key, String storageSetID, LinkedHashSet ids) { + String fullKey = key +"|" + storageSetID; + if (bulkFetchBodies.containsKey(fullKey)) { + return; + } + bulkFetchBodies.put(fullKey, new Pair<>(storageSetID, ids)); + } + + public boolean hasMatchingRecordSet(String recordSetName, int recordId) { + return getRecordSetForRecordId(recordSetName, recordId) != null; + } + + public Pair> getRecordSetForRecordId(String recordSetName, + int recordId) { + for (String key : bulkFetchBodies.keySet()) { + Pair> tranche = bulkFetchBodies.get(key); + if (tranche.second.contains(recordId) && tranche.first.equals(recordSetName)) { + return new Pair<>(key, tranche.second); + } + } + return null; + } +} diff --git a/src/main/java/org/javarosa/core/model/FormDef.java b/src/main/java/org/javarosa/core/model/FormDef.java index 6fbe8aeaa7..c1af573594 100755 --- a/src/main/java/org/javarosa/core/model/FormDef.java +++ b/src/main/java/org/javarosa/core/model/FormDef.java @@ -1,5 +1,6 @@ package org.javarosa.core.model; +import org.commcare.cases.query.QueryContext; import org.commcare.modern.util.Pair; import org.javarosa.core.log.WrappedException; import org.javarosa.core.model.actions.Action; @@ -25,7 +26,9 @@ import org.javarosa.core.model.instance.TreeElement; import org.javarosa.core.model.instance.TreeReference; import org.javarosa.core.model.trace.EvaluationTrace; +import org.javarosa.core.model.trace.ReducingTraceReporter; import org.javarosa.core.model.util.restorable.RestoreUtils; +import org.javarosa.core.model.utils.InstrumentationUtils; import org.javarosa.core.services.locale.Localizer; import org.javarosa.core.services.storage.IMetaData; import org.javarosa.core.util.CacheTable; @@ -133,6 +136,8 @@ public class FormDef implements IFormElement, IMetaData, //If this instance is just being edited, don't fire end of form events private boolean isCompletedInstance; + private boolean mProfilingEnabled = false; + /** * Cache children that trigger target will cascade to. For speeding up * calculations that determine what needs to be triggered when a value @@ -1315,8 +1320,20 @@ public void populateDynamicChoices(ItemsetBinding itemset, TreeReference curQRef formInstance = getMainInstance(); } - Vector matches = itemset.nodesetExpr.evalNodeset(formInstance, - new EvaluationContext(exprEvalContext, itemset.contextRef.contextualize(curQRef))); + EvaluationContext ec = + new EvaluationContext(exprEvalContext, itemset.contextRef.contextualize(curQRef)); + + ReducingTraceReporter reporter= null; + if(mProfilingEnabled) { + reporter = new ReducingTraceReporter(); + ec.setDebugModeOn(reporter); + } + + Vector matches = itemset.nodesetExpr.evalNodeset(formInstance,ec); + + if(reporter != null) { + InstrumentationUtils.printAndClearTraces(reporter, "itemset expansion"); + } if (matches == null) { String instanceName = itemset.nodesetRef.getInstanceName(); @@ -1331,28 +1348,49 @@ public void populateDynamicChoices(ItemsetBinding itemset, TreeReference curQRef } Vector choices = new Vector<>(); + //Escalate the new context if our result set is substantial, this will prevent reverting + //from a bulk read mode to a scanned read mode + QueryContext newContext = ec.getCurrentQueryContext() + .checkForDerivativeContextAndReturn(matches.size()); + ec.setQueryContext(newContext); + for (int i = 0; i < matches.size(); i++) { - choices.addElement(buildSelectChoice(matches.elementAt(i), itemset, formInstance, i)); + choices.addElement(buildSelectChoice(matches.elementAt(i), itemset, formInstance, + ec, reporter, i)); } itemset.setChoices(choices); } private SelectChoice buildSelectChoice(TreeReference choiceRef, ItemsetBinding itemset, - DataInstance formInstance, int index) { + DataInstance formInstance, EvaluationContext ec, + ReducingTraceReporter reporter, int index) { + String label = itemset.labelExpr.evalReadable(formInstance, - new EvaluationContext(exprEvalContext, choiceRef)); + new EvaluationContext(ec, choiceRef)); + + if(reporter != null) { + InstrumentationUtils.printAndClearTraces(reporter, "ItemSet [label] population"); + } + String value = null; TreeElement copyNode = null; + if (itemset.copyMode) { copyNode = this.getMainInstance().resolveReference(itemset.copyRef.contextualize(choiceRef)); } + if (itemset.valueRef != null) { value = itemset.valueExpr.evalReadable(formInstance, - new EvaluationContext(exprEvalContext, choiceRef)); + new EvaluationContext(ec, choiceRef)); + } + + if(reporter != null) { + InstrumentationUtils.printAndClearTraces(reporter, "ItemSet [value] population"); } SelectChoice choice = new SelectChoice(label, value != null ? value : "dynamic:" + index, itemset.labelIsItext); + choice.setIndex(index); if (itemset.copyMode) { @@ -1361,10 +1399,14 @@ private SelectChoice buildSelectChoice(TreeReference choiceRef, ItemsetBinding i if (itemset.sortRef != null) { String evaluatedSortProperty = itemset.sortExpr.evalReadable(formInstance, - new EvaluationContext(exprEvalContext, choiceRef)); + new EvaluationContext(ec, choiceRef)); choice.setSortProperty(evaluatedSortProperty); } + if(reporter != null) { + InstrumentationUtils.printAndClearTraces(reporter, "ItemSet [sort] population"); + } + return choice; } diff --git a/src/main/java/org/javarosa/core/model/utils/InstrumentationUtils.java b/src/main/java/org/javarosa/core/model/utils/InstrumentationUtils.java new file mode 100644 index 0000000000..b75c08ae4c --- /dev/null +++ b/src/main/java/org/javarosa/core/model/utils/InstrumentationUtils.java @@ -0,0 +1,32 @@ +package org.javarosa.core.model.utils; + +import org.javarosa.core.model.trace.EvaluationTrace; +import org.javarosa.core.model.trace.EvaluationTraceReporter; +import org.javarosa.core.model.trace.StringEvaluationTraceSerializer; + +/** + * Utility functions for instrumentation in the engine + * + * Created by ctsims on 7/6/2017. + */ + +public class InstrumentationUtils { + + /** + * Prints out traces (if any exist) from the provided reporter with a description into sysout + */ + public static void printAndClearTraces(EvaluationTraceReporter reporter, String description) { + if (reporter.wereTracesReported()) { + System.out.println(description); + } + + StringEvaluationTraceSerializer serializer = new StringEvaluationTraceSerializer(); + + for (EvaluationTrace trace : reporter.getCollectedTraces()) { + System.out.println(trace.getExpression() + ": " + trace.getValue()); + System.out.print(serializer.serializeEvaluationLevels(trace)); + } + + reporter.reset(); + } +} diff --git a/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java b/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java index fa674a1e92..2a9d8e772f 100644 --- a/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java +++ b/src/main/java/org/javarosa/core/services/storage/IStorageUtilityIndexed.java @@ -3,6 +3,8 @@ import org.javarosa.core.util.InvalidIndexException; import org.javarosa.core.util.externalizable.Externalizable; +import java.util.HashMap; +import java.util.LinkedHashSet; import java.util.NoSuchElementException; import java.util.Vector; @@ -159,4 +161,12 @@ public interface IStorageUtilityIndexed { * contains the value of the index requested. */ E getRecordForValue(String fieldName, Object value) throws NoSuchElementException, InvalidIndexException; -} + + /** + * Load multiple record objects from storage at one time from a list of record ids. + * + * If the provided recordMap already contains entries for any ids, it is _not_ + * required for them to be retrieved from storage again. + */ + void bulkRead(LinkedHashSet ids, HashMap recordMap); +} \ No newline at end of file diff --git a/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java b/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java index a5d75ad0eb..10752bb1ea 100644 --- a/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java +++ b/src/main/java/org/javarosa/core/services/storage/util/DummyIndexedStorageUtility.java @@ -17,7 +17,9 @@ import java.io.DataOutputStream; import java.io.IOException; import java.util.Enumeration; +import java.util.HashMap; import java.util.Hashtable; +import java.util.LinkedHashSet; import java.util.NoSuchElementException; import java.util.Vector; @@ -255,4 +257,11 @@ private void syncMeta() { } } } + + @Override + public void bulkRead(LinkedHashSet cuedCases, HashMap recordMap) { + for(int i : cuedCases) { + recordMap.put(i, data.get(i)); + } + } } diff --git a/src/test/java/org/commcare/fixtures/test/IndexedFixtureTests.java b/src/test/java/org/commcare/fixtures/test/IndexedFixtureTests.java index 802c44c4e4..fdcfd71dec 100644 --- a/src/test/java/org/commcare/fixtures/test/IndexedFixtureTests.java +++ b/src/test/java/org/commcare/fixtures/test/IndexedFixtureTests.java @@ -45,6 +45,18 @@ public void queryIndexedLookup() throws XPathSyntaxException, UnfullfilledRequir assertEquals(4, sandbox.getIndexedFixtureStorage("commtrack:products").getNumRecords()); } + @Test + public void queryLargeBodyLookup() throws XPathSyntaxException, UnfullfilledRequirementsException, + XmlPullParserException, IOException, InvalidStructureException { + ParseUtils.parseIntoSandbox(getClass().getResourceAsStream("/indexed_fixture/large_body.xml"), sandbox); + + EvaluationContext ec = + MockDataUtils.buildContextWithInstance(sandbox, "testfixture", "jr://fixture/testfixture"); + + CaseTestUtils.xpathEvalAndAssert(ec, "count(instance('testfixture')/test/entry[@type = 'a'][value = 1])", 40.0); + } + + @Test(expected = InvalidStructureException.class) public void errorOnSchemaAfterFixtureTest() throws XPathSyntaxException, UnfullfilledRequirementsException, XmlPullParserException, IOException, InvalidStructureException { diff --git a/src/test/java/org/javarosa/core/model/test/FormDefTest.java b/src/test/java/org/javarosa/core/model/test/FormDefTest.java index c947250de0..eaa09dd35f 100644 --- a/src/test/java/org/javarosa/core/model/test/FormDefTest.java +++ b/src/test/java/org/javarosa/core/model/test/FormDefTest.java @@ -614,4 +614,31 @@ public boolean rawArgs() { fec.answerQuestion(new SelectOneData(new Selection("yes"))); } while (fec.stepToNextEvent() != FormEntryController.EVENT_END_OF_FORM); } + + @Test + public void testItemsetPopulationAndFilter() { + FormParseInit fpi = new FormParseInit("/xform_tests/itemset_population_test.xhtml"); + + FormEntryController fec = fpi.getFormEntryController(); + + do { + QuestionDef q = fpi.getCurrentQuestion(); + if (q == null) { + continue; + } + TreeReference currentRef = fec.getModel().getFormIndex().getReference(); + if(currentRef == null) { continue; } + + if(currentRef.genericize().toString().equals("/data/filter")) { + fec.answerQuestion(new SelectOneData(new Selection("a"))); + } + + if(currentRef.genericize().toString().equals("/data/question")) { + assertEquals("Itemset Filter returned the wrong size", + fec.getModel().getQuestionPrompt().getSelectChoices().size(), + 3); + } + + } while (fec.stepToNextEvent() != FormEntryController.EVENT_END_OF_FORM); + } } diff --git a/src/test/java/org/javarosa/core/model/utils/test/LocalizerTest.java b/src/test/java/org/javarosa/core/model/utils/test/LocalizerTest.java index 214d2bf9c5..bb85397bd4 100644 --- a/src/test/java/org/javarosa/core/model/utils/test/LocalizerTest.java +++ b/src/test/java/org/javarosa/core/model/utils/test/LocalizerTest.java @@ -643,7 +643,7 @@ public void testLinearSub() { public void run() { holder[0] = Localizer.processArguments("${0}", new String[]{C}); } - }); + }, "Argument processing: " + C); assertEquals(holder[0], C); @@ -653,7 +653,7 @@ public void run() { public void run() { holder[0] = Localizer.processArguments("${0}", new String[]{D}); } - }); + }, "Argument processing: " + D); assertEquals(holder[0], D); @@ -662,7 +662,7 @@ public void run() { public void run() { holder[0] = Localizer.processArguments(holder[0], res); } - }); + }, "Argument processing: " + res[1] + res[0]); assertEquals(holder[0], res[1] + res[0]); @@ -671,13 +671,13 @@ public void run() { public void run() { holder[0] = Localizer.processArguments("$ {0} ${1}", res); } - }); + }, "Argument processing: " + "$ {0} " + res[1]); assertEquals(holder[0], "$ {0} " + res[1]); } - private void runAsync(Runnable test) { + private void runAsync(Runnable test, String label) { Thread t = new Thread(test); t.start(); try { @@ -687,7 +687,7 @@ private void runAsync(Runnable test) { } if (t.isAlive()) { t.stop(); - throw new RuntimeException("Failed to return from recursive argument processing"); + throw new RuntimeException("Failed to return from recursive argument processing: "+ label); } } diff --git a/src/test/resources/indexed_fixture/large_body.xml b/src/test/resources/indexed_fixture/large_body.xml new file mode 100644 index 0000000000..5f2f43ded1 --- /dev/null +++ b/src/test/resources/indexed_fixture/large_body.xml @@ -0,0 +1,379 @@ + + Successfully restored account test! + + + @type + + + + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + 2 + + + + diff --git a/src/test/resources/xform_tests/itemset_population_test.xhtml b/src/test/resources/xform_tests/itemset_population_test.xhtml new file mode 100644 index 0000000000..2fab7676a0 --- /dev/null +++ b/src/test/resources/xform_tests/itemset_population_test.xhtml @@ -0,0 +1,65 @@ + + + Itemset Test + + + + + + + + + + + + + 1_a + + + + 2_a + + + + 3_a + + + + + 1_b + + + + 2_b + + + + + + + + + + + a + + + + b + + + + + + + + +