Merge changes from topic "invalidationTest" into androidx-main am: dc1dd76470
Original change: https://android-review.googlesource.com/c/platform/frameworks/support/+/2112370
Change-Id: Ib446ef5c30d417e3fdfe23aa01e44f2b0da4d172
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 6d128fb..97b429e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -146,6 +146,7 @@
mockitoCore = { module = "org.mockito:mockito-core", version.ref = "mockito" }
mockitoAndroid = { module = "org.mockito:mockito-android", version.ref = "mockito" }
mockitoKotlin = { module = "com.nhaarman.mockitokotlin2:mockito-kotlin", version = "2.1.0" }
+mockitoKotlin4 = { module = "org.mockito.kotlin:mockito-kotlin", version = "4.0.0" }
multidex = { module = "androidx.multidex:multidex", version = "2.0.1" }
nullaway = { module = "com.uber.nullaway:nullaway", version = "0.3.7" }
okhttpMockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version = "3.14.7" }
diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index e872903..5d3ed73 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -4259,6 +4259,54 @@
-----END PGP PUBLIC KEY BLOCK-----
+pub 689CBE64F4BC997F
+uid Szczepan Faber <szczepiq@gmail.com>
+
+sub C0058C509A81C102
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: BCPG v1.68
+
+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
+B1jblPtxo2UqRO/qhPfqSkoVcpWmxgiPUFOr
+=w8k7
+-----END PGP PUBLIC KEY BLOCK-----
+
+
pub 6A65176A0FB1CD0B
uid Paul King <paulk@apache.org>
uid keybase.io/paulk_asert <paulk_asert@keybase.io>
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index d802f30..d3f231d 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -65,6 +65,7 @@
<trusted-key id="1188b69f6d6259ca" group="com.google.accompanist"/>
<trusted-key id="11b581967f079a30a3e93140d57506cd188fd842" group="com.google.api.grpc"/>
<trusted-key id="120d6f34e627ed3a772ebbfe55c7e5e701832382" group="org.snakeyaml"/>
+ <trusted-key id="147b691a19097624902f4ea9689cbe64f4bc997f" group="^org[.]mockito($|([.].*))" regex="true"/>
<trusted-key id="151ba00a46886a5f95441a0f5d67bffcba1f9a39" group="com.google.gradle" name="osdetector-gradle-plugin"/>
<trusted-key id="160a7a9cf46221a56b06ad64461a804f2609fd89" group="com.github.shyiko.klob" name="klob"/>
<trusted-key id="1861c322c56014b2" group="commons-lang"/>
diff --git a/room/room-runtime/build.gradle b/room/room-runtime/build.gradle
index cb11beb..56968c37 100644
--- a/room/room-runtime/build.gradle
+++ b/room/room-runtime/build.gradle
@@ -49,8 +49,10 @@
testImplementation("androidx.arch.core:core-testing:2.0.1")
testImplementation(libs.junit)
testImplementation(libs.mockitoCore)
+ testImplementation(libs.mockitoKotlin4)
testImplementation("androidx.lifecycle:lifecycle-livedata-core:2.0.0")
testImplementation(libs.kotlinStdlib)
+ testImplementation(libs.kotlinTest)
testImplementation(libs.truth)
testImplementation(libs.testRunner) // Needed for @FlakyTest and @Ignore
diff --git a/room/room-runtime/lint-baseline.xml b/room/room-runtime/lint-baseline.xml
index d5a2665..abbcbf7 100644
--- a/room/room-runtime/lint-baseline.xml
+++ b/room/room-runtime/lint-baseline.xml
@@ -331,7 +331,7 @@
errorLine1=" Locale.setDefault(Locale.forLanguageTag("tr-TR"));"
errorLine2=" ~~~~~~~~~~~~~~">
<location
- file="src/test/java/androidx/room/InvalidationTrackerTest.java"/>
+ file="src/test/java/androidx/room/InvalidationTrackerTest.kt"/>
</issue>
<issue
diff --git a/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt b/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
index a31e893..8b83f50 100644
--- a/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
+++ b/room/room-runtime/src/main/java/androidx/room/InvalidationTracker.kt
@@ -93,8 +93,16 @@
@GuardedBy("observerMap")
@RestrictTo(RestrictTo.Scope.LIBRARY)
@JvmField
+ @VisibleForTesting
protected val observerMap = SafeIterableMap<Observer, ObserverWrapper>()
+ @GuardedBy("observerMap")
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @VisibleForTesting
+ fun getObserverMap(): SafeIterableMap<Observer, ObserverWrapper> {
+ return observerMap
+ }
+
private var multiInstanceInvalidationClient: MultiInstanceInvalidationClient? = null
private val syncTriggersLock = Any()
@@ -609,7 +617,7 @@
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
- protected class ObserverWrapper(
+ class ObserverWrapper(
@get:RestrictTo(RestrictTo.Scope.LIBRARY)
val observer: Observer,
@get:RestrictTo(RestrictTo.Scope.LIBRARY)
diff --git a/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.java b/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.java
deleted file mode 100644
index 2729620..0000000
--- a/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.java
+++ /dev/null
@@ -1,580 +0,0 @@
-/*
- * Copyright (C) 2017 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;
-
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.IsCollectionContaining.hasItem;
-import static org.hamcrest.core.IsCollectionContaining.hasItems;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.reset;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import android.database.Cursor;
-import android.database.sqlite.SQLiteException;
-
-import androidx.annotation.NonNull;
-import androidx.arch.core.executor.ArchTaskExecutor;
-import androidx.arch.core.executor.JunitTaskExecutorRule;
-import androidx.sqlite.db.SimpleSQLiteQuery;
-import androidx.sqlite.db.SupportSQLiteDatabase;
-import androidx.sqlite.db.SupportSQLiteOpenHelper;
-import androidx.sqlite.db.SupportSQLiteStatement;
-
-import com.google.common.truth.Truth;
-
-import org.junit.After;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.JUnit4;
-import org.mockito.ArgumentCaptor;
-import org.mockito.ArgumentMatcher;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-import java.lang.ref.Reference;
-import java.lang.ref.ReferenceQueue;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-import java.util.concurrent.Callable;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.locks.ReentrantLock;
-
-@RunWith(JUnit4.class)
-public class InvalidationTrackerTest {
- private InvalidationTracker mTracker;
- @Mock
- private RoomDatabase mRoomDatabase;
- @Mock
- private SupportSQLiteDatabase mSqliteDb;
- @Mock
- private SupportSQLiteOpenHelper mOpenHelper;
- @Rule
- public JunitTaskExecutorRule mTaskExecutorRule = new JunitTaskExecutorRule(1, true);
-
- @Before
- public void setup() {
- MockitoAnnotations.initMocks(this);
- final SupportSQLiteStatement statement = mock(SupportSQLiteStatement.class);
- doReturn(statement).when(mSqliteDb)
- .compileStatement(eq(InvalidationTracker.RESET_UPDATED_TABLES_SQL));
- doReturn(mSqliteDb).when(mOpenHelper).getWritableDatabase();
- doReturn(true).when(mRoomDatabase).isOpen();
- doReturn(ArchTaskExecutor.getIOThreadExecutor()).when(mRoomDatabase).getQueryExecutor();
- ReentrantLock closeLock = new ReentrantLock();
- doReturn(closeLock).when(mRoomDatabase).getCloseLock();
- //noinspection ResultOfMethodCallIgnored
- doReturn(mOpenHelper).when(mRoomDatabase).getOpenHelper();
- HashMap<String, String> shadowTables = new HashMap<>();
- shadowTables.put("C", "C_content");
- shadowTables.put("d", "a");
- HashMap<String, Set<String>> viewTables = new HashMap<>();
- HashSet<String> tableSet = new HashSet<>();
- tableSet.add("a");
- viewTables.put("e", tableSet);
- mTracker = new InvalidationTracker(mRoomDatabase, shadowTables, viewTables,
- "a", "B", "i", "C", "d");
- mTracker.internalInit(mSqliteDb);
- reset(mSqliteDb);
- }
-
- @Before
- public void setLocale() {
- Locale.setDefault(Locale.forLanguageTag("tr-TR"));
- }
-
- @After
- public void unsetLocale() {
- Locale.setDefault(Locale.US);
- }
-
- @Test
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void tableIds() {
- assertThat(mTracker.tableIdLookup.size(), is(5));
- assertThat(mTracker.tableIdLookup.get("a"), is(0));
- assertThat(mTracker.tableIdLookup.get("b"), is(1));
- assertThat(mTracker.tableIdLookup.get("i"), is(2));
- assertThat(mTracker.tableIdLookup.get("c"), is(3)); // fts
- assertThat(mTracker.tableIdLookup.get("d"), is(0)); // external content fts
- }
-
- @Test
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void tableNames() {
- assertThat(mTracker.tablesNames.length, is(5));
- assertThat(mTracker.tablesNames[0], is("a"));
- assertThat(mTracker.tablesNames[1], is("b"));
- assertThat(mTracker.tablesNames[2], is("i"));
- assertThat(mTracker.tablesNames[3], is("c_content")); // fts
- assertThat(mTracker.tablesNames[4], is("a")); // external content fts
- }
-
- @Test
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void testWeak() throws InterruptedException {
- final AtomicInteger data = new AtomicInteger(0);
- InvalidationTracker.Observer observer = new InvalidationTracker.Observer("a") {
- @Override
- public void onInvalidated(@NonNull Set<String> tables) {
- data.incrementAndGet();
- }
- };
- ReferenceQueue<Object> queue = new ReferenceQueue<>();
- WeakReference<InvalidationTracker.Observer> weakRef = new WeakReference<>(observer, queue);
- mTracker.addWeakObserver(observer);
- setInvalidatedTables(0);
- refreshSync();
- assertThat(data.get(), is(1));
- observer = null;
- forceGc(queue);
- setInvalidatedTables(0);
- refreshSync();
- assertThat(data.get(), is(1));
- }
-
- @Test
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void addRemoveObserver() throws Exception {
- InvalidationTracker.Observer observer = new LatchObserver(1, "a");
- mTracker.addObserver(observer);
- assertThat(mTracker.observerMap.size(), is(1));
- mTracker.removeObserver(new LatchObserver(1, "a"));
- assertThat(mTracker.observerMap.size(), is(1));
- mTracker.removeObserver(observer);
- assertThat(mTracker.observerMap.size(), is(0));
- }
-
- private void drainTasks() throws InterruptedException {
- mTaskExecutorRule.drainTasks(200);
- }
-
- @Test(expected = IllegalArgumentException.class)
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void badObserver() {
- InvalidationTracker.Observer observer = new LatchObserver(1, "x");
- mTracker.addObserver(observer);
- }
-
- private void refreshSync() throws InterruptedException {
- mTracker.refreshVersionsAsync();
- drainTasks();
- }
-
- @Test
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void refreshCheckTasks() throws Exception {
- when(mRoomDatabase.query(any(SimpleSQLiteQuery.class)))
- .thenReturn(mock(Cursor.class));
- mTracker.refreshVersionsAsync();
- mTracker.refreshVersionsAsync();
- verify(mTaskExecutorRule.getTaskExecutor()).executeOnDiskIO(mTracker.refreshRunnable);
- drainTasks();
-
- reset(mTaskExecutorRule.getTaskExecutor());
- mTracker.refreshVersionsAsync();
- verify(mTaskExecutorRule.getTaskExecutor()).executeOnDiskIO(mTracker.refreshRunnable);
- }
-
- @Test
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void observe1Table() throws Exception {
- LatchObserver observer = new LatchObserver(1, "a");
- mTracker.addObserver(observer);
- setInvalidatedTables(0);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(1));
- assertThat(observer.getInvalidatedTables(), hasItem("a"));
-
- setInvalidatedTables(1);
- observer.reset(1);
- refreshSync();
- assertThat(observer.await(), is(false));
-
- setInvalidatedTables(0);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(1));
- assertThat(observer.getInvalidatedTables(), hasItem("a"));
- }
-
- @Test
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void observe2Tables() throws Exception {
- LatchObserver observer = new LatchObserver(1, "A", "B");
- mTracker.addObserver(observer);
- setInvalidatedTables(0, 1);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(2));
- assertThat(observer.getInvalidatedTables(), hasItems("A", "B"));
-
- setInvalidatedTables(1, 2);
- observer.reset(1);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(1));
- assertThat(observer.getInvalidatedTables(), hasItem("B"));
-
- setInvalidatedTables(0, 3);
- observer.reset(1);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(1));
- assertThat(observer.getInvalidatedTables(), hasItem("A"));
-
- observer.reset(1);
- refreshSync();
- assertThat(observer.await(), is(false));
- }
-
- @Test
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void locale() {
- LatchObserver observer = new LatchObserver(1, "I");
- mTracker.addObserver(observer);
- }
-
- @Test
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void closedDb() {
- doReturn(false).when(mRoomDatabase).isOpen();
- doThrow(new IllegalStateException("foo")).when(mOpenHelper).getWritableDatabase();
- mTracker.addObserver(new LatchObserver(1, "a", "b"));
- mTracker.refreshRunnable.run();
- }
-
- @Test
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void createTriggerOnShadowTable() {
- LatchObserver observer = new LatchObserver(1, "C");
- String[] triggers = new String[]{"UPDATE", "DELETE", "INSERT"};
- ArgumentCaptor<String> sqlArgCaptor;
- List<String> sqlCaptorValues;
-
- mTracker.addObserver(observer);
- sqlArgCaptor = ArgumentCaptor.forClass(String.class);
- verify(mSqliteDb, times(4)).execSQL(sqlArgCaptor.capture());
- sqlCaptorValues = sqlArgCaptor.getAllValues();
- assertThat(sqlCaptorValues.get(0),
- is("INSERT OR IGNORE INTO room_table_modification_log VALUES(3, 0)"));
- for (int i = 0; i < triggers.length; i++) {
- assertThat(sqlCaptorValues.get(i + 1),
- is("CREATE TEMP TRIGGER IF NOT EXISTS "
- + "`room_table_modification_trigger_c_content_" + triggers[i]
- + "` AFTER " + triggers[i] + " ON `c_content` BEGIN UPDATE "
- + "room_table_modification_log SET invalidated = 1 WHERE table_id = 3 "
- + "AND invalidated = 0; END"
- ));
- }
-
- reset(mSqliteDb);
-
- mTracker.removeObserver(observer);
- sqlArgCaptor = ArgumentCaptor.forClass(String.class);
- verify(mSqliteDb, times(3)).execSQL(sqlArgCaptor.capture());
- sqlCaptorValues = sqlArgCaptor.getAllValues();
- for (int i = 0; i < triggers.length; i++) {
- assertThat(sqlCaptorValues.get(i),
- is("DROP TRIGGER IF EXISTS `room_table_modification_trigger_c_content_"
- + triggers[i] + "`"));
- }
- }
-
- @Test
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void observeFtsTable() throws InterruptedException {
- LatchObserver observer = new LatchObserver(1, "C");
- mTracker.addObserver(observer);
- setInvalidatedTables(3);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(1));
- assertThat(observer.getInvalidatedTables(), hasItem("C"));
-
- setInvalidatedTables(1);
- observer.reset(1);
- refreshSync();
- assertThat(observer.await(), is(false));
-
- setInvalidatedTables(0, 3);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(1));
- assertThat(observer.getInvalidatedTables(), hasItem("C"));
- }
-
- @Test
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void observeExternalContentFtsTable() throws InterruptedException {
- LatchObserver observer = new LatchObserver(1, "d");
- mTracker.addObserver(observer);
- setInvalidatedTables(0);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(1));
- assertThat(observer.getInvalidatedTables(), hasItem("d"));
-
- setInvalidatedTables(2, 3);
- observer.reset(1);
- refreshSync();
- assertThat(observer.await(), is(false));
-
- setInvalidatedTables(0, 1);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(1));
- assertThat(observer.getInvalidatedTables(), hasItem("d"));
- }
-
- @Test
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void observeExternalContentFtsTableAndContentTable() throws InterruptedException {
- LatchObserver observer = new LatchObserver(1, "d", "a");
- mTracker.addObserver(observer);
- setInvalidatedTables(0);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(2));
- assertThat(observer.getInvalidatedTables(), hasItems("d", "a"));
-
- setInvalidatedTables(2, 3);
- observer.reset(1);
- refreshSync();
- assertThat(observer.await(), is(false));
-
- setInvalidatedTables(0, 1);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(2));
- assertThat(observer.getInvalidatedTables(), hasItems("d", "a"));
- }
-
- @Test
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void observeExternalContentFatsTableAndContentTableSeparately()
- throws InterruptedException {
- LatchObserver observerA = new LatchObserver(1, "a");
- LatchObserver observerD = new LatchObserver(1, "d");
- mTracker.addObserver(observerA);
- mTracker.addObserver(observerD);
-
- setInvalidatedTables(0);
- refreshSync();
-
- assertThat(observerA.await(), is(true));
- assertThat(observerD.await(), is(true));
- assertThat(observerA.getInvalidatedTables().size(), is(1));
- assertThat(observerD.getInvalidatedTables().size(), is(1));
- assertThat(observerA.getInvalidatedTables(), hasItem("a"));
- assertThat(observerD.getInvalidatedTables(), hasItem("d"));
-
- // Remove observer 'd' which is backed by 'a', observers to 'a' should still work.
- mTracker.removeObserver(observerD);
-
- setInvalidatedTables(0);
- observerA.reset(1);
- observerD.reset(1);
- refreshSync();
-
- assertThat(observerA.await(), is(true));
- assertThat(observerD.await(), is(false));
- assertThat(observerA.getInvalidatedTables().size(), is(1));
- assertThat(observerA.getInvalidatedTables(), hasItem("a"));
- }
-
- @Test
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void observeView() throws InterruptedException {
- LatchObserver observer = new LatchObserver(1, "E");
- mTracker.addObserver(observer);
- setInvalidatedTables(0, 1);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(1));
- assertThat(observer.getInvalidatedTables(), hasItem("a"));
-
- setInvalidatedTables(2, 3);
- observer.reset(1);
- refreshSync();
- assertThat(observer.await(), is(false));
-
- setInvalidatedTables(0, 1);
- refreshSync();
- assertThat(observer.await(), is(true));
- assertThat(observer.getInvalidatedTables().size(), is(1));
- assertThat(observer.getInvalidatedTables(), hasItem("a"));
- }
-
- @SuppressWarnings("deprecation")
- @Test
- @Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
- public void failFastCreateLiveData() {
- // assert that sending a bad createLiveData table name fails instantly
- try {
- mTracker.createLiveData(new String[]{"invalid table name"}, new Callable<Void>() {
- @Override
- public Void call() throws Exception {
- return null;
- }
- });
- Assert.fail("should've throw an exception for invalid table name");
- } catch (IllegalArgumentException expected) {
- // expected
- }
- }
-
- @Test
- @Ignore // disabled due to flakiness b/65257997
- public void closedDbAfterOpen() throws InterruptedException {
- setInvalidatedTables(3, 1);
- mTracker.addObserver(new LatchObserver(1, "a", "b"));
- mTracker.syncTriggers();
- mTracker.refreshRunnable.run();
- doThrow(new SQLiteException("foo")).when(mRoomDatabase).query(
- Mockito.eq(InvalidationTracker.SELECT_UPDATED_TABLES_SQL),
- any(Object[].class));
- mTracker.pendingRefresh.set(true);
- mTracker.refreshRunnable.run();
- }
-
- /**
- * Setup Cursor result to return INVALIDATED for given tableIds
- */
- private void setInvalidatedTables(int... tableIds) throws InterruptedException {
- // mockito does not like multi-threaded access so before setting versions, make sure we
- // sync background tasks.
- drainTasks();
- Cursor cursor = createCursorWithValues(tableIds);
- doReturn(cursor).when(mRoomDatabase).query(
- argThat(new ArgumentMatcher<SimpleSQLiteQuery>() {
- @Override
- public boolean matches(SimpleSQLiteQuery argument) {
- return argument.getSql().equals(
- InvalidationTracker.SELECT_UPDATED_TABLES_SQL);
- }
- })
- );
- }
-
- private Cursor createCursorWithValues(final int... tableIds) {
- Cursor cursor = mock(Cursor.class);
- final AtomicInteger index = new AtomicInteger(-1);
- when(cursor.moveToNext()).thenAnswer(new Answer<Boolean>() {
- @Override
- public Boolean answer(InvocationOnMock invocation) throws Throwable {
- return index.addAndGet(1) < tableIds.length;
- }
- });
- Answer<Integer> intAnswer = new Answer<Integer>() {
- @Override
- public Integer answer(InvocationOnMock invocation) throws Throwable {
- // checkUpdatedTable only checks for column 0 (invalidated table id)
- assert ((Integer) invocation.getArguments()[0]) == 0;
- return tableIds[index.intValue()];
- }
- };
- when(cursor.getInt(anyInt())).thenAnswer(intAnswer);
- return cursor;
- }
-
- static class LatchObserver extends InvalidationTracker.Observer {
- private CountDownLatch mLatch;
- private Set<String> mInvalidatedTables;
-
- LatchObserver(int count, String... tableNames) {
- super(tableNames);
- mLatch = new CountDownLatch(count);
- }
-
- boolean await() throws InterruptedException {
- return mLatch.await(3, TimeUnit.SECONDS);
- }
-
- @Override
- public void onInvalidated(@NonNull Set<String> tables) {
- mInvalidatedTables = tables;
- mLatch.countDown();
- }
-
- void reset(@SuppressWarnings("SameParameterValue") int count) {
- mInvalidatedTables = null;
- mLatch = new CountDownLatch(count);
- }
-
- Set<String> getInvalidatedTables() {
- return mInvalidatedTables;
- }
- }
-
- /**
- * Tries to trigger garbage collection by allocating in the heap until an element is
- * available in the given reference queue.
- */
- private static void forceGc(ReferenceQueue<Object> queue) throws InterruptedException {
- AtomicBoolean continueTriggeringGc = new AtomicBoolean(true);
- Thread t = new Thread(() -> {
- int byteCount = 0;
- try {
- @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
- ArrayList<byte[]> leak = new ArrayList<>();
- do {
- int arraySize = (int) (Math.random() * 1000);
- byteCount += arraySize;
- leak.add(new byte[arraySize]);
- System.gc(); // Not guaranteed to trigger GC, hence the leak and the timeout
- Thread.sleep(10);
- } while (continueTriggeringGc.get());
- } catch (InterruptedException e) {
- // Ignored
- }
- System.out.println("Allocated " + byteCount + " bytes trying to force a GC.");
- });
- t.start();
- Reference<?> result = queue.remove(TimeUnit.SECONDS.toMillis(10));
- continueTriggeringGc.set(false);
- t.interrupt();
- Truth.assertWithMessage("Couldn't trigger garbage collection, test flake")
- .that(result)
- .isNotNull();
- result.clear();
- }
-}
diff --git a/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt b/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt
new file mode 100644
index 0000000..b897d80
--- /dev/null
+++ b/room/room-runtime/src/test/java/androidx/room/InvalidationTrackerTest.kt
@@ -0,0 +1,514 @@
+/*
+ * Copyright (C) 2017 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
+
+import android.database.Cursor
+import android.database.sqlite.SQLiteException
+import androidx.arch.core.executor.ArchTaskExecutor
+import androidx.arch.core.executor.JunitTaskExecutorRule
+import androidx.sqlite.db.SimpleSQLiteQuery
+import androidx.sqlite.db.SupportSQLiteDatabase
+import androidx.sqlite.db.SupportSQLiteOpenHelper
+import androidx.sqlite.db.SupportSQLiteStatement
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.lang.ref.ReferenceQueue
+import java.lang.ref.WeakReference
+import java.util.Locale
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicInteger
+import java.util.concurrent.locks.ReentrantLock
+import kotlin.test.assertFailsWith
+import kotlin.test.fail
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argThat
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.doThrow
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.isNull
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+import org.mockito.stubbing.Answer
+
+@RunWith(JUnit4::class)
+class InvalidationTrackerTest {
+ private lateinit var mTracker: InvalidationTracker
+
+ private val mRoomDatabase: RoomDatabase = mock()
+
+ private val mSqliteDb: SupportSQLiteDatabase = mock()
+
+ private val mOpenHelper: SupportSQLiteOpenHelper = mock()
+
+ @get:Rule
+ var mTaskExecutorRule = JunitTaskExecutorRule(1, true)
+
+ @Before
+ fun setup() {
+ val statement: SupportSQLiteStatement = mock()
+ doReturn(statement).whenever(mSqliteDb)
+ .compileStatement(eq(InvalidationTracker.RESET_UPDATED_TABLES_SQL))
+ doReturn(mSqliteDb).whenever(mOpenHelper).writableDatabase
+ doReturn(true).whenever(mRoomDatabase).isOpen
+ doReturn(ArchTaskExecutor.getIOThreadExecutor()).whenever(mRoomDatabase).queryExecutor
+ val closeLock = ReentrantLock()
+ doReturn(closeLock).whenever(mRoomDatabase).getCloseLock()
+ doReturn(mOpenHelper).whenever(mRoomDatabase).openHelper
+ val shadowTables = HashMap<String, String>()
+ shadowTables["C"] = "C_content"
+ shadowTables["d"] = "a"
+ val viewTables = HashMap<String, Set<String>>()
+ val tableSet = HashSet<String>()
+ tableSet.add("a")
+ viewTables["e"] = tableSet
+ mTracker = InvalidationTracker(
+ mRoomDatabase, shadowTables, viewTables,
+ "a", "B", "i", "C", "d"
+ )
+ mTracker.internalInit(mSqliteDb)
+ reset(mSqliteDb)
+ }
+
+ @Before
+ fun setLocale() {
+ Locale.setDefault(Locale.forLanguageTag("tr-TR"))
+ }
+
+ @After
+ fun unsetLocale() {
+ Locale.setDefault(Locale.US)
+ }
+
+ @Test
+ fun tableIds() {
+ assertThat(mTracker.tableIdLookup.size).isEqualTo(5)
+ assertThat(mTracker.tableIdLookup["a"]).isEqualTo(0)
+ assertThat(mTracker.tableIdLookup["b"]).isEqualTo(1)
+ assertThat(mTracker.tableIdLookup["i"]).isEqualTo(2)
+ assertThat(mTracker.tableIdLookup["c"]).isEqualTo(3) // fts
+ assertThat(mTracker.tableIdLookup["d"]).isEqualTo(0) // external content fts
+ }
+
+ @Test
+ fun tableNames() {
+ assertThat(mTracker.tablesNames.size).isEqualTo(5)
+ assertThat(mTracker.tablesNames[0]).isEqualTo("a")
+ assertThat(mTracker.tablesNames[1]).isEqualTo("b")
+ assertThat(mTracker.tablesNames[2]).isEqualTo("i")
+ assertThat(mTracker.tablesNames[3]).isEqualTo("c_content") // fts
+ assertThat(mTracker.tablesNames[4]).isEqualTo("a") // external content fts
+ }
+
+ @Test
+ @org.junit.Ignore // TODO(b/233855234) - disabled until test is moved to Kotlin
+ fun testWeak() {
+ val data = AtomicInteger(0)
+ var observer: InvalidationTracker.Observer? = object : InvalidationTracker.Observer("a") {
+ override fun onInvalidated(tables: Set<String>) {
+ data.incrementAndGet()
+ }
+ }
+ val queue = ReferenceQueue<Any?>()
+ WeakReference(observer, queue)
+ mTracker.addWeakObserver(observer!!)
+ setInvalidatedTables(0)
+ refreshSync()
+ assertThat(data.get()).isEqualTo(1)
+ @Suppress("UNUSED_VALUE") // On purpose, to dereference the observer and GC it
+ observer = null
+ forceGc(queue)
+ setInvalidatedTables(0)
+ refreshSync()
+ assertThat(data.get()).isEqualTo(1)
+ }
+
+ @Test
+ fun addRemoveObserver() {
+ val observer: InvalidationTracker.Observer = LatchObserver(1, "a")
+ mTracker.addObserver(observer)
+ assertThat(mTracker.getObserverMap().size()).isEqualTo(1)
+ mTracker.removeObserver(LatchObserver(1, "a"))
+ assertThat(mTracker.getObserverMap().size()).isEqualTo(1)
+ mTracker.removeObserver(observer)
+ assertThat(mTracker.getObserverMap().size()).isEqualTo(0)
+ }
+
+ private fun drainTasks() {
+ mTaskExecutorRule.drainTasks(200)
+ }
+
+ @Test
+ fun badObserver() {
+ assertFailsWith<IllegalArgumentException>(message = "There is no table with name x") {
+ val observer: InvalidationTracker.Observer = LatchObserver(1, "x")
+ mTracker.addObserver(observer)
+ }
+ }
+
+ private fun refreshSync() {
+ mTracker.refreshVersionsAsync()
+ drainTasks()
+ }
+
+ @Test
+ fun refreshCheckTasks() {
+ whenever(mRoomDatabase.query(any<SimpleSQLiteQuery>(), isNull())).thenReturn(mock<Cursor>())
+ mTracker.refreshVersionsAsync()
+ mTracker.refreshVersionsAsync()
+ verify(mTaskExecutorRule.taskExecutor).executeOnDiskIO(mTracker.refreshRunnable)
+ drainTasks()
+ reset(mTaskExecutorRule.taskExecutor)
+ mTracker.refreshVersionsAsync()
+ verify(mTaskExecutorRule.taskExecutor).executeOnDiskIO(mTracker.refreshRunnable)
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun observe1Table() {
+ val observer = LatchObserver(1, "a")
+ mTracker.addObserver(observer)
+ setInvalidatedTables(0)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(true)
+ assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
+ assertThat(observer.invalidatedTables).contains("a")
+ setInvalidatedTables(1)
+ observer.reset(1)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(false)
+ setInvalidatedTables(0)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(true)
+ assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
+ assertThat(observer.invalidatedTables).contains("a")
+ }
+
+ @Test
+ @Throws(Exception::class)
+ fun observe2Tables() {
+ val observer = LatchObserver(1, "A", "B")
+ mTracker.addObserver(observer)
+ setInvalidatedTables(0, 1)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(true)
+ assertThat(observer.invalidatedTables!!.size).isEqualTo(2)
+ assertThat(observer.invalidatedTables).containsAtLeast("A", "B")
+ setInvalidatedTables(1, 2)
+ observer.reset(1)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(true)
+ assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
+ assertThat(observer.invalidatedTables).contains("B")
+ setInvalidatedTables(0, 3)
+ observer.reset(1)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(true)
+ assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
+ assertThat(observer.invalidatedTables).contains("A")
+ observer.reset(1)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(false)
+ }
+
+ @Test
+ fun locale() {
+ val observer = LatchObserver(1, "I")
+ mTracker.addObserver(observer)
+ }
+
+ @Test
+ fun closedDb() {
+ doReturn(false).whenever(mRoomDatabase).isOpen
+ doThrow(IllegalStateException("foo")).whenever(mOpenHelper).writableDatabase
+ mTracker.addObserver(LatchObserver(1, "a", "b"))
+ mTracker.refreshRunnable.run()
+ }
+
+ @Test
+ fun createTriggerOnShadowTable() {
+ val observer = LatchObserver(1, "C")
+ val triggers = arrayOf("UPDATE", "DELETE", "INSERT")
+ var sqlArgCaptor: ArgumentCaptor<String>
+ var sqlCaptorValues: List<String>
+ mTracker.addObserver(observer)
+ sqlArgCaptor = ArgumentCaptor.forClass(String::class.java)
+ verify(mSqliteDb, times(4)).execSQL(sqlArgCaptor.capture())
+ sqlCaptorValues = sqlArgCaptor.allValues
+ assertThat(sqlCaptorValues[0])
+ .isEqualTo("INSERT OR IGNORE INTO room_table_modification_log VALUES(3, 0)")
+ for (i in triggers.indices) {
+ assertThat(sqlCaptorValues[i + 1])
+ .isEqualTo(
+ "CREATE TEMP TRIGGER IF NOT EXISTS " +
+ "`room_table_modification_trigger_c_content_" + triggers[i] +
+ "` AFTER " + triggers[i] + " ON `c_content` BEGIN UPDATE " +
+ "room_table_modification_log SET invalidated = 1 WHERE table_id = 3 " +
+ "AND invalidated = 0; END"
+ )
+ }
+ reset(mSqliteDb)
+ mTracker.removeObserver(observer)
+ sqlArgCaptor = ArgumentCaptor.forClass(String::class.java)
+ verify(mSqliteDb, times(3)).execSQL(sqlArgCaptor.capture())
+ sqlCaptorValues = sqlArgCaptor.allValues
+ for (i in triggers.indices) {
+ assertThat(sqlCaptorValues[i])
+ .isEqualTo(
+ "DROP TRIGGER IF EXISTS `room_table_modification_trigger_c_content_" +
+ triggers[i] + "`"
+ )
+ }
+ }
+
+ @Test
+ fun observeFtsTable() {
+ val observer = LatchObserver(1, "C")
+ mTracker.addObserver(observer)
+ setInvalidatedTables(3)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(true)
+ assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
+ assertThat(observer.invalidatedTables).contains("C")
+ setInvalidatedTables(1)
+ observer.reset(1)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(false)
+ setInvalidatedTables(0, 3)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(true)
+ assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
+ assertThat(observer.invalidatedTables).contains("C")
+ }
+
+ @Test
+ fun observeExternalContentFtsTable() {
+ val observer = LatchObserver(1, "d")
+ mTracker.addObserver(observer)
+ setInvalidatedTables(0)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(true)
+ assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
+ assertThat(observer.invalidatedTables).contains("d")
+ setInvalidatedTables(2, 3)
+ observer.reset(1)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(false)
+ setInvalidatedTables(0, 1)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(true)
+ assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
+ assertThat(observer.invalidatedTables).contains("d")
+ }
+
+ @Test
+ fun observeExternalContentFtsTableAndContentTable() {
+ val observer = LatchObserver(1, "d", "a")
+ mTracker.addObserver(observer)
+ setInvalidatedTables(0)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(true)
+ assertThat(observer.invalidatedTables!!.size).isEqualTo(2)
+ assertThat(observer.invalidatedTables).containsAtLeast("d", "a")
+ setInvalidatedTables(2, 3)
+ observer.reset(1)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(false)
+ setInvalidatedTables(0, 1)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(true)
+ assertThat(observer.invalidatedTables!!.size).isEqualTo(2)
+ assertThat(observer.invalidatedTables).containsAtLeast("d", "a")
+ }
+
+ @Test
+ fun observeExternalContentFatsTableAndContentTableSeparately() {
+ val observerA = LatchObserver(1, "a")
+ val observerD = LatchObserver(1, "d")
+ mTracker.addObserver(observerA)
+ mTracker.addObserver(observerD)
+ setInvalidatedTables(0)
+ refreshSync()
+ assertThat(observerA.await()).isEqualTo(true)
+ assertThat(observerD.await()).isEqualTo(true)
+ assertThat(observerA.invalidatedTables!!.size).isEqualTo(1)
+ assertThat(observerD.invalidatedTables!!.size).isEqualTo(1)
+ assertThat(observerA.invalidatedTables).contains("a")
+ assertThat(observerD.invalidatedTables).contains("d")
+
+ // Remove observer 'd' which is backed by 'a', observers to 'a' should still work.
+ mTracker.removeObserver(observerD)
+ setInvalidatedTables(0)
+ observerA.reset(1)
+ observerD.reset(1)
+ refreshSync()
+ assertThat(observerA.await()).isEqualTo(true)
+ assertThat(observerD.await()).isEqualTo(false)
+ assertThat(observerA.invalidatedTables!!.size).isEqualTo(1)
+ assertThat(observerA.invalidatedTables).contains("a")
+ }
+
+ @Test
+ fun observeView() {
+ val observer = LatchObserver(1, "E")
+ mTracker.addObserver(observer)
+ setInvalidatedTables(0, 1)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(true)
+ assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
+ assertThat(observer.invalidatedTables).contains("a")
+ setInvalidatedTables(2, 3)
+ observer.reset(1)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(false)
+ setInvalidatedTables(0, 1)
+ refreshSync()
+ assertThat(observer.await()).isEqualTo(true)
+ assertThat(observer.invalidatedTables!!.size).isEqualTo(1)
+ assertThat(observer.invalidatedTables).contains("a")
+ }
+
+ @Test
+ fun failFastCreateLiveData() {
+ // assert that sending a bad createLiveData table name fails instantly
+ try {
+ mTracker.createLiveData<Unit>(
+ tableNames = arrayOf("invalid table name"),
+ inTransaction = false
+ ) {}
+ fail("should've throw an exception for invalid table name")
+ } catch (expected: IllegalArgumentException) {
+ // expected
+ }
+ }
+
+ @Test
+ fun closedDbAfterOpen() {
+ setInvalidatedTables(3, 1)
+ mTracker.addObserver(LatchObserver(1, "a", "b"))
+ mTracker.syncTriggers()
+ mTracker.refreshRunnable.run()
+ doThrow(SQLiteException("foo")).whenever(mRoomDatabase)?.query(
+ query = InvalidationTracker.SELECT_UPDATED_TABLES_SQL,
+ args = arrayOf(Array<Any>::class.java)
+ )
+ mTracker.pendingRefresh.set(true)
+ mTracker.refreshRunnable.run()
+ }
+
+ /**
+ * Setup Cursor result to return INVALIDATED for given tableIds
+ */
+ private fun setInvalidatedTables(vararg tableIds: Int) {
+ // mockito does not like multi-threaded access so before setting versions, make sure we
+ // sync background tasks.
+ drainTasks()
+ val cursor = createCursorWithValues(*tableIds)
+ doReturn(cursor).whenever(mRoomDatabase)?.query(
+ query = argThat<SimpleSQLiteQuery> { argument ->
+ argument.sql == InvalidationTracker.SELECT_UPDATED_TABLES_SQL
+ },
+ signal = isNull(),
+ )
+ }
+
+ private fun createCursorWithValues(vararg tableIds: Int): Cursor {
+ val cursor: Cursor = mock()
+ val index = AtomicInteger(-1)
+ whenever(cursor.moveToNext()).thenAnswer { index.addAndGet(1) < tableIds.size }
+ val intAnswer = Answer { invocation ->
+ // checkUpdatedTable only checks for column 0 (invalidated table id)
+ assert(invocation.arguments[0] as Int == 0)
+ tableIds[index.toInt()]
+ }
+ whenever(cursor.getInt(anyInt())).thenAnswer(intAnswer)
+ return cursor
+ }
+
+ internal class LatchObserver(
+ count: Int,
+ vararg tableNames: String
+ ) : InvalidationTracker.Observer(arrayOf(*tableNames)) {
+ private var mLatch: CountDownLatch
+
+ var invalidatedTables: Set<String>? = null
+ private set
+
+ init {
+ mLatch = CountDownLatch(count)
+ }
+
+ fun await(): Boolean {
+ return mLatch.await(3, TimeUnit.SECONDS)
+ }
+
+ override fun onInvalidated(tables: Set<String>) {
+ invalidatedTables = tables
+ mLatch.countDown()
+ }
+
+ fun reset(count: Int) {
+ invalidatedTables = null
+ mLatch = CountDownLatch(count)
+ }
+ }
+
+ companion object {
+ /**
+ * Tries to trigger garbage collection by allocating in the heap until an element is
+ * available in the given reference queue.
+ */
+ private fun forceGc(queue: ReferenceQueue<Any?>) {
+ val continueTriggeringGc = AtomicBoolean(true)
+ val t = Thread {
+ var byteCount = 0
+ try {
+ val leak = ArrayList<ByteArray>()
+ do {
+ val arraySize = (Math.random() * 1000).toInt()
+ byteCount += arraySize
+ leak.add(ByteArray(arraySize))
+ System.gc() // Not guaranteed to trigger GC, hence the leak and the timeout
+ Thread.sleep(10)
+ } while (continueTriggeringGc.get())
+ } catch (e: InterruptedException) {
+ // Ignored
+ }
+ println("Allocated $byteCount bytes trying to force a GC.")
+ }
+ t.start()
+ val result = queue.remove(TimeUnit.SECONDS.toMillis(10))
+ continueTriggeringGc.set(false)
+ t.interrupt()
+ assertWithMessage("Couldn't trigger garbage collection, test flake")
+ .that(result)
+ .isNotNull()
+ result.clear()
+ }
+ }
+}
\ No newline at end of file
diff --git a/room/room-runtime/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/room/room-runtime/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000..1f0955d
--- /dev/null
+++ b/room/room-runtime/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline