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