1 /*
2  * Copyright 2018 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 com.google.common.truth.Truth.assertThat;
20 import static com.google.common.truth.Truth.assertWithMessage;
21 
22 import android.content.Context;
23 
24 import androidx.arch.core.executor.ArchTaskExecutor;
25 import androidx.arch.core.executor.testing.CountingTaskExecutorRule;
26 import androidx.collection.SimpleArrayMap;
27 import androidx.lifecycle.LiveData;
28 import androidx.lifecycle.Observer;
29 import androidx.room.InvalidationTracker;
30 import androidx.room.Room;
31 import androidx.room.RoomDatabase;
32 import androidx.room.integration.testapp.ISampleDatabaseService;
33 import androidx.room.integration.testapp.SampleDatabaseService;
34 import androidx.room.integration.testapp.database.Customer;
35 import androidx.room.integration.testapp.database.Description;
36 import androidx.room.integration.testapp.database.Product;
37 import androidx.room.integration.testapp.database.SampleDatabase;
38 import androidx.room.integration.testapp.database.SampleFtsDatabase;
39 import androidx.test.core.app.ApplicationProvider;
40 import androidx.test.ext.junit.runners.AndroidJUnit4;
41 import androidx.test.filters.LargeTest;
42 import androidx.test.platform.app.InstrumentationRegistry;
43 import androidx.test.rule.ServiceTestRule;
44 
45 import org.jspecify.annotations.NonNull;
46 import org.junit.After;
47 import org.junit.Before;
48 import org.junit.Ignore;
49 import org.junit.Rule;
50 import org.junit.Test;
51 import org.junit.rules.TestName;
52 import org.junit.runner.RunWith;
53 
54 import java.util.ArrayList;
55 import java.util.List;
56 import java.util.Set;
57 import java.util.concurrent.CountDownLatch;
58 import java.util.concurrent.TimeUnit;
59 import java.util.concurrent.TimeoutException;
60 
61 @LargeTest
62 @RunWith(AndroidJUnit4.class)
63 public class MultiInstanceInvalidationTest {
64 
65     @Rule
66     public TestName testName = new TestName();
67 
68     private static final Customer CUSTOMER_1 = new Customer();
69     private static final Customer CUSTOMER_2 = new Customer();
70 
71     static {
72         CUSTOMER_1.setId(1);
73         CUSTOMER_1.setName("John");
74         CUSTOMER_1.setLastName("Doe");
75         CUSTOMER_2.setId(2);
76         CUSTOMER_2.setName("Jane");
77         CUSTOMER_2.setLastName("Doe");
78     }
79 
80     @Rule
81     public final ServiceTestRule serviceRule = new ServiceTestRule();
82 
83     @Rule
84     public final CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule();
85 
86     private ISampleDatabaseService mService;
87 
88     private String mDatabaseName;
89 
90     private final ArrayList<RoomDatabase> mDatabases = new ArrayList<>();
91     private final SimpleArrayMap<LiveData<List<Customer>>, Observer<List<Customer>>> mObservers =
92             new SimpleArrayMap<>();
93 
94     @Before
setUp()95     public void setUp() {
96         final Context context = ApplicationProvider.getApplicationContext();
97         // use a separate database file for each test because we are not fully capable of closing
98         // and deleting a database connection in a multi-process setup
99         mDatabaseName = "multi-process-" + testName.getMethodName() + ".db";
100         context.deleteDatabase(mDatabaseName);
101     }
102 
103     @After
tearDown()104     public void tearDown() throws InterruptedException, TimeoutException {
105         for (int i = 0, size = mObservers.size(); i < size; i++) {
106             final LiveData<List<Customer>> liveData = mObservers.keyAt(i);
107             final Observer<List<Customer>> observer = mObservers.valueAt(i);
108             InstrumentationRegistry.getInstrumentation().runOnMainSync(() ->
109                     liveData.removeObserver(observer));
110         }
111         if (mService != null) {
112             serviceRule.unbindService();
113         }
114         for (RoomDatabase db : mDatabases) {
115             db.close();
116         }
117         mExecutorRule.drainTasks(3, TimeUnit.SECONDS);
118         assertWithMessage("Executor isIdle")
119                 .that(mExecutorRule.isIdle())
120                 .isTrue();
121     }
122 
123     @Ignore("Due to b/366106924")
124     @Test
invalidateInAnotherInstance()125     public void invalidateInAnotherInstance() throws Exception {
126         final SampleDatabase db1 = openDatabase(true);
127         final SampleDatabase db2 = openDatabase(true);
128 
129         final CountDownLatch invalidated1 = prepareTableObserver(db1);
130 
131         db2.getCustomerDao().insert(CUSTOMER_1);
132 
133         assertWithMessage("Observer invalidation")
134                 .that(invalidated1.await(3, TimeUnit.SECONDS))
135                 .isTrue();
136     }
137 
138     @Test
invalidateInAnotherInstanceFts()139     public void invalidateInAnotherInstanceFts() throws Exception {
140         final SampleFtsDatabase db1 = openFtsDatabase(true);
141         final SampleFtsDatabase db2 = openFtsDatabase(true);
142 
143         Product theProduct = new Product(1, "Candy");
144         db2.getProductDao().insert(theProduct);
145         db2.getProductDao().addDescription(new Description(1, "Delicious candy."));
146 
147         final CountDownLatch changed = new CountDownLatch(1);
148         db1.getInvalidationTracker().addObserver(new InvalidationTracker.Observer("Description") {
149             @Override
150             public void onInvalidated(@NonNull Set<String> tables) {
151                 changed.countDown();
152             }
153         });
154 
155         db2.getProductDao().addDescription(new Description(1, "Wonderful candy."));
156 
157         assertWithMessage("Observer invalidation")
158                 .that(changed.await(3, TimeUnit.SECONDS))
159                 .isTrue();
160 
161         List<Product> result = db1.getProductDao().getProductsWithDescription("candy");
162         assertThat(result).hasSize(1);
163         assertThat(result).containsExactly(theProduct);
164     }
165 
166     @Test
invalidationInAnotherInstance_noMultiInstanceInvalidation()167     public void invalidationInAnotherInstance_noMultiInstanceInvalidation() throws Exception {
168         final SampleDatabase db1 = openDatabase(false);
169         final SampleDatabase db2 = openDatabase(false);
170 
171         final CountDownLatch invalidated1 = prepareTableObserver(db1);
172         final CountDownLatch invalidated2 = prepareTableObserver(db2);
173 
174         db2.getCustomerDao().insert(CUSTOMER_1);
175 
176         assertWithMessage("Observer invalidation")
177                 .that(invalidated1.await(200, TimeUnit.MILLISECONDS))
178                 .isFalse();
179 
180         assertWithMessage("Observer invalidation")
181                 .that(invalidated2.await(3, TimeUnit.SECONDS))
182                 .isTrue();
183     }
184 
185     @Test
invalidationInAnotherInstance_mixed()186     public void invalidationInAnotherInstance_mixed() throws Exception {
187         final SampleDatabase db1 = openDatabase(false);
188         final SampleDatabase db2 = openDatabase(true); // Enabled only on one side
189 
190         final CountDownLatch invalidated1 = prepareTableObserver(db1);
191         final CountDownLatch invalidated2 = prepareTableObserver(db2);
192 
193         db2.getCustomerDao().insert(CUSTOMER_1);
194 
195         assertWithMessage("Observer invalidation")
196                 .that(invalidated1.await(200, TimeUnit.MILLISECONDS))
197                 .isFalse();
198 
199         assertWithMessage("Observer invalidation")
200                 .that(invalidated2.await(3, TimeUnit.SECONDS))
201                 .isTrue();
202     }
203 
204     @Test
205     @Ignore // Flaky test, b/363246309.
invalidationInAnotherInstance_closed()206     public void invalidationInAnotherInstance_closed() throws Exception {
207         final SampleDatabase db1 = openDatabase(true);
208         final SampleDatabase db2 = openDatabase(true);
209         final SampleDatabase db3 = openDatabase(true);
210 
211         final CountDownLatch invalidated1 = prepareTableObserver(db1);
212         final CountDownLatch invalidated2 = prepareTableObserver(db2);
213         final CountDownLatch invalidated3 = prepareTableObserver(db3);
214 
215         db3.close();
216         db2.getCustomerDao().insert(CUSTOMER_1);
217 
218         assertWithMessage("Observer invalidation")
219                 .that(invalidated1.await(3, TimeUnit.SECONDS))
220                 .isTrue();
221         assertWithMessage("Observer invalidation")
222                 .that(invalidated2.await(3, TimeUnit.SECONDS))
223                 .isTrue();
224         assertWithMessage("Observer invalidation")
225                 .that(invalidated3.await(200, TimeUnit.MILLISECONDS))
226                 .isFalse();
227     }
228 
229     @Ignore("Due to b/366106924")
230     @Test
invalidatedByAnotherProcess()231     public void invalidatedByAnotherProcess() throws Exception {
232         bindTestService();
233 
234         final SampleDatabase db = openDatabase(true);
235         assertThat(db.getCustomerDao().countCustomers()).isEqualTo(0);
236 
237         final CountDownLatch invalidated = prepareTableObserver(db);
238 
239         mService.insertCustomer(CUSTOMER_1.getId(), CUSTOMER_1.getName(), CUSTOMER_1.getLastName());
240 
241         assertWithMessage("Observer invalidation")
242                 .that(invalidated.await(3, TimeUnit.SECONDS))
243                 .isTrue();
244     }
245 
246     @Test
invalidateAnotherProcess()247     public void invalidateAnotherProcess() throws Exception {
248         bindTestService();
249 
250         final SampleDatabase db = openDatabase(true);
251 
252         ArchTaskExecutor.getIOThreadExecutor().execute(
253                 () -> {
254                     // This sleep is needed to wait for the IPC of waitForCustomer() to
255                     // reach the other process and to let the observer in the other process
256                     // subscribe to invalidation. If we insert before the other process registers
257                     // the observer then we'll miss the observer.
258                     try {
259                         Thread.sleep(200);
260                     } catch (InterruptedException e) {
261                         // no-op
262                     }
263                     db.getCustomerDao().insert(CUSTOMER_1);
264                 }
265         );
266 
267         boolean awaitResult = mService.waitForCustomer(
268                 CUSTOMER_1.getId(), CUSTOMER_1.getName(), CUSTOMER_1.getLastName());
269         assertWithMessage(
270                 "Observer invalidation in another process")
271                 .that(awaitResult)
272                 .isTrue();
273     }
274 
openDatabase(boolean multiInstanceInvalidation)275     private SampleDatabase openDatabase(boolean multiInstanceInvalidation) {
276         final Context context = ApplicationProvider.getApplicationContext();
277         final RoomDatabase.Builder<SampleDatabase> builder = Room
278                 .databaseBuilder(context, SampleDatabase.class, mDatabaseName);
279         if (multiInstanceInvalidation) {
280             builder.enableMultiInstanceInvalidation();
281         }
282         final SampleDatabase db = builder.build();
283         mDatabases.add(db);
284         return db;
285     }
286 
openFtsDatabase(boolean multiInstanceInvalidation)287     private SampleFtsDatabase openFtsDatabase(boolean multiInstanceInvalidation) {
288         final Context context = ApplicationProvider.getApplicationContext();
289         final RoomDatabase.Builder<SampleFtsDatabase> builder = Room
290                 .databaseBuilder(context, SampleFtsDatabase.class, mDatabaseName);
291         if (multiInstanceInvalidation) {
292             builder.enableMultiInstanceInvalidation();
293         }
294         final SampleFtsDatabase db = builder.build();
295         mDatabases.add(db);
296         return db;
297     }
298 
bindTestService()299     private void bindTestService() throws TimeoutException {
300         final Context context = ApplicationProvider.getApplicationContext();
301         mService = ISampleDatabaseService.Stub.asInterface(
302                 serviceRule.bindService(SampleDatabaseService.intentFor(
303                         context,
304                         mDatabaseName
305                 )));
306     }
307 
prepareTableObserver(SampleDatabase db)308     private CountDownLatch prepareTableObserver(SampleDatabase db) {
309         final CountDownLatch invalidated = new CountDownLatch(1);
310         db.getInvalidationTracker()
311                 .addObserver(new InvalidationTracker.Observer("Customer", "Product") {
312                     @Override
313                     public void onInvalidated(@NonNull Set<String> tables) {
314                         assertThat(tables).hasSize(1);
315                         assertThat(tables).contains("Customer");
316                         invalidated.countDown();
317                     }
318                 });
319         return invalidated;
320     }
321 }
322