1 /* 2 * Copyright 2020 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.integration.testapp.test; 18 19 import static org.junit.Assert.assertEquals; 20 import static org.junit.Assert.assertFalse; 21 import static org.junit.Assert.assertNotNull; 22 import static org.junit.Assert.assertNull; 23 import static org.junit.Assert.assertTrue; 24 25 import android.content.Context; 26 import android.database.Cursor; 27 28 import androidx.arch.core.executor.testing.CountingTaskExecutorRule; 29 import androidx.lifecycle.LiveData; 30 import androidx.lifecycle.testing.TestLifecycleOwner; 31 import androidx.room.InvalidationTracker; 32 import androidx.room.Room; 33 import androidx.room.RoomDatabase; 34 import androidx.room.integration.testapp.TestDatabase; 35 import androidx.room.integration.testapp.dao.UserDao; 36 import androidx.room.integration.testapp.vo.User; 37 import androidx.sqlite.db.SupportSQLiteDatabase; 38 import androidx.test.core.app.ApplicationProvider; 39 import androidx.test.filters.MediumTest; 40 import androidx.testutils.AssertionsKt; 41 42 import org.jspecify.annotations.NonNull; 43 import org.junit.After; 44 import org.junit.Before; 45 import org.junit.Ignore; 46 import org.junit.Rule; 47 import org.junit.Test; 48 49 import java.util.Set; 50 import java.util.concurrent.TimeUnit; 51 import java.util.concurrent.TimeoutException; 52 import java.util.concurrent.atomic.AtomicInteger; 53 54 // TODO: Consolidate with AutoClosingDatabaseTest that has access to internal APIs. 55 public class AutoClosingRoomOpenHelperTest { 56 @Rule 57 public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule(); 58 private UserDao mUserDao; 59 private TestDatabase mDb; 60 private final DatabaseCallbackTest.TestDatabaseCallback mCallback = 61 new DatabaseCallbackTest.TestDatabaseCallback(); 62 63 @Before createDb()64 public void createDb() throws TimeoutException, InterruptedException { 65 Context context = ApplicationProvider.getApplicationContext(); 66 context.deleteDatabase("testDb"); 67 mDb = Room.databaseBuilder(context, TestDatabase.class, "testDb") 68 .setAutoCloseTimeout(10, TimeUnit.MILLISECONDS) 69 .addCallback(mCallback).build(); 70 mUserDao = mDb.getUserDao(); 71 } 72 73 @After cleanUp()74 public void cleanUp() throws Exception { 75 drain(); 76 mDb.close(); 77 } 78 79 @Test 80 @MediumTest inactiveConnection_shouldAutoClose()81 public void inactiveConnection_shouldAutoClose() throws Exception { 82 assertFalse(mCallback.mOpened); 83 User user = TestUtil.createUser(1); 84 user.setName("bob"); 85 mUserDao.insert(user); 86 assertTrue(mCallback.mOpened); 87 88 Thread.sleep(100); 89 // Connection should be auto closed here 90 91 User readUser = mUserDao.load(1); 92 assertEquals(readUser.getName(), user.getName()); 93 } 94 95 @Test 96 @MediumTest slowTransaction_keepsDbAlive()97 public void slowTransaction_keepsDbAlive() throws Exception { 98 assertFalse(mCallback.mOpened); 99 100 User user = TestUtil.createUser(1); 101 user.setName("bob"); 102 mUserDao.insert(user); 103 assertTrue(mCallback.mOpened); 104 Thread.sleep(30); 105 mUserDao.load(1); 106 // Connection should be auto closed here 107 mDb.runInTransaction( 108 () -> { 109 try { 110 Thread.sleep(100); 111 } catch (InterruptedException ignored) { } 112 // Connection would've been auto closed here 113 } 114 ); 115 116 Thread.sleep(100); 117 // Connection should be auto closed here 118 } 119 120 @Test 121 @MediumTest slowCursorClosing_keepsDbAlive()122 public void slowCursorClosing_keepsDbAlive() throws Exception { 123 User user = TestUtil.createUser(1); 124 user.setName("bob"); 125 mUserDao.insert(user); 126 mUserDao.load(1); 127 128 Cursor cursor = mDb.query("select * from user", null); 129 130 Thread.sleep(100); 131 132 cursor.close(); 133 134 Thread.sleep(100); 135 // Connection should be auto closed here 136 } 137 138 @Test 139 @MediumTest autoClosedConnection_canReopen()140 public void autoClosedConnection_canReopen() throws Exception { 141 User user1 = TestUtil.createUser(1); 142 user1.setName("bob"); 143 mUserDao.insert(user1); 144 145 Thread.sleep(100); 146 // Connection should be auto closed here 147 148 User user2 = TestUtil.createUser(2); 149 user2.setName("bob2"); 150 mUserDao.insert(user2); 151 Thread.sleep(100); 152 // Connection should be auto closed here 153 } 154 155 @Test 156 @MediumTest liveDataTriggers_shouldApplyOnReopen()157 public void liveDataTriggers_shouldApplyOnReopen() throws Exception { 158 LiveData<Boolean> adminLiveData = mUserDao.isAdminLiveData(1); 159 160 final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner(); 161 final TestObserver<Boolean> observer = new AutoClosingRoomOpenHelperTest 162 .MyTestObserver<>(); 163 TestUtil.observeOnMainThread(adminLiveData, lifecycleOwner, observer); 164 assertNull(observer.get()); 165 166 User user = TestUtil.createUser(1); 167 user.setAdmin(true); 168 mUserDao.insert(user); 169 170 assertNotNull(observer.get()); 171 assertTrue(observer.get()); 172 173 user.setAdmin(false); 174 mUserDao.insertOrReplace(user); 175 assertNotNull(observer.get()); 176 assertFalse(observer.get()); 177 178 Thread.sleep(100); 179 // Connection should be auto closed here 180 181 user.setAdmin(true); 182 mUserDao.insertOrReplace(user); 183 assertNotNull(observer.get()); 184 assertTrue(observer.get()); 185 } 186 187 @Test 188 @MediumTest testCanExecSqlInCallback()189 public void testCanExecSqlInCallback() throws Exception { 190 Context context = ApplicationProvider.getApplicationContext(); 191 context.deleteDatabase("testDb2"); 192 TestDatabase db = Room.databaseBuilder(context, TestDatabase.class, "testDb2") 193 .setAutoCloseTimeout(10, TimeUnit.MILLISECONDS) 194 .addCallback(new ExecSqlInCallback()) 195 .build(); 196 197 db.getUserDao().insert(TestUtil.createUser(1)); 198 199 db.close(); 200 } 201 202 @Test testManuallyRoomDatabaseClose()203 public void testManuallyRoomDatabaseClose() throws Exception { 204 Context context = ApplicationProvider.getApplicationContext(); 205 // Create a new db since the other one is cleared in the @After 206 TestDatabase testDatabase = Room.databaseBuilder(context, TestDatabase.class, "testDb2") 207 .setAutoCloseTimeout(10, TimeUnit.MILLISECONDS) 208 .addCallback(new ExecSqlInCallback()) 209 .build(); 210 211 testDatabase.close(); 212 213 // We shouldn't be able to do anything with the database now... 214 AssertionsKt.assertThrows(IllegalStateException.class, () -> { 215 testDatabase.getUserDao().count(); 216 }).hasMessageThat().contains("closed"); 217 218 assertFalse(testDatabase.isOpen()); 219 220 assertFalse(testDatabase.isOpen()); 221 TestDatabase testDatabase2 = Room.databaseBuilder(context, TestDatabase.class, "testDb2") 222 .setAutoCloseTimeout(10, TimeUnit.MILLISECONDS) 223 .addCallback(new ExecSqlInCallback()) 224 .build(); 225 testDatabase2.getUserDao().count(); // db should open now 226 testDatabase2.close(); 227 assertFalse(testDatabase.isOpen()); 228 } 229 230 @Test testManuallyOpenHelperClose()231 public void testManuallyOpenHelperClose() throws Exception { 232 Context context = ApplicationProvider.getApplicationContext(); 233 // Create a new db since the other one is cleared in the @After 234 TestDatabase testDatabase = Room.databaseBuilder(context, TestDatabase.class, "testDb2") 235 .setAutoCloseTimeout(10, TimeUnit.MILLISECONDS) 236 .addCallback(new ExecSqlInCallback()) 237 .build(); 238 239 testDatabase.getOpenHelper().close(); 240 241 // We shouldn't be able to do anything with the database now... 242 AssertionsKt.assertThrows(IllegalStateException.class, () -> { 243 testDatabase.getUserDao().count(); 244 }).hasMessageThat().contains("closed"); 245 246 assertFalse(testDatabase.isOpen()); 247 TestDatabase testDatabase2 = Room.databaseBuilder(context, TestDatabase.class, "testDb2") 248 .setAutoCloseTimeout(10, TimeUnit.MILLISECONDS) 249 .addCallback(new ExecSqlInCallback()) 250 .build(); 251 testDatabase2.getUserDao().count(); // db should open now 252 testDatabase2.getOpenHelper().close(); 253 assertFalse(testDatabase.isOpen()); 254 } 255 256 // TODO(336671494): broken test 257 @Ignore 258 @Test 259 @MediumTest invalidationObserver_isCalledOnEachInvalidation()260 public void invalidationObserver_isCalledOnEachInvalidation() 261 throws TimeoutException, InterruptedException { 262 AtomicInteger invalidationCount = new AtomicInteger(0); 263 264 UserTableObserver userTableObserver = 265 new UserTableObserver(invalidationCount::getAndIncrement); 266 267 mDb.getInvalidationTracker().addObserver(userTableObserver); 268 269 mUserDao.insert(TestUtil.createUser(1)); 270 271 drain(); 272 assertEquals(1, invalidationCount.get()); 273 274 User user1 = TestUtil.createUser(1); 275 user1.setAge(123); 276 mUserDao.insertOrReplace(user1); 277 278 drain(); 279 assertEquals(2, invalidationCount.get()); 280 281 Thread.sleep(15); 282 // Connection should be closed now 283 284 mUserDao.insert(TestUtil.createUser(2)); 285 286 drain(); 287 assertEquals(3, invalidationCount.get()); 288 } 289 290 // TODO(372946311): Broken test 291 @Ignore 292 @Test 293 @MediumTest invalidationObserver_canRequeryDb()294 public void invalidationObserver_canRequeryDb() throws TimeoutException, InterruptedException { 295 Context context = ApplicationProvider.getApplicationContext(); 296 297 context.deleteDatabase("testDb2"); 298 TestDatabase db = Room.databaseBuilder(context, TestDatabase.class, "testDb2") 299 // create contention for callback 300 .setAutoCloseTimeout(0, TimeUnit.MILLISECONDS) 301 .addCallback(mCallback).build(); 302 303 AtomicInteger userCount = new AtomicInteger(0); 304 305 UserTableObserver userTableObserver = new UserTableObserver( 306 () -> userCount.set(db.getUserDao().count())); 307 308 db.getInvalidationTracker().addObserver(userTableObserver); 309 310 db.getUserDao().insert(TestUtil.createUser(1)); 311 db.getUserDao().insert(TestUtil.createUser(2)); 312 db.getUserDao().insert(TestUtil.createUser(3)); 313 db.getUserDao().insert(TestUtil.createUser(4)); 314 db.getUserDao().insert(TestUtil.createUser(5)); 315 db.getUserDao().insert(TestUtil.createUser(6)); 316 db.getUserDao().insert(TestUtil.createUser(7)); 317 318 drain(); 319 assertEquals(7, userCount.get()); 320 db.close(); 321 } 322 drain()323 private void drain() throws TimeoutException, InterruptedException { 324 mExecutorRule.drainTasks(1, TimeUnit.MINUTES); 325 } 326 327 private class MyTestObserver<T> extends TestObserver<T> { 328 @Override drain()329 protected void drain() throws TimeoutException, InterruptedException { 330 AutoClosingRoomOpenHelperTest.this.drain(); 331 } 332 } 333 334 private static class ExecSqlInCallback extends RoomDatabase.Callback { 335 @Override onOpen(@onNull SupportSQLiteDatabase db)336 public void onOpen(@NonNull SupportSQLiteDatabase db) { 337 db.query("select * from user").close(); 338 } 339 } 340 341 private static class UserTableObserver extends InvalidationTracker.Observer { 342 343 private final Runnable mInvalidationCallback; 344 UserTableObserver(Runnable invalidationCallback)345 UserTableObserver(Runnable invalidationCallback) { 346 super("user"); 347 mInvalidationCallback = invalidationCallback; 348 } 349 350 @Override onInvalidated(@onNull Set<String> tables)351 public void onInvalidated(@NonNull Set<String> tables) { 352 mInvalidationCallback.run(); 353 } 354 } 355 } 356