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