1/* 2 * Copyright (C) 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 */ 16package androidx.room.integration.testapp.migration; 17 18import static org.hamcrest.CoreMatchers.is; 19import static org.hamcrest.CoreMatchers.notNullValue; 20import static org.hamcrest.MatcherAssert.assertThat; 21 22import android.content.ContentValues; 23import android.content.Context; 24import android.database.Cursor; 25import android.database.sqlite.SQLiteDatabase; 26import android.support.annotation.NonNull; 27import android.support.test.InstrumentationRegistry; 28import android.support.test.filters.SdkSuppress; 29import android.support.test.filters.SmallTest; 30import android.support.test.runner.AndroidJUnit4; 31import android.text.TextUtils; 32 33import androidx.arch.core.executor.testing.CountingTaskExecutorRule; 34import androidx.lifecycle.LiveData; 35import androidx.room.ColumnInfo; 36import androidx.room.Dao; 37import androidx.room.Database; 38import androidx.room.Entity; 39import androidx.room.Insert; 40import androidx.room.OnConflictStrategy; 41import androidx.room.PrimaryKey; 42import androidx.room.Query; 43import androidx.room.Room; 44import androidx.room.RoomDatabase; 45import androidx.room.migration.Migration; 46import androidx.sqlite.db.SupportSQLiteDatabase; 47 48import org.junit.After; 49import org.junit.Before; 50import org.junit.Rule; 51import org.junit.Test; 52import org.junit.runner.RunWith; 53 54import java.io.File; 55import java.io.FileOutputStream; 56import java.io.IOException; 57import java.io.InputStream; 58import java.io.OutputStream; 59import java.util.List; 60import java.util.UUID; 61import java.util.concurrent.TimeUnit; 62import java.util.concurrent.TimeoutException; 63import java.util.concurrent.atomic.AtomicInteger; 64 65/** 66 * reproduces b/78359448 67 */ 68@SdkSuppress(minSdkVersion = 24) 69@RunWith(AndroidJUnit4.class) 70@SmallTest 71public class JournalDbPostMigrationTest { 72 @Rule 73 public CountingTaskExecutorRule executorRule = new CountingTaskExecutorRule(); 74 private static final String DB_NAME = "journal-db"; 75 private AtomicInteger mOnOpenCount = new AtomicInteger(0); 76 private AtomicInteger mOnCreateCount = new AtomicInteger(0); 77 78 @Entity 79 public static class User { 80 @PrimaryKey(autoGenerate = true) 81 public int uid; 82 83 @ColumnInfo(name = "first_name") 84 public String firstName; 85 86 @ColumnInfo(name = "last_name") 87 public String lastName; 88 89 @ColumnInfo(name = "address") 90 public String address; 91 92 @ColumnInfo(name = "age") 93 public int age; 94 } 95 96 @Dao 97 public interface UserDao { 98 @Query("SELECT * FROM user") 99 List<User> getAll(); 100 101 @Query("SELECT * FROM user WHERE uid = :uid LIMIT 1") 102 LiveData<User> getUser(int uid); 103 104 @Insert(onConflict = OnConflictStrategy.REPLACE) 105 void insert(User user); 106 } 107 108 @Database(entities = {User.class}, version = 3, exportSchema = false) 109 public abstract static class AppDatabase extends RoomDatabase { 110 public abstract UserDao userDao(); 111 } 112 113 private static Migration sMigrationV1toV2 = new Migration(1, 2) { 114 @Override 115 public void migrate(@NonNull SupportSQLiteDatabase database) { 116 database.execSQL("ALTER TABLE user ADD COLUMN `address` TEXT"); 117 Cursor cursor = database.query("SELECT * FROM user"); 118 while (cursor.moveToNext()) { 119 String authCode = cursor.getString(cursor.getColumnIndex("address")); 120 if (TextUtils.isEmpty(authCode)) { 121 int id = cursor.getInt(cursor.getColumnIndex("uid")); 122 ContentValues values = new ContentValues(); 123 values.put("address", UUID.randomUUID().toString()); 124 database.update( 125 "user", SQLiteDatabase.CONFLICT_IGNORE, values, 126 "uid = " + id, null); 127 } 128 } 129 } 130 }; 131 private static Migration sMigrationV2toV3 = new Migration(2, 3) { 132 @Override 133 public void migrate(@NonNull SupportSQLiteDatabase database) { 134 database.execSQL("ALTER TABLE user ADD COLUMN `age` INTEGER NOT NULL DEFAULT 0"); 135 } 136 }; 137 138 private AppDatabase getDb() { 139 return Room.databaseBuilder(InstrumentationRegistry.getTargetContext(), 140 AppDatabase.class, "journal-db") 141 .addMigrations(sMigrationV1toV2, sMigrationV2toV3) 142 .addCallback(new RoomDatabase.Callback() { 143 @Override 144 public void onCreate(@NonNull SupportSQLiteDatabase db) { 145 mOnCreateCount.incrementAndGet(); 146 } 147 148 @Override 149 public void onOpen(@NonNull SupportSQLiteDatabase db) { 150 mOnOpenCount.incrementAndGet(); 151 } 152 }) 153 .setJournalMode(RoomDatabase.JournalMode.TRUNCATE).build(); 154 } 155 156 private void copyAsset(String path, File outFile) throws IOException { 157 byte[] buffer = new byte[1024]; 158 int length; 159 InputStream fis = InstrumentationRegistry.getContext().getAssets().open(path); 160 OutputStream out = new FileOutputStream(outFile, false); 161 //noinspection TryFinallyCanBeTryWithResources 162 try { 163 while ((length = fis.read(buffer)) > 0) { 164 out.write(buffer, 0, length); 165 } 166 } finally { 167 fis.close(); 168 out.close(); 169 } 170 } 171 172 @After 173 public void deleteDb() { 174 InstrumentationRegistry.getTargetContext().deleteDatabase(DB_NAME); 175 } 176 177 @Before 178 public void copyDbFromAssets() throws IOException { 179 Context testContext = InstrumentationRegistry.getContext(); 180 Context targetContext = InstrumentationRegistry.getTargetContext(); 181 String[] walModificationDbs = testContext.getAssets().list(DB_NAME); 182 File databasePath = targetContext.getDatabasePath(DB_NAME); 183 for (String file : walModificationDbs) { 184 copyAsset(DB_NAME + "/" + file, 185 new File(databasePath.getParentFile(), file)); 186 } 187 } 188 189 @Test 190 public void migrateAndRead() { 191 List<User> users = getDb().userDao().getAll(); 192 assertThat(users.size(), is(10)); 193 } 194 195 @Test 196 public void checkCallbacks() { 197 // trigger db open 198 getDb().userDao().getAll(); 199 assertThat(mOnOpenCount.get(), is(1)); 200 assertThat(mOnCreateCount.get(), is(0)); 201 } 202 203 @Test 204 public void liveDataPostMigrations() throws TimeoutException, InterruptedException { 205 UserDao dao = getDb().userDao(); 206 LiveData<User> liveData = dao.getUser(3); 207 InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> 208 liveData.observeForever(user -> { 209 }) 210 ); 211 executorRule.drainTasks(1, TimeUnit.MINUTES); 212 assertThat(liveData.getValue(), notNullValue()); 213 // update user 214 User user = new User(); 215 user.uid = 3; 216 user.firstName = "foo-bar"; 217 dao.insert(user); 218 executorRule.drainTasks(1, TimeUnit.MINUTES); 219 //noinspection ConstantConditions 220 assertThat(liveData.getValue().firstName, is("foo-bar")); 221 } 222} 223