/*
* Copyright 2015 Realm Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.realm;
import android.os.SystemClock;
import android.support.test.rule.UiThreadTestRule;
import android.support.test.runner.AndroidJUnit4;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.runner.RunWith;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import io.realm.entities.AllJavaTypes;
import io.realm.entities.AllTypes;
import io.realm.entities.AnnotationIndexTypes;
import io.realm.entities.Dog;
import io.realm.entities.NonLatinFieldNames;
import io.realm.entities.Owner;
import io.realm.internal.async.RealmThreadPoolExecutor;
import io.realm.log.LogLevel;
import io.realm.log.RealmLog;
import io.realm.rule.RunInLooperThread;
import io.realm.rule.RunTestInLooperThread;
import io.realm.rule.TestRealmConfigurationFactory;
import io.realm.util.RealmBackgroundTask;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
@RunWith(AndroidJUnit4.class)
public class RealmAsyncQueryTests {
@Rule
public final RunInLooperThread looperThread = new RunInLooperThread();
@Rule
public final TestRealmConfigurationFactory configFactory = new TestRealmConfigurationFactory();
@Rule
public final UiThreadTestRule uiThreadTestRule = new UiThreadTestRule();
@Rule
public final ExpectedException thrown = ExpectedException.none();
// ****************************
// **** Async transaction ***
// ****************************
// Starts asynchronously a transaction to insert one element.
@Test
@RunTestInLooperThread
public void executeTransactionAsync() throws Throwable {
final Realm realm = looperThread.getRealm();
assertEquals(0, realm.where(Owner.class).count());
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
Owner owner = realm.createObject(Owner.class);
owner.setName("Owner");
}
}, new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
assertEquals(1, realm.where(Owner.class).count());
assertEquals("Owner", realm.where(Owner.class).findFirst().getName());
looperThread.testComplete();
}
}, new Realm.Transaction.OnError() {
@Override
public void onError(Throwable error) {
fail(error.getMessage());
}
});
}
@Test
@RunTestInLooperThread
public void executeTransactionAsync_onSuccess() throws Throwable {
final Realm realm = looperThread.getRealm();
assertEquals(0, realm.where(Owner.class).count());
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
Owner owner = realm.createObject(Owner.class);
owner.setName("Owner");
}
}, new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
assertEquals(1, realm.where(Owner.class).count());
assertEquals("Owner", realm.where(Owner.class).findFirst().getName());
looperThread.testComplete();
}
});
}
@Test
@RunTestInLooperThread
public void executeTransactionAsync_onSuccessCallerRealmClosed() throws Throwable {
final Realm realm = looperThread.getRealm();
assertEquals(0, realm.where(Owner.class).count());
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
Owner owner = realm.createObject(Owner.class);
owner.setName("Owner");
}
}, new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
assertTrue(realm.isClosed());
Realm newRealm = Realm.getInstance(looperThread.getConfiguration());
assertEquals(1, newRealm.where(Owner.class).count());
assertEquals("Owner", newRealm.where(Owner.class).findFirst().getName());
newRealm.close();
looperThread.testComplete();
}
});
realm.close();
}
@Test
@RunTestInLooperThread
public void executeTransactionAsync_onError() throws Throwable {
final Realm realm = looperThread.getRealm();
final RuntimeException runtimeException = new RuntimeException("Oh! What a Terrible Failure");
assertEquals(0, realm.where(Owner.class).count());
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
throw runtimeException;
}
}, new Realm.Transaction.OnError() {
@Override
public void onError(Throwable error) {
assertEquals(0, realm.where(Owner.class).count());
assertNull(realm.where(Owner.class).findFirst());
assertEquals(runtimeException, error);
looperThread.testComplete();
}
});
}
@Test
@RunTestInLooperThread
public void executeTransactionAsync_onErrorCallerRealmClosed() throws Throwable {
final Realm realm = looperThread.getRealm();
final RuntimeException runtimeException = new RuntimeException("Oh! What a Terrible Failure");
assertEquals(0, realm.where(Owner.class).count());
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
throw runtimeException;
}
}, new Realm.Transaction.OnError() {
@Override
public void onError(Throwable error) {
assertTrue(realm.isClosed());
Realm newRealm = Realm.getInstance(looperThread.getConfiguration());
assertEquals(0, newRealm.where(Owner.class).count());
assertNull(newRealm.where(Owner.class).findFirst());
assertEquals(runtimeException, error);
newRealm.close();
looperThread.testComplete();
}
});
realm.close();
}
@Test
@RunTestInLooperThread
public void executeTransactionAsync_NoCallbacks() throws Throwable {
final Realm realm = looperThread.getRealm();
assertEquals(0, realm.where(Owner.class).count());
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
Owner owner = realm.createObject(Owner.class);
owner.setName("Owner");
}
});
realm.addChangeListener(new RealmChangeListener<Realm>() {
@Override
public void onChange(Realm object) {
assertEquals("Owner", realm.where(Owner.class).findFirst().getName());
looperThread.testComplete();
}
});
}
// Tests that an async transaction that throws when call cancelTransaction manually.
@Test
@RunTestInLooperThread
public void executeTransactionAsync_cancelTransactionInside() throws Throwable {
final TestHelper.TestLogger testLogger = new TestHelper.TestLogger(LogLevel.DEBUG);
RealmLog.add(testLogger);
final Realm realm = looperThread.getRealm();
assertEquals(0, realm.where(Owner.class).count());
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
Owner owner = realm.createObject(Owner.class);
owner.setName("Owner");
realm.cancelTransaction();
}
}, new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
fail("Should not reach success if runtime exception is thrown in callback.");
}
}, new Realm.Transaction.OnError() {
@Override
public void onError(Throwable error) {
// Ensure we are giving developers quality messages in the logs.
assertTrue(testLogger.message.contains(
"Exception has been thrown: Can't commit a non-existing write transaction"));
assertTrue(error instanceof IllegalStateException);
RealmLog.remove(testLogger);
looperThread.testComplete();
}
});
}
// Tests if the background Realm is closed when transaction success returned.
@Test
@RunTestInLooperThread
public void executeTransactionAsync_realmClosedOnSuccess() {
final AtomicInteger counter = new AtomicInteger(100);
final Realm realm = looperThread.getRealm();
final RealmCache.Callback cacheCallback = new RealmCache.Callback() {
@Override
public void onResult(int count) {
assertEquals(1, count);
if (counter.decrementAndGet() == 0) {
realm.close();
looperThread.testComplete();
}
}
};
final Realm.Transaction.OnSuccess transactionCallback = new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
RealmCache.invokeWithGlobalRefCount(realm.getConfiguration(), cacheCallback);
if (counter.get() == 0) {
// Finishes testing.
return;
}
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
}
}, this);
}
};
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
}
}, transactionCallback);
}
// Tests if the background Realm is closed when transaction error returned.
@Test
@RunTestInLooperThread
public void executeTransaction_async_realmClosedOnError() {
final AtomicInteger counter = new AtomicInteger(100);
final Realm realm = looperThread.getRealm();
final RealmCache.Callback cacheCallback = new RealmCache.Callback() {
@Override
public void onResult(int count) {
assertEquals(1, count);
if (counter.decrementAndGet() == 0) {
realm.close();
looperThread.testComplete();
}
}
};
final Realm.Transaction.OnError transactionCallback = new Realm.Transaction.OnError() {
@Override
public void onError(Throwable error) {
RealmCache.invokeWithGlobalRefCount(realm.getConfiguration(), cacheCallback);
if (counter.get() == 0) {
// Finishes testing.
return;
}
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
throw new RuntimeException("Dummy exception");
}
}, this);
}
};
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
throw new RuntimeException("Dummy exception");
}
}, transactionCallback);
}
// Test case for https://github.com/realm/realm-java/issues/1893
// Ensures that onSuccess is called with the correct Realm version for async transaction.
@Test
@RunTestInLooperThread
public void executeTransactionAsync_asyncQuery() {
final Realm realm = looperThread.getRealm();
final RealmResults<AllTypes> results = realm.where(AllTypes.class).findAllAsync();
assertEquals(0, results.size());
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.createObject(AllTypes.class);
}
}, new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
assertEquals(1, realm.where(AllTypes.class).count());
// We cannot guarantee the async results get delivered from OS.
if (results.isLoaded()) {
assertEquals(1, results.size());
} else {
assertEquals(0, results.size());
}
looperThread.testComplete();
}
}, new Realm.Transaction.OnError() {
@Override
public void onError(Throwable error) {
fail();
}
});
}
@Test
public void executeTransactionAsync_onSuccessOnNonLooperThreadThrows() {
Realm realm = Realm.getInstance(configFactory.createConfiguration());
thrown.expect(IllegalStateException.class);
try {
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
}
}, new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
}
});
} finally {
realm.close();
}
}
@Test
public void executeTransactionAsync_onErrorOnNonLooperThreadThrows() {
Realm realm = Realm.getInstance(configFactory.createConfiguration());
thrown.expect(IllegalStateException.class);
try {
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
}
}, new Realm.Transaction.OnError() {
@Override
public void onError(Throwable error) {
}
});
} finally {
realm.close();
}
}
// https://github.com/realm/realm-java/issues/4595#issuecomment-298830411
// onSuccess might commit another transaction which will call didChange. So before calling async transaction
// callbacks, the callback should be cleared.
@Test
@RunTestInLooperThread
public void executeTransactionAsync_callbacksShouldBeClearedBeforeCalling()
throws NoSuchFieldException, IllegalAccessException {
final AtomicInteger callbackCounter = new AtomicInteger(0);
final Realm foregroundRealm = looperThread.getRealm();
// Use single thread executor
TestHelper.replaceRealmThreadExecutor(RealmThreadPoolExecutor.newSingleThreadExecutor());
// To reproduce the issue, the posted callback needs to arrived before the Object Store did_change called.
// We just disable the auto refresh here then the did_change won't be called.
foregroundRealm.setAutoRefresh(false);
foregroundRealm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.createObject(AllTypes.class);
}
}, new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
// This will be called first and only once
assertEquals(0, callbackCounter.getAndIncrement());
// This transaction should never trigger the onSuccess.
foregroundRealm.beginTransaction();
foregroundRealm.createObject(AllTypes.class);
foregroundRealm.commitTransaction();
}
});
foregroundRealm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.createObject(AllTypes.class);
}
}, new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
// This will be called 2nd and only once
assertEquals(1, callbackCounter.getAndIncrement());
looperThread.testComplete();
}
});
// Wait for all async tasks finish to ensure the async transaction posted callback will arrive first.
TestHelper.resetRealmThreadExecutor();
looperThread.postRunnable(new Runnable() {
@Override
public void run() {
// Manually call refresh, so the did_change will be triggered.
foregroundRealm.sharedRealm.refresh();
foregroundRealm.setAutoRefresh(true);
}
});
}
// ************************************
// *** promises based async queries ***
// ************************************
// Finds element [0-4] asynchronously then waits for the promise to be loaded.
@Test
@RunTestInLooperThread
public void findAllAsync() throws Throwable {
final Realm realm = looperThread.getRealm();
populateTestRealm(realm, 10);
final RealmResults<AllTypes> results = realm.where(AllTypes.class)
.between("columnLong", 0, 4)
.findAllAsync();
assertFalse(results.isLoaded());
assertEquals(0, results.size());
looperThread.keepStrongReference(results);
results.addChangeListener(new RealmChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> object) {
assertTrue(results.isLoaded());
assertEquals(5, results.size());
assertTrue(results.get(0).isValid());
looperThread.testComplete();
}
});
}
@Test
@RunTestInLooperThread
public void accessingRealmListOnUnloadedRealmObjectShouldThrow() {
Realm realm = looperThread.getRealm();
populateTestRealm(realm, 10);
final AllTypes results = realm.where(AllTypes.class)
.equalTo("columnLong", 0)
.findFirstAsync();
assertFalse(results.isLoaded());
try {
results.getColumnRealmList();
fail("Accessing property on an empty row");
} catch (IllegalStateException ignored) {
}
looperThread.testComplete();
}
@Test
public void unmanagedObjectAsyncBehaviour() {
Dog dog = new Dog();
dog.setName("Akamaru");
dog.setAge(10);
assertTrue(dog.isLoaded());
assertTrue(dog.isValid());
assertFalse(dog.isManaged());
}
@Test
public void findAllAsync_throwsOnNonLooperThread() throws Throwable {
Realm realm = Realm.getInstance(configFactory.createConfiguration());
try {
realm.where(AllTypes.class).findAllAsync();
fail();
} catch (IllegalStateException ignored) {
} finally {
realm.close();
}
}
// finding elements [0-4] asynchronously then wait for the promise to be loaded
// using a callback to be notified when the data is loaded
@Test
@RunTestInLooperThread
public void findAllAsync_withNotification() throws Throwable {
Realm realm = looperThread.getRealm();
populateTestRealm(realm, 10);
final RealmResults<AllTypes> results = realm.where(AllTypes.class)
.between("columnLong", 0, 4)
.findAllAsync();
results.addChangeListener(new RealmChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> object) {
assertTrue(results.isLoaded());
assertEquals(5, results.size());
assertTrue(results.get(4).isValid());
looperThread.testComplete();
}
});
looperThread.keepStrongReference(results);
assertFalse(results.isLoaded());
assertEquals(0, results.size());
}
// Transforms an async query into sync by calling load to force
// the blocking behaviour.
@Test
@RunTestInLooperThread
public void findAllAsync_forceLoad() throws Throwable {
Realm realm = looperThread.getRealm();
populateTestRealm(realm, 10);
final RealmResults<AllTypes> realmResults = realm.where(AllTypes.class)
.between("columnLong", 0, 4)
.findAllAsync();
looperThread.keepStrongReference(realmResults);
// Notification should be called as well.
realmResults.addChangeListener(new RealmChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> object) {
assertTrue(realmResults.isLoaded());
assertEquals(5, realmResults.size());
looperThread.testComplete();
}
});
assertFalse(realmResults.isLoaded());
assertEquals(0, realmResults.size());
boolean successful = realmResults.load();
assertTrue(successful);
assertTrue(realmResults.isLoaded());
assertEquals(5, realmResults.size());
}
// **********************************
// *** 'findFirst' async queries ***
// **********************************
// Similar UC as #testFindAllAsync using 'findFirst'.
@Test
@RunTestInLooperThread
public void findFirstAsync() {
Realm realm = looperThread.getRealm();
populateTestRealm(realm, 10);
final AllTypes asyncObj = realm.where(AllTypes.class).findFirstAsync();
assertFalse(asyncObj.isValid());
assertFalse(asyncObj.isLoaded());
looperThread.keepStrongReference(asyncObj);
asyncObj.addChangeListener(new RealmChangeListener<AllTypes>() {
@Override
public void onChange(AllTypes object) {
assertTrue(asyncObj.isLoaded());
assertTrue(asyncObj.isValid());
looperThread.testComplete();
}
});
}
// When there is no object match the query condition, findFirstAsync should return with an invalid row.
@Test
@RunTestInLooperThread
public void findFirstAsync_initialEmptyRow() throws Throwable {
Realm realm = looperThread.getRealm();
final AllTypes firstAsync = realm.where(AllTypes.class).findFirstAsync();
looperThread.keepStrongReference(firstAsync);
firstAsync.addChangeListener(new RealmChangeListener<AllTypes>() {
@Override
public void onChange(AllTypes object) {
assertTrue(firstAsync.isLoaded());
assertFalse(firstAsync.isValid());
looperThread.testComplete();
}
});
}
@Test
@RunTestInLooperThread
public void findFirstAsync_updatedIfSyncRealmObjectIsUpdated() throws Throwable {
populateTestRealm(looperThread.getRealm(), 1);
AllTypes firstSync = looperThread.getRealm().where(AllTypes.class).findFirst();
assertEquals(0, firstSync.getColumnLong());
assertEquals("test data 0", firstSync.getColumnString());
final AllTypes firstAsync = looperThread.getRealm().where(AllTypes.class).findFirstAsync();
assertTrue(firstAsync.load());
assertTrue(firstAsync.isLoaded());
assertTrue(firstAsync.isValid());
assertEquals(0, firstAsync.getColumnLong());
assertEquals("test data 0", firstAsync.getColumnString());
looperThread.keepStrongReference(firstAsync);
firstAsync.addChangeListener(new RealmChangeListener<AllTypes>() {
@Override
public void onChange(AllTypes object) {
assertEquals("Galacticon", firstAsync.getColumnString());
looperThread.testComplete();
}
});
looperThread.getRealm().beginTransaction();
firstSync.setColumnString("Galacticon");
looperThread.getRealm().commitTransaction();
}
// Finds elements [0-4] asynchronously then waits for the promise to be loaded
// using a callback to be notified when the data is loaded.
@Test
@RunTestInLooperThread
public void findFirstAsync_withNotification() throws Throwable {
Realm realm = looperThread.getRealm();
populateTestRealm(realm, 10);
final AllTypes realmResults = realm.where(AllTypes.class)
.between("columnLong", 4, 9)
.findFirstAsync();
looperThread.keepStrongReference(realmResults);
realmResults.addChangeListener(new RealmChangeListener<AllTypes>() {
@Override
public void onChange(AllTypes object) {
assertTrue(realmResults.isLoaded());
assertTrue(realmResults.isValid());
assertEquals("test data 4", realmResults.getColumnString());
looperThread.testComplete();
}
});
assertFalse(realmResults.isLoaded());
assertFalse(realmResults.isValid());
try {
realmResults.setColumnString("should fail");
fail("Accessing an unloaded object should throw");
} catch (IllegalStateException ignored) {
}
}
// load should trigger the listener with empty change set.
@Test
@RunTestInLooperThread
public void findFirstAsync_forceLoad() throws Throwable {
final AtomicBoolean listenerCalled = new AtomicBoolean(false);
Realm realm = looperThread.getRealm();
populateTestRealm(realm, 10);
final AllTypes realmResults = realm.where(AllTypes.class)
.between("columnLong", 4, 9)
.findFirstAsync();
assertFalse(realmResults.isLoaded());
realmResults.addChangeListener(new RealmObjectChangeListener<RealmModel>() {
@Override
public void onChange(RealmModel object, ObjectChangeSet changeSet) {
assertNull(changeSet);
assertFalse(listenerCalled.get());
listenerCalled.set(true);
}
});
assertTrue(realmResults.load());
assertTrue(realmResults.isLoaded());
assertEquals("test data 4", realmResults.getColumnString());
assertTrue(listenerCalled.get());
looperThread.testComplete();
}
// For issue https://github.com/realm/realm-java/issues/4495
@Test
@RunTestInLooperThread
public void findFirstAsync_twoListenersOnSameInvalidObjectsCauseNPE() {
final Realm realm = looperThread.getRealm();
final AllTypes allTypes = realm.where(AllTypes.class).findFirstAsync();
final AtomicBoolean firstListenerCalled = new AtomicBoolean(false);
allTypes.addChangeListener(new RealmChangeListener<AllTypes>() {
@Override
public void onChange(AllTypes element) {
allTypes.removeChangeListener(this);
assertFalse(firstListenerCalled.getAndSet(true));
if (!element.isValid()) {
realm.beginTransaction();
realm.createObject(AllTypes.class);
realm.commitTransaction();
}
}
});
allTypes.addChangeListener(new RealmChangeListener<AllTypes>() {
@Override
public void onChange(AllTypes element) {
allTypes.removeChangeListener(this);
assertTrue(firstListenerCalled.get());
assertFalse(element.isValid());
looperThread.testComplete();
}
});
}
// **************************************
// *** 'findAllSorted' async queries ***
// **************************************
// similar UC as #testFindAllAsync using 'findAllSorted'
@Test
@RunTestInLooperThread
public void findAllSortedAsync() throws Throwable {
final Realm realm = looperThread.getRealm();
populateTestRealm(realm, 10);
final RealmResults<AllTypes> results = realm.where(AllTypes.class)
.between("columnLong", 0, 4)
.findAllSortedAsync("columnString", Sort.DESCENDING);
assertFalse(results.isLoaded());
assertEquals(0, results.size());
looperThread.keepStrongReference(results);
results.addChangeListener(new RealmChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> object) {
assertTrue(results.isLoaded());
assertEquals(5, results.size());
for (int i = 0; i < 5; i++) {
int iteration = (4 - i);
assertEquals("test data " + iteration, results.get(4 - iteration).getColumnString());
}
looperThread.testComplete();
}
});
}
@Test
@RunTestInLooperThread
public void combiningAsyncAndSync() {
populateTestRealm(looperThread.getRealm(), 10);
final RealmResults<AllTypes> allTypesAsync = looperThread.getRealm().where(AllTypes.class).greaterThan("columnLong", 5).findAllAsync();
final RealmResults<AllTypes> allTypesSync = allTypesAsync.where().greaterThan("columnLong", 3).findAll();
// Call where() on an async results will load query. But to maintain the pre version 2.4.0 behaviour of
// RealmResults.load(), we still treat it as a not loaded results.
assertEquals(0, allTypesAsync.size());
assertEquals(4, allTypesSync.size()); // columnLong > 5 && columnLong > 3
allTypesAsync.addChangeListener(new RealmChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> object) {
assertEquals(4, allTypesAsync.size());
assertEquals(4, allTypesSync.size());
looperThread.testComplete();
}
});
looperThread.keepStrongReference(allTypesAsync);
}
// Keeps advancing the Realm by sending 1 commit for each frame (16ms).
// The async queries should keep up with the modification.
@Test
@RunTestInLooperThread
public void stressTestBackgroundCommits() throws Throwable {
final int NUMBER_OF_COMMITS = 100;
final CountDownLatch bgRealmClosed = new CountDownLatch(1);
final long[] latestLongValue = new long[1];
final float[] latestFloatValue = new float[1];
// Starts a background thread that pushes a commit every 16ms.
final Thread backgroundThread = new Thread() {
@Override
public void run() {
Random random = new Random(System.currentTimeMillis());
Realm backgroundThreadRealm = Realm.getInstance(looperThread.getRealm().getConfiguration());
for (int i = 0; i < NUMBER_OF_COMMITS; i++) {
backgroundThreadRealm.beginTransaction();
AllTypes object = backgroundThreadRealm.createObject(AllTypes.class);
latestLongValue[0] = random.nextInt(100);
latestFloatValue[0] = random.nextFloat();
object.setColumnFloat(latestFloatValue[0]);
object.setColumnLong(latestLongValue[0]);
backgroundThreadRealm.commitTransaction();
// Waits 16ms. Before adding the next commit.
SystemClock.sleep(16);
}
backgroundThreadRealm.close();
bgRealmClosed.countDown();
}
};
final RealmResults<AllTypes> allAsync = looperThread.getRealm().where(AllTypes.class).findAllAsync();
allAsync.addChangeListener(new RealmChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> object) {
assertTrue(allAsync.isLoaded());
if (allAsync.size() == NUMBER_OF_COMMITS) {
AllTypes lastInserted = looperThread.getRealm().where(AllTypes.class)
.equalTo("columnLong", latestLongValue[0])
.equalTo("columnFloat", latestFloatValue[0])
.findFirst();
assertNotNull(lastInserted);
TestHelper.awaitOrFail(bgRealmClosed);
looperThread.testComplete();
}
}
});
looperThread.keepStrongReference(allAsync);
looperThread.postRunnableDelayed(new Runnable() {
@Override
public void run() {
backgroundThread.start();
}
}, 16);
}
@Test
@RunTestInLooperThread
public void distinctAsync() throws Throwable {
Realm realm = looperThread.getRealm();
final long numberOfBlocks = 25;
final long numberOfObjects = 10; // Must be greater than 1
populateForDistinct(realm, numberOfBlocks, numberOfObjects, false);
final RealmResults<AnnotationIndexTypes> distinctBool = realm.where(AnnotationIndexTypes.class).distinctAsync("indexBoolean");
final RealmResults<AnnotationIndexTypes> distinctLong = realm.where(AnnotationIndexTypes.class).distinctAsync("indexLong");
final RealmResults<AnnotationIndexTypes> distinctDate = realm.where(AnnotationIndexTypes.class).distinctAsync("indexDate");
final RealmResults<AnnotationIndexTypes> distinctString = realm.where(AnnotationIndexTypes.class).distinctAsync("indexString");
assertFalse(distinctBool.isLoaded());
assertTrue(distinctBool.isValid());
assertTrue(distinctBool.isEmpty());
assertFalse(distinctLong.isLoaded());
assertTrue(distinctLong.isValid());
assertTrue(distinctLong.isEmpty());
assertFalse(distinctDate.isLoaded());
assertTrue(distinctDate.isValid());
assertTrue(distinctDate.isEmpty());
assertFalse(distinctString.isLoaded());
assertTrue(distinctString.isValid());
assertTrue(distinctString.isEmpty());
final Runnable changeListenerDone = new Runnable() {
final AtomicInteger signalCallbackFinished = new AtomicInteger(4);
@Override
public void run() {
if (signalCallbackFinished.decrementAndGet() == 0) {
looperThread.testComplete();
}
}
};
looperThread.keepStrongReference(distinctBool);
looperThread.keepStrongReference(distinctLong);
looperThread.keepStrongReference(distinctDate);
looperThread.keepStrongReference(distinctString);
distinctBool.addChangeListener(new RealmChangeListener<RealmResults<AnnotationIndexTypes>>() {
@Override
public void onChange(RealmResults<AnnotationIndexTypes> object) {
assertEquals(2, distinctBool.size());
changeListenerDone.run();
}
});
distinctLong.addChangeListener(new RealmChangeListener<RealmResults<AnnotationIndexTypes>>() {
@Override
public void onChange(RealmResults<AnnotationIndexTypes> object) {
assertEquals(numberOfBlocks, distinctLong.size());
changeListenerDone.run();
}
});
distinctDate.addChangeListener(new RealmChangeListener<RealmResults<AnnotationIndexTypes>>() {
@Override
public void onChange(RealmResults<AnnotationIndexTypes> object) {
assertEquals(numberOfBlocks, distinctDate.size());
changeListenerDone.run();
}
});
distinctString.addChangeListener(new RealmChangeListener<RealmResults<AnnotationIndexTypes>>() {
@Override
public void onChange(RealmResults<AnnotationIndexTypes> object) {
assertEquals(numberOfBlocks, distinctString.size());
changeListenerDone.run();
}
});
}
@Test
@RunTestInLooperThread()
public void distinctAsync_rememberQueryParams() {
final Realm realm = looperThread.getRealm();
realm.beginTransaction();
final int TEST_SIZE = 10;
for (int i = 0; i < TEST_SIZE; i++) {
realm.createObject(AllJavaTypes.class, i);
}
realm.commitTransaction();
RealmResults<AllJavaTypes> results = realm.where(AllJavaTypes.class)
.notEqualTo(AllJavaTypes.FIELD_ID, TEST_SIZE / 2)
.distinctAsync(AllJavaTypes.FIELD_ID);
results.addChangeListener(new RealmChangeListener<RealmResults<AllJavaTypes>>() {
@Override
public void onChange(RealmResults<AllJavaTypes> results) {
assertEquals(TEST_SIZE - 1, results.size());
assertEquals(0, results.where().equalTo(AllJavaTypes.FIELD_ID, TEST_SIZE / 2).count());
looperThread.testComplete();
}
});
}
@Test
@RunTestInLooperThread
public void distinctAsync_notIndexedFields() throws Throwable {
Realm realm = looperThread.getRealm();
final long numberOfBlocks = 25;
final long numberOfObjects = 10; // Must be greater than 1
populateForDistinct(realm, numberOfBlocks, numberOfObjects, false);
final RealmResults<AnnotationIndexTypes> distinctBool = realm.where(AnnotationIndexTypes.class)
.distinctAsync(AnnotationIndexTypes.FIELD_NOT_INDEX_BOOL);
final RealmResults<AnnotationIndexTypes> distinctLong = realm.where(AnnotationIndexTypes.class)
.distinctAsync(AnnotationIndexTypes.FIELD_NOT_INDEX_LONG);
final RealmResults<AnnotationIndexTypes> distinctDate = realm.where(AnnotationIndexTypes.class)
.distinctAsync(AnnotationIndexTypes.FIELD_NOT_INDEX_DATE);
final RealmResults<AnnotationIndexTypes> distinctString = realm.where(AnnotationIndexTypes.class)
.distinctAsync(AnnotationIndexTypes.FIELD_INDEX_STRING);
assertFalse(distinctBool.isLoaded());
assertTrue(distinctBool.isValid());
assertTrue(distinctBool.isEmpty());
assertFalse(distinctLong.isLoaded());
assertTrue(distinctLong.isValid());
assertTrue(distinctLong.isEmpty());
assertFalse(distinctDate.isLoaded());
assertTrue(distinctDate.isValid());
assertTrue(distinctDate.isEmpty());
assertFalse(distinctString.isLoaded());
assertTrue(distinctString.isValid());
assertTrue(distinctString.isEmpty());
final Runnable changeListenerDone = new Runnable() {
final AtomicInteger signalCallbackFinished = new AtomicInteger(4);
@Override
public void run() {
if (signalCallbackFinished.decrementAndGet() == 0) {
looperThread.testComplete();
}
}
};
looperThread.keepStrongReference(distinctBool);
looperThread.keepStrongReference(distinctLong);
looperThread.keepStrongReference(distinctDate);
looperThread.keepStrongReference(distinctString);
distinctBool.addChangeListener(new RealmChangeListener<RealmResults<AnnotationIndexTypes>>() {
@Override
public void onChange(RealmResults<AnnotationIndexTypes> object) {
assertEquals(2, distinctBool.size());
changeListenerDone.run();
}
});
distinctLong.addChangeListener(new RealmChangeListener<RealmResults<AnnotationIndexTypes>>() {
@Override
public void onChange(RealmResults<AnnotationIndexTypes> object) {
assertEquals(numberOfBlocks, distinctLong.size());
changeListenerDone.run();
}
});
distinctDate.addChangeListener(new RealmChangeListener<RealmResults<AnnotationIndexTypes>>() {
@Override
public void onChange(RealmResults<AnnotationIndexTypes> object) {
assertEquals(numberOfBlocks, distinctDate.size());
changeListenerDone.run();
}
});
distinctString.addChangeListener(new RealmChangeListener<RealmResults<AnnotationIndexTypes>>() {
@Override
public void onChange(RealmResults<AnnotationIndexTypes> object) {
assertEquals(numberOfBlocks, distinctString.size());
changeListenerDone.run();
}
});
}
@Test
@RunTestInLooperThread
public void distinctAsync_noneExistingField() throws Throwable {
Realm realm = looperThread.getRealm();
final long numberOfBlocks = 25;
final long numberOfObjects = 10; // Must be greater than 1
populateForDistinct(realm, numberOfBlocks, numberOfObjects, false);
try {
realm.where(AnnotationIndexTypes.class).distinctAsync("doesNotExist");
fail();
} catch (IllegalArgumentException ignored) {
looperThread.testComplete();
}
}
@Test
@RunTestInLooperThread
public void batchUpdateDifferentTypeOfQueries() {
final Realm realm = looperThread.getRealm();
realm.beginTransaction();
for (int i = 0; i < 5; ) {
AllTypes allTypes = realm.createObject(AllTypes.class);
allTypes.setColumnLong(i);
allTypes.setColumnString("data " + i % 3);
allTypes = realm.createObject(AllTypes.class);
allTypes.setColumnLong(i);
allTypes.setColumnString("data " + (++i % 3));
}
final long numberOfBlocks = 25;
final long numberOfObjects = 10; // Must be greater than 1
realm.commitTransaction();
populateForDistinct(realm, numberOfBlocks, numberOfObjects, false);
RealmResults<AllTypes> findAllAsync = realm.where(AllTypes.class).findAllAsync();
RealmResults<AllTypes> findAllSorted = realm.where(AllTypes.class).findAllSortedAsync("columnString", Sort.ASCENDING);
RealmResults<AllTypes> findAllSortedMulti = realm.where(AllTypes.class).findAllSortedAsync(new String[]{"columnString", "columnLong"},
new Sort[]{Sort.ASCENDING, Sort.DESCENDING});
RealmResults<AnnotationIndexTypes> findDistinct = realm.where(AnnotationIndexTypes.class).distinctAsync("indexString");
looperThread.keepStrongReference(findAllAsync);
looperThread.keepStrongReference(findAllSorted);
looperThread.keepStrongReference(findAllSortedMulti);
looperThread.keepStrongReference(findDistinct);
final CountDownLatch queriesCompleted = new CountDownLatch(4);
final CountDownLatch bgRealmClosedLatch = new CountDownLatch(1);
final AtomicInteger batchUpdateCompleted = new AtomicInteger(0);
final AtomicInteger findAllAsyncInvocation = new AtomicInteger(0);
final AtomicInteger findAllSortedInvocation = new AtomicInteger(0);
final AtomicInteger findAllSortedMultiInvocation = new AtomicInteger(0);
final AtomicInteger findDistinctInvocation = new AtomicInteger(0);
findAllAsync.addChangeListener(new RealmChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> object) {
switch (findAllAsyncInvocation.incrementAndGet()) {
case 1: {
queriesCompleted.countDown();
break;
}
case 2: {
if (batchUpdateCompleted.incrementAndGet() == 4) {
looperThread.testComplete(bgRealmClosedLatch);
}
break;
}
}
}
});
findAllSorted.addChangeListener(new RealmChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> object) {
switch (findAllSortedInvocation.incrementAndGet()) {
case 1: {
queriesCompleted.countDown();
break;
}
case 2: {
if (batchUpdateCompleted.incrementAndGet() == 4) {
looperThread.testComplete(bgRealmClosedLatch);
}
break;
}
}
}
});
findAllSortedMulti.addChangeListener(new RealmChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> object) {
switch (findAllSortedMultiInvocation.incrementAndGet()) {
case 1: {
queriesCompleted.countDown();
break;
}
case 2: {
if (batchUpdateCompleted.incrementAndGet() == 4) {
looperThread.testComplete(bgRealmClosedLatch);
}
break;
}
}
}
});
findDistinct.addChangeListener(new RealmChangeListener<RealmResults<AnnotationIndexTypes>>() {
@Override
public void onChange(RealmResults<AnnotationIndexTypes> object) {
switch (findDistinctInvocation.incrementAndGet()) {
case 1: {
queriesCompleted.countDown();
break;
}
case 2: {
if (batchUpdateCompleted.incrementAndGet() == 4) {
looperThread.testComplete(bgRealmClosedLatch);
}
break;
}
}
}
});
// Waits for the queries to complete then sends a commit from
// another thread to trigger a batch update of the 4 queries.
new Thread() {
@Override
public void run() {
TestHelper.awaitOrFail(queriesCompleted);
Realm bgRealm = Realm.getInstance(realm.getConfiguration());
bgRealm.beginTransaction();
bgRealm.createObject(AllTypes.class);
bgRealm.createObject(AnnotationIndexTypes.class);
bgRealm.commitTransaction();
bgRealm.close();
bgRealmClosedLatch.countDown();
}
}.start();
}
// This test makes sure that Async queries update when using link.
@Test
@RunTestInLooperThread
public void queryingLinkHandover() throws Throwable {
final AtomicInteger numberOfInvocations = new AtomicInteger(0);
final Realm realm = looperThread.getRealm();
final RealmResults<Dog> allAsync = realm.where(Dog.class).equalTo("owner.name", "kiba").findAllAsync();
looperThread.keepStrongReference(allAsync);
allAsync.addChangeListener(new RealmChangeListener<RealmResults<Dog>>() {
@Override
public void onChange(RealmResults<Dog> object) {
switch (numberOfInvocations.incrementAndGet()) {
case 1:
assertEquals(0, allAsync.size());
assertTrue(allAsync.isLoaded());
assertTrue(allAsync.isValid());
assertTrue(allAsync.isEmpty());
new RealmBackgroundTask(realm.getConfiguration()) {
@Override
public void doInBackground(Realm realm) {
realm.beginTransaction();
Dog dog = realm.createObject(Dog.class);
dog.setAge(10);
dog.setName("Akamaru");
Owner kiba = realm.createObject(Owner.class);
kiba.setName("kiba");
dog.setOwner(kiba);
realm.commitTransaction();
}
}.awaitOrFail();
break;
case 2:
assertEquals(1, realm.where(Dog.class).count());
assertEquals(1, realm.where(Owner.class).count());
assertEquals(1, allAsync.size());
assertTrue(allAsync.isLoaded());
assertTrue(allAsync.isValid());
assertFalse(allAsync.isEmpty());
assertEquals(1, allAsync.size());
assertEquals("Akamaru", allAsync.get(0).getName());
assertEquals("kiba", allAsync.get(0).getOwner().getName());
looperThread.testComplete();
break;
}
}
});
}
// Test case for https://github.com/realm/realm-java/issues/2417
// Ensures that a UnreachableVersion exception during handover doesn't crash the app or cause a segfault.
// NOTE: This test is not checking the same thing after the OS results integration. Just keep it for an additional
// test for async.
@Test
@RunTestInLooperThread
public void badVersion_syncTransaction() throws NoSuchFieldException, IllegalAccessException {
final AtomicInteger listenerCount = new AtomicInteger(0);
Realm realm = looperThread.getRealm();
// 1. Makes sure that async query is not started.
final RealmResults<AllTypes> result = realm.where(AllTypes.class).findAllSortedAsync(AllTypes.FIELD_STRING);
looperThread.keepStrongReference(result);
result.addChangeListener(new RealmChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> object) {
assertTrue(result.isValid());
assertTrue(result.isLoaded());
switch (listenerCount.getAndIncrement()) {
case 0:
// Triggered by beginTransaction
assertEquals(0, result.size());
break;
case 1:
// 4. The commit in #2, should result in a refresh being triggered, which means this callback will
// be notified once the updated async queries has run.
assertEquals(1, result.size());
looperThread.testComplete();
break;
default:
fail();
break;
}
}
});
// 2. Advances the caller Realm, invalidating the version in the handover object.
realm.beginTransaction();
assertTrue(result.isLoaded());
realm.createObject(AllTypes.class);
realm.commitTransaction();
// 3. The async query should now (hopefully) fail with a BadVersion.
// NOTE: Step 3 is from the original test. After integration of Object Store Results, it has been loaded already
// when beginTransaction.
result.load();
}
// This test reproduces the issue in https://secure.helpscout.net/conversation/244053233/6163/?folderId=366141
// First it creates 512 async queries, then trigger a transaction to make the queries gets update with
// nativeBatchUpdateQueries. It should not exceed the limits of local ref map size in JNI.
// NOTE: This test is not checking the same thing after the OS results integration. Just keep it for an additional
// test for async.
@Test
@RunTestInLooperThread
public void batchUpdate_localRefIsDeletedInLoopOfNativeBatchUpdateQueries() {
final Realm realm = looperThread.getRealm();
// For Android, the size of local ref map is 512. Uses 1024 for more pressure.
final int TEST_COUNT = 1024;
final AtomicBoolean updatesTriggered = new AtomicBoolean(false);
// The first time onChange gets called for every results.
final AtomicInteger firstOnChangeCounter = new AtomicInteger(0);
// The second time onChange gets called for every results which is triggered by the transaction.
final AtomicInteger secondOnChangeCounter = new AtomicInteger(0);
final RealmChangeListener<RealmResults<AllTypes>> listener = new RealmChangeListener<RealmResults<AllTypes>>() {
@Override
public void onChange(RealmResults<AllTypes> element) {
if (updatesTriggered.get()) {
// Step 4: Test finished after all results's onChange gets called the 2nd time.
int count = secondOnChangeCounter.addAndGet(1);
if (count == TEST_COUNT) {
realm.removeAllChangeListeners();
looperThread.testComplete();
}
} else {
int count = firstOnChangeCounter.addAndGet(1);
if (count == TEST_COUNT) {
// Step 3: Commits the transaction to trigger queries updates.
updatesTriggered.set(true);
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.createObject(AllTypes.class);
}
});
} else {
// Step 2: Creates 2nd - TEST_COUNT queries.
RealmResults<AllTypes> results = realm.where(AllTypes.class).findAllAsync();
results.addChangeListener(this);
looperThread.keepStrongReference(results);
}
}
}
};
// Step 1. Creates first async to kick the test start.
RealmResults<AllTypes> results = realm.where(AllTypes.class).findAllAsync();
results.addChangeListener(listener);
looperThread.keepStrongReference(results);
}
// *** Helper methods ***
private void populateTestRealm(final Realm testRealm, int objects) {
testRealm.setAutoRefresh(false);
testRealm.beginTransaction();
testRealm.deleteAll();
for (int i = 0; i < objects; ++i) {
AllTypes allTypes = testRealm.createObject(AllTypes.class);
allTypes.setColumnBoolean((i % 3) == 0);
allTypes.setColumnBinary(new byte[]{1, 2, 3});
allTypes.setColumnDate(new Date());
allTypes.setColumnDouble(Math.PI);
allTypes.setColumnFloat(1.234567f + i);
allTypes.setColumnString("test data " + i);
allTypes.setColumnLong(i);
NonLatinFieldNames nonLatinFieldNames = testRealm.createObject(NonLatinFieldNames.class);
nonLatinFieldNames.set델타(i);
nonLatinFieldNames.setΔέλτα(i);
nonLatinFieldNames.set베타(1.234567f + i);
nonLatinFieldNames.setΒήτα(1.234567f + i);
}
testRealm.commitTransaction();
testRealm.setAutoRefresh(true);
}
private void populateForDistinct(Realm realm, long numberOfBlocks, long numberOfObjects, boolean withNull) {
realm.beginTransaction();
for (int i = 0; i < numberOfObjects * numberOfBlocks; i++) {
for (int j = 0; j < numberOfBlocks; j++) {
AnnotationIndexTypes obj = realm.createObject(AnnotationIndexTypes.class);
obj.setIndexBoolean(j % 2 == 0);
obj.setIndexLong(j);
obj.setIndexDate(withNull ? null : new Date(1000L * j));
obj.setIndexString(withNull ? null : "Test " + j);
obj.setNotIndexBoolean(j % 2 == 0);
obj.setNotIndexLong(j);
obj.setNotIndexDate(withNull ? null : new Date(1000L * j));
obj.setNotIndexString(withNull ? null : "Test " + j);
}
}
realm.commitTransaction();
}
}