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