1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.room; 18 19 import static org.hamcrest.CoreMatchers.is; 20 import static org.hamcrest.MatcherAssert.assertThat; 21 import static org.hamcrest.core.IsCollectionContaining.hasItem; 22 import static org.hamcrest.core.IsCollectionContaining.hasItems; 23 import static org.mockito.Matchers.any; 24 import static org.mockito.Matchers.anyInt; 25 import static org.mockito.Matchers.anyString; 26 import static org.mockito.Matchers.eq; 27 import static org.mockito.Mockito.doReturn; 28 import static org.mockito.Mockito.doThrow; 29 import static org.mockito.Mockito.mock; 30 import static org.mockito.Mockito.reset; 31 import static org.mockito.Mockito.verify; 32 import static org.mockito.Mockito.when; 33 34 import android.database.Cursor; 35 import android.database.sqlite.SQLiteException; 36 37 import androidx.annotation.NonNull; 38 import androidx.arch.core.executor.JunitTaskExecutorRule; 39 import androidx.sqlite.db.SupportSQLiteDatabase; 40 import androidx.sqlite.db.SupportSQLiteOpenHelper; 41 import androidx.sqlite.db.SupportSQLiteStatement; 42 43 import org.junit.After; 44 import org.junit.Before; 45 import org.junit.Rule; 46 import org.junit.Test; 47 import org.junit.runner.RunWith; 48 import org.junit.runners.JUnit4; 49 import org.mockito.Mockito; 50 import org.mockito.invocation.InvocationOnMock; 51 import org.mockito.stubbing.Answer; 52 53 import java.lang.ref.WeakReference; 54 import java.util.ArrayList; 55 import java.util.Locale; 56 import java.util.Set; 57 import java.util.concurrent.CountDownLatch; 58 import java.util.concurrent.TimeUnit; 59 import java.util.concurrent.atomic.AtomicInteger; 60 import java.util.concurrent.locks.ReentrantLock; 61 62 @RunWith(JUnit4.class) 63 public class InvalidationTrackerTest { 64 private InvalidationTracker mTracker; 65 private RoomDatabase mRoomDatabase; 66 private SupportSQLiteOpenHelper mOpenHelper; 67 @Rule 68 public JunitTaskExecutorRule mTaskExecutorRule = new JunitTaskExecutorRule(1, true); 69 70 @Before 71 public void setup() { 72 mRoomDatabase = mock(RoomDatabase.class); 73 SupportSQLiteDatabase sqliteDb = mock(SupportSQLiteDatabase.class); 74 final SupportSQLiteStatement statement = mock(SupportSQLiteStatement.class); 75 mOpenHelper = mock(SupportSQLiteOpenHelper.class); 76 77 doReturn(statement).when(sqliteDb).compileStatement(eq(InvalidationTracker.CLEANUP_SQL)); 78 doReturn(sqliteDb).when(mOpenHelper).getWritableDatabase(); 79 doReturn(true).when(mRoomDatabase).isOpen(); 80 ReentrantLock closeLock = new ReentrantLock(); 81 doReturn(closeLock).when(mRoomDatabase).getCloseLock(); 82 //noinspection ResultOfMethodCallIgnored 83 doReturn(mOpenHelper).when(mRoomDatabase).getOpenHelper(); 84 85 mTracker = new InvalidationTracker(mRoomDatabase, "a", "B", "i"); 86 mTracker.internalInit(sqliteDb); 87 } 88 89 @Before 90 public void setLocale() { 91 Locale.setDefault(Locale.forLanguageTag("tr-TR")); 92 } 93 94 @After 95 public void unsetLocale() { 96 Locale.setDefault(Locale.US); 97 } 98 99 @Test 100 public void tableIds() { 101 assertThat(mTracker.mTableIdLookup.get("a"), is(0)); 102 assertThat(mTracker.mTableIdLookup.get("b"), is(1)); 103 } 104 105 @Test 106 public void testWeak() throws InterruptedException { 107 final AtomicInteger data = new AtomicInteger(0); 108 InvalidationTracker.Observer observer = new InvalidationTracker.Observer("a") { 109 @Override 110 public void onInvalidated(@NonNull Set<String> tables) { 111 data.incrementAndGet(); 112 } 113 }; 114 mTracker.addWeakObserver(observer); 115 setVersions(1, 0); 116 refreshSync(); 117 assertThat(data.get(), is(1)); 118 observer = null; 119 forceGc(); 120 setVersions(2, 0); 121 refreshSync(); 122 assertThat(data.get(), is(1)); 123 } 124 125 @Test 126 public void addRemoveObserver() throws Exception { 127 InvalidationTracker.Observer observer = new LatchObserver(1, "a"); 128 mTracker.addObserver(observer); 129 assertThat(mTracker.mObserverMap.size(), is(1)); 130 mTracker.removeObserver(new LatchObserver(1, "a")); 131 assertThat(mTracker.mObserverMap.size(), is(1)); 132 mTracker.removeObserver(observer); 133 assertThat(mTracker.mObserverMap.size(), is(0)); 134 } 135 136 private void drainTasks() throws InterruptedException { 137 mTaskExecutorRule.drainTasks(200); 138 } 139 140 @Test(expected = IllegalArgumentException.class) 141 public void badObserver() { 142 InvalidationTracker.Observer observer = new LatchObserver(1, "x"); 143 mTracker.addObserver(observer); 144 } 145 146 @Test 147 public void refreshReadValues() throws Exception { 148 setVersions(1, 0, 2, 1); 149 refreshSync(); 150 assertThat(mTracker.mTableVersions, is(new long[]{1, 2, 0})); 151 152 setVersions(3, 1); 153 refreshSync(); 154 assertThat(mTracker.mTableVersions, is(new long[]{1, 3, 0})); 155 156 setVersions(7, 0); 157 refreshSync(); 158 assertThat(mTracker.mTableVersions, is(new long[]{7, 3, 0})); 159 160 refreshSync(); 161 assertThat(mTracker.mTableVersions, is(new long[]{7, 3, 0})); 162 } 163 164 private void refreshSync() throws InterruptedException { 165 mTracker.refreshVersionsAsync(); 166 drainTasks(); 167 } 168 169 @Test 170 public void refreshCheckTasks() throws Exception { 171 when(mRoomDatabase.query(anyString(), any(Object[].class))) 172 .thenReturn(mock(Cursor.class)); 173 mTracker.refreshVersionsAsync(); 174 mTracker.refreshVersionsAsync(); 175 verify(mTaskExecutorRule.getTaskExecutor()).executeOnDiskIO(mTracker.mRefreshRunnable); 176 drainTasks(); 177 178 reset(mTaskExecutorRule.getTaskExecutor()); 179 mTracker.refreshVersionsAsync(); 180 verify(mTaskExecutorRule.getTaskExecutor()).executeOnDiskIO(mTracker.mRefreshRunnable); 181 } 182 183 @Test 184 public void observe1Table() throws Exception { 185 LatchObserver observer = new LatchObserver(1, "a"); 186 mTracker.addObserver(observer); 187 setVersions(1, 0, 2, 1); 188 refreshSync(); 189 assertThat(observer.await(), is(true)); 190 assertThat(observer.getInvalidatedTables().size(), is(1)); 191 assertThat(observer.getInvalidatedTables(), hasItem("a")); 192 193 setVersions(3, 1); 194 observer.reset(1); 195 refreshSync(); 196 assertThat(observer.await(), is(false)); 197 198 setVersions(4, 0); 199 refreshSync(); 200 assertThat(observer.await(), is(true)); 201 assertThat(observer.getInvalidatedTables().size(), is(1)); 202 assertThat(observer.getInvalidatedTables(), hasItem("a")); 203 } 204 205 @Test 206 public void observe2Tables() throws Exception { 207 LatchObserver observer = new LatchObserver(1, "A", "B"); 208 mTracker.addObserver(observer); 209 setVersions(1, 0, 2, 1); 210 refreshSync(); 211 assertThat(observer.await(), is(true)); 212 assertThat(observer.getInvalidatedTables().size(), is(2)); 213 assertThat(observer.getInvalidatedTables(), hasItems("A", "B")); 214 215 setVersions(3, 1); 216 observer.reset(1); 217 refreshSync(); 218 assertThat(observer.await(), is(true)); 219 assertThat(observer.getInvalidatedTables().size(), is(1)); 220 assertThat(observer.getInvalidatedTables(), hasItem("B")); 221 222 setVersions(4, 0); 223 observer.reset(1); 224 refreshSync(); 225 assertThat(observer.await(), is(true)); 226 assertThat(observer.getInvalidatedTables().size(), is(1)); 227 assertThat(observer.getInvalidatedTables(), hasItem("A")); 228 229 observer.reset(1); 230 refreshSync(); 231 assertThat(observer.await(), is(false)); 232 } 233 234 @Test 235 public void locale() { 236 LatchObserver observer = new LatchObserver(1, "I"); 237 mTracker.addObserver(observer); 238 } 239 240 @Test 241 public void closedDb() { 242 doReturn(false).when(mRoomDatabase).isOpen(); 243 doThrow(new IllegalStateException("foo")).when(mOpenHelper).getWritableDatabase(); 244 mTracker.addObserver(new LatchObserver(1, "a", "b")); 245 mTracker.mRefreshRunnable.run(); 246 } 247 248 // @Test - disabled due to flakiness b/65257997 249 public void closedDbAfterOpen() throws InterruptedException { 250 setVersions(3, 1); 251 mTracker.addObserver(new LatchObserver(1, "a", "b")); 252 mTracker.syncTriggers(); 253 mTracker.mRefreshRunnable.run(); 254 doThrow(new SQLiteException("foo")).when(mRoomDatabase).query( 255 Mockito.eq(InvalidationTracker.SELECT_UPDATED_TABLES_SQL), 256 any(Object[].class)); 257 mTracker.mPendingRefresh.set(true); 258 mTracker.mRefreshRunnable.run(); 259 } 260 261 /** 262 * Key value pairs of VERSION, TABLE_ID 263 */ 264 private void setVersions(int... keyValuePairs) throws InterruptedException { 265 // mockito does not like multi-threaded access so before setting versions, make sure we 266 // sync background tasks. 267 drainTasks(); 268 Cursor cursor = createCursorWithValues(keyValuePairs); 269 doReturn(cursor).when(mRoomDatabase).query( 270 Mockito.eq(InvalidationTracker.SELECT_UPDATED_TABLES_SQL), 271 any(Object[].class) 272 ); 273 } 274 275 private Cursor createCursorWithValues(final int... keyValuePairs) { 276 Cursor cursor = mock(Cursor.class); 277 final AtomicInteger index = new AtomicInteger(-2); 278 when(cursor.moveToNext()).thenAnswer(new Answer<Boolean>() { 279 @Override 280 public Boolean answer(InvocationOnMock invocation) throws Throwable { 281 return index.addAndGet(2) < keyValuePairs.length; 282 } 283 }); 284 Answer<Integer> intAnswer = new Answer<Integer>() { 285 @Override 286 public Integer answer(InvocationOnMock invocation) throws Throwable { 287 return keyValuePairs[index.intValue() + (Integer) invocation.getArguments()[0]]; 288 } 289 }; 290 Answer<Long> longAnswer = new Answer<Long>() { 291 @Override 292 public Long answer(InvocationOnMock invocation) throws Throwable { 293 return (long) keyValuePairs[index.intValue() 294 + (Integer) invocation.getArguments()[0]]; 295 } 296 }; 297 when(cursor.getInt(anyInt())).thenAnswer(intAnswer); 298 when(cursor.getLong(anyInt())).thenAnswer(longAnswer); 299 return cursor; 300 } 301 302 static class LatchObserver extends InvalidationTracker.Observer { 303 private CountDownLatch mLatch; 304 private Set<String> mInvalidatedTables; 305 306 LatchObserver(int count, String... tableNames) { 307 super(tableNames); 308 mLatch = new CountDownLatch(count); 309 } 310 311 boolean await() throws InterruptedException { 312 return mLatch.await(3, TimeUnit.SECONDS); 313 } 314 315 @Override 316 public void onInvalidated(@NonNull Set<String> tables) { 317 mInvalidatedTables = tables; 318 mLatch.countDown(); 319 } 320 321 void reset(@SuppressWarnings("SameParameterValue") int count) { 322 mInvalidatedTables = null; 323 mLatch = new CountDownLatch(count); 324 } 325 326 Set<String> getInvalidatedTables() { 327 return mInvalidatedTables; 328 } 329 } 330 331 private static void forceGc() { 332 // Use a random index in the list to detect the garbage collection each time because 333 // .get() may accidentally trigger a strong reference during collection. 334 ArrayList<WeakReference<byte[]>> leak = new ArrayList<>(); 335 do { 336 WeakReference<byte[]> arr = new WeakReference<>(new byte[100]); 337 leak.add(arr); 338 } while (leak.get((int) (Math.random() * leak.size())).get() != null); 339 } 340 } 341