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.integration.testapp.test;
18 
19 import static org.hamcrest.CoreMatchers.is;
20 import static org.hamcrest.MatcherAssert.assertThat;
21 import static org.hamcrest.Matchers.empty;
22 import static org.hamcrest.core.IsCollectionContaining.hasItem;
23 import static org.junit.Assert.assertFalse;
24 import static org.junit.Assert.assertTrue;
25 
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.database.Cursor;
29 import android.database.sqlite.SQLiteDatabase;
30 import android.database.sqlite.SQLiteException;
31 
32 import androidx.room.Database;
33 import androidx.room.Room;
34 import androidx.room.RoomDatabase;
35 import androidx.room.integration.testapp.TestDatabase;
36 import androidx.room.integration.testapp.dao.ProductDao;
37 import androidx.room.integration.testapp.vo.Product;
38 import androidx.room.integration.testapp.vo.User;
39 import androidx.sqlite.db.SupportSQLiteDatabase;
40 import androidx.test.core.app.ApplicationProvider;
41 import androidx.test.ext.junit.runners.AndroidJUnit4;
42 import androidx.test.filters.MediumTest;
43 
44 import org.jspecify.annotations.NonNull;
45 import org.junit.Test;
46 import org.junit.runner.RunWith;
47 
48 import java.io.File;
49 import java.io.FileWriter;
50 import java.io.IOException;
51 import java.util.ArrayList;
52 import java.util.List;
53 
54 @MediumTest
55 @RunWith(AndroidJUnit4.class)
56 public class DatabaseCallbackTest {
57 
58     @Test
createAndOpen()59     public void createAndOpen() {
60         Context context = ApplicationProvider.getApplicationContext();
61         TestDatabaseCallback callback1 = new TestDatabaseCallback();
62         TestDatabase db1 = null;
63         TestDatabase db2 = null;
64         try {
65             db1 = Room.databaseBuilder(context, TestDatabase.class, "test")
66                     .addCallback(callback1)
67                     .build();
68             assertFalse(callback1.mCreated);
69             assertFalse(callback1.mOpened);
70             User user1 = TestUtil.createUser(3);
71             user1.setName("george");
72             db1.getUserDao().insert(user1);
73             assertTrue(callback1.mCreated);
74             assertTrue(callback1.mOpened);
75             TestDatabaseCallback callback2 = new TestDatabaseCallback();
76             db2 = Room.databaseBuilder(context, TestDatabase.class, "test")
77                     .addCallback(callback2)
78                     .build();
79             assertFalse(callback2.mCreated);
80             assertFalse(callback2.mOpened);
81             User user2 = db2.getUserDao().load(3);
82             assertThat(user2.getName(), is("george"));
83             assertFalse(callback2.mCreated); // Not called; already created by db1
84             assertTrue(callback2.mOpened);
85         } finally {
86             if (db1 != null) {
87                 db1.close();
88             }
89             if (db2 != null) {
90                 db2.close();
91             }
92             assertTrue(context.deleteDatabase("test"));
93         }
94     }
95 
96     @Test
writeOnCreate()97     public void writeOnCreate() {
98         Context context = ApplicationProvider.getApplicationContext();
99         TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
100                 .addCallback(new RoomDatabase.Callback() {
101                     @Override
102                     public void onCreate(@NonNull SupportSQLiteDatabase db) {
103                         Cursor cursor = null;
104                         try {
105                             cursor = db.query(
106                                     "SELECT name FROM sqlite_master WHERE type = 'table'");
107                             ArrayList<String> names = new ArrayList<>();
108                             while (cursor.moveToNext()) {
109                                 names.add(cursor.getString(0));
110                             }
111                             assertThat(names, hasItem("User"));
112                         } finally {
113                             if (cursor != null) {
114                                 cursor.close();
115                             }
116                         }
117                     }
118                 })
119                 .build();
120         List<Integer> ids = db.getUserDao().loadIds();
121         assertThat(ids, is(empty()));
122     }
123 
124     @Test
exceptionOnCreate()125     public void exceptionOnCreate() {
126         Context context = ApplicationProvider.getApplicationContext();
127         TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
128                 .addCallback(new RoomDatabase.Callback() {
129                     boolean mIsBadInsertDone;
130 
131                     @Override
132                     public void onCreate(@NonNull SupportSQLiteDatabase db) {
133                         if (!mIsBadInsertDone) {
134                             mIsBadInsertDone = true;
135                             db.insert("fake_table",
136                                     SQLiteDatabase.CONFLICT_NONE,
137                                     new ContentValues());
138                         }
139                     }
140                 })
141                 .build();
142 
143         try {
144             db.getUserDao().loadIds();
145         } catch (SQLiteException e) {
146             // Simulate user catching DB exceptions.
147         }
148 
149         // Should not throw an "IllegalStateException: attempt to re-open an already-closed"
150         List<Integer> ids = db.getUserDao().loadIds();
151         assertThat(ids, is(empty()));
152     }
153 
154     @Test
corruptExceptionOnCreate()155     public void corruptExceptionOnCreate() throws IOException {
156         Context context = ApplicationProvider.getApplicationContext();
157 
158         TestDatabaseCallback callback = new TestDatabaseCallback();
159 
160         // Create fake DB files that will cause a SQLiteDatabaseCorruptException: SQLITE_NOTADB.
161         String[] dbFiles = new String[]{"corrupted", "corrupted-shm", "corrupted-wal"};
162         for (String fileName : dbFiles) {
163             File dbFile = context.getDatabasePath(fileName);
164             try (FileWriter fileWriter = new FileWriter(dbFile)) {
165                 fileWriter.write(new char[]{'p', 'o', 'i', 's', 'o', 'n'});
166             }
167         }
168 
169         TestDatabase db = Room.databaseBuilder(context, TestDatabase.class, "corrupted")
170                 .addCallback(callback)
171                 .build();
172 
173         assertFalse(callback.mCreated);
174         assertFalse(callback.mOpened);
175 
176         // Should not throw a SQLiteDatabaseCorruptException, i.e. default onCorruption() was
177         // executed and DB file was re-created.
178         List<Integer> ids = db.getUserDao().loadIds();
179         db.close();
180         assertThat(ids, is(empty()));
181 
182         assertTrue(callback.mCreated);
183         assertTrue(callback.mOpened);
184     }
185 
186     @Test
onDestructiveMigration_calledOnUpgrade()187     public void onDestructiveMigration_calledOnUpgrade() {
188         Context context = ApplicationProvider.getApplicationContext();
189         context.deleteDatabase("products.db");
190         TestDatabaseCallback callback = new TestDatabaseCallback();
191         ProductsDatabase_v2 database = Room.databaseBuilder(
192                 context, ProductsDatabase_v2.class, "products.db")
193                 .createFromAsset("databases/products_v1.db")
194                 .addCallback(callback)
195                 .fallbackToDestructiveMigration(false)
196                 .build();
197 
198         assertFalse(callback.mDestructivelyMigrated);
199 
200         // Use the database to trigger the opening and migration of the database
201         ProductDao dao = database.getProductDao();
202         dao.countProducts();
203 
204         assertTrue(callback.mDestructivelyMigrated);
205         database.close();
206     }
207 
208     @Database(entities = Product.class, version = 2, exportSchema = false)
209     abstract static class ProductsDatabase_v2 extends RoomDatabase {
getProductDao()210         abstract ProductDao getProductDao();
211     }
212 
213     public static class TestDatabaseCallback extends RoomDatabase.Callback {
214 
215         boolean mCreated;
216         boolean mOpened;
217         boolean mDestructivelyMigrated;
218 
219         @Override
onCreate(@onNull SupportSQLiteDatabase db)220         public void onCreate(@NonNull SupportSQLiteDatabase db) {
221             mCreated = true;
222         }
223 
224         @Override
onOpen(@onNull SupportSQLiteDatabase db)225         public void onOpen(@NonNull SupportSQLiteDatabase db) {
226             mOpened = true;
227         }
228 
229         @Override
onDestructiveMigration(@onNull SupportSQLiteDatabase db)230         public void onDestructiveMigration(@NonNull SupportSQLiteDatabase db) {
231             mDestructivelyMigrated = true;
232         }
233     }
234 }
235