blob: c41d5b2cd12cbae50191075afee011bf7b412123 [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* 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 androidx.room.integration.testapp.test;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.hasItem;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalToIgnoringCase;
import static org.hamcrest.Matchers.hasSize;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import android.content.Context;
import android.database.Cursor;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.LargeTest;
import android.support.test.filters.MediumTest;
import android.support.test.filters.SdkSuppress;
import android.support.test.runner.AndroidJUnit4;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.room.InvalidationTracker;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.room.integration.testapp.TestDatabase;
import androidx.room.integration.testapp.dao.UserDao;
import androidx.room.integration.testapp.vo.User;
import androidx.sqlite.db.SupportSQLiteDatabase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
@MediumTest
@SdkSuppress(minSdkVersion = 16)
public class WriteAheadLoggingTest {
private static final String DATABASE_NAME = "wal.db";
private TestDatabase mDatabase;
@Before
public void openDatabase() {
Context context = InstrumentationRegistry.getTargetContext();
context.deleteDatabase(DATABASE_NAME);
mDatabase = Room.databaseBuilder(context, TestDatabase.class, DATABASE_NAME)
.setJournalMode(RoomDatabase.JournalMode.WRITE_AHEAD_LOGGING)
.build();
}
@After
public void closeDatabase() {
mDatabase.close();
Context context = InstrumentationRegistry.getTargetContext();
context.deleteDatabase(DATABASE_NAME);
}
@Test
public void checkJournalMode() {
Cursor c = null;
try {
SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase();
c = db.query("PRAGMA journal_mode");
c.moveToFirst();
String journalMode = c.getString(0);
assertThat(journalMode, is(equalToIgnoringCase("wal")));
} finally {
if (c != null) {
c.close();
}
}
}
@Test
public void disableWal() {
Context context = InstrumentationRegistry.getTargetContext();
mDatabase.close();
mDatabase = Room.databaseBuilder(context, TestDatabase.class, DATABASE_NAME)
.setJournalMode(RoomDatabase.JournalMode.TRUNCATE)
.build();
Cursor c = null;
try {
SupportSQLiteDatabase db = mDatabase.getOpenHelper().getWritableDatabase();
c = db.query("PRAGMA journal_mode");
c.moveToFirst();
String journalMode = c.getString(0);
assertThat(journalMode, is(not(equalToIgnoringCase("wal"))));
} finally {
if (c != null) {
c.close();
}
}
}
@Test
public void observeLiveData() {
UserDao dao = mDatabase.getUserDao();
LiveData<User> user1 = dao.liveUserById(1);
Observer<User> observer = startObserver(user1);
dao.insert(TestUtil.createUser(1));
verify(observer, timeout(3000).atLeastOnce())
.onChanged(argThat(user -> user != null && user.getId() == 1));
stopObserver(user1, observer);
}
@Test
public void observeLiveDataWithTransaction() {
UserDao dao = mDatabase.getUserDao();
LiveData<User> user1 = dao.liveUserByIdInTransaction(1);
Observer<User> observer = startObserver(user1);
dao.insert(TestUtil.createUser(1));
verify(observer, timeout(3000).atLeastOnce())
.onChanged(argThat(user -> user != null && user.getId() == 1));
stopObserver(user1, observer);
}
@Test
public void parallelWrites() throws InterruptedException, ExecutionException {
int numberOfThreads = 10;
ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);
ArrayList<Future<Boolean>> futures = new ArrayList<>();
for (int i = 0; i < numberOfThreads; i++) {
final int id = i + 1;
futures.add(i, executor.submit(() -> {
User user = TestUtil.createUser(id);
user.setName("user" + id);
mDatabase.getUserDao().insert(user);
return true;
}));
}
LiveData<List<User>> usersList = mDatabase.getUserDao().liveUsersListByName("user");
Observer<List<User>> observer = startObserver(usersList);
for (Future future : futures) {
assertThat(future.get(), is(true));
}
verify(observer, timeout(3000).atLeastOnce())
.onChanged(argThat(users -> users != null && users.size() == numberOfThreads));
stopObserver(usersList, observer);
}
@Test
public void readInBackground() throws InterruptedException, ExecutionException {
final UserDao dao = mDatabase.getUserDao();
final User user1 = TestUtil.createUser(1);
dao.insert(user1);
try {
mDatabase.beginTransaction();
dao.delete(user1);
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() ->
assertThat(dao.load(1), is(equalTo(user1))));
future.get();
mDatabase.setTransactionSuccessful();
} finally {
mDatabase.endTransaction();
}
assertThat(dao.count(), is(0));
}
@Test
@LargeTest
public void observeInvalidationInBackground() throws InterruptedException, ExecutionException {
final UserDao dao = mDatabase.getUserDao();
final User user1 = TestUtil.createUser(1);
final CountDownLatch observerRegistered = new CountDownLatch(1);
final CountDownLatch onInvalidatedCalled = new CountDownLatch(1);
dao.insert(user1);
Future future;
try {
mDatabase.beginTransaction();
dao.delete(user1);
future = Executors.newSingleThreadExecutor().submit(() -> {
// Adding this observer will be blocked by the surrounding transaction.
mDatabase.getInvalidationTracker().addObserver(
new InvalidationTracker.Observer("User") {
@Override
public void onInvalidated(@NonNull Set<String> tables) {
onInvalidatedCalled.countDown(); // This should not happen
}
});
observerRegistered.countDown();
});
mDatabase.setTransactionSuccessful();
} finally {
assertThat(observerRegistered.getCount(), is(1L));
mDatabase.endTransaction();
}
assertThat(dao.count(), is(0));
assertThat(observerRegistered.await(3000, TimeUnit.MILLISECONDS), is(true));
future.get();
assertThat(onInvalidatedCalled.await(500, TimeUnit.MILLISECONDS), is(false));
}
@Test
public void invalidation() throws InterruptedException {
final CountDownLatch latch = new CountDownLatch(1);
mDatabase.getInvalidationTracker().addObserver(new InvalidationTracker.Observer("User") {
@Override
public void onInvalidated(@NonNull Set<String> tables) {
assertThat(tables, hasSize(1));
assertThat(tables, hasItem("User"));
latch.countDown();
}
});
mDatabase.getUserDao().insert(TestUtil.createUser(1));
assertThat(latch.await(3000, TimeUnit.MILLISECONDS), is(true));
}
private static <T> Observer<T> startObserver(LiveData<T> liveData) {
@SuppressWarnings("unchecked")
Observer<T> observer = mock(Observer.class);
InstrumentationRegistry.getInstrumentation().runOnMainSync(() ->
liveData.observeForever(observer));
return observer;
}
private static <T> void stopObserver(LiveData<T> liveData, Observer<T> observer) {
InstrumentationRegistry.getInstrumentation().runOnMainSync(() ->
liveData.removeObserver(observer));
}
}