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  */
16 
17 package android.database.sqlite.cts;
18 
19 import android.content.Context;
20 import android.database.DatabaseUtils;
21 import android.database.sqlite.SQLiteCompatibilityWalFlags;
22 import android.database.sqlite.SQLiteDatabase;
23 import android.test.AndroidTestCase;
24 import android.util.Log;
25 
26 import com.android.compatibility.common.util.SystemUtil;
27 
28 import java.io.File;
29 import java.io.FileInputStream;
30 import java.io.FileNotFoundException;
31 import java.io.FileOutputStream;
32 import java.io.InputStream;
33 import java.io.OutputStream;
34 
35 public class SQLiteWalTest extends AndroidTestCase {
36     private static final String TAG = "SQLiteWalTest";
37 
38     private static final String DB_FILE = "SQLiteWalTest.db";
39     private static final String SHM_SUFFIX = "-shm";
40     private static final String WAL_SUFFIX = "-wal";
41 
42     private static final String BACKUP_SUFFIX = ".bak";
43 
44     private static final String SQLITE_COMPATIBILITY_WAL_FLAGS = "sqlite_compatibility_wal_flags";
45     private static final String TRUNCATE_SIZE_KEY = "truncate_size";
46 
47     @Override
setUp()48     protected void setUp() throws Exception {
49         super.setUp();
50     }
51 
52     @Override
tearDown()53     protected void tearDown() throws Exception {
54         SystemUtil.runShellCommand("settings delete global " + SQLITE_COMPATIBILITY_WAL_FLAGS);
55 
56         super.tearDown();
57     }
58 
setCompatibilityWalFlags(String value)59     private void setCompatibilityWalFlags(String value) {
60         // settings put global sqlite_compatibility_wal_flags truncate_size=0
61 
62         SystemUtil.runShellCommand("settings put global " + SQLITE_COMPATIBILITY_WAL_FLAGS + " "
63                 + value);
64     }
65 
copyFile(String from, String to)66     private void copyFile(String from, String to) throws Exception {
67         (new File(to)).delete();
68 
69         try (InputStream in = new FileInputStream(from)) {
70             try (OutputStream out = new FileOutputStream(to)) {
71                 byte[] buf = new byte[1024 * 32];
72                 int len;
73                 while ((len = in.read(buf)) > 0) {
74                     out.write(buf, 0, len);
75                 }
76             }
77         }
78     }
79 
backupFile(String from)80     private void backupFile(String from) throws Exception {
81         copyFile(from, from + BACKUP_SUFFIX);
82     }
83 
restoreFile(String from)84     private void restoreFile(String from) throws Exception {
85         copyFile(from + BACKUP_SUFFIX, from);
86     }
87 
openDatabase()88     private SQLiteDatabase openDatabase() {
89         SQLiteDatabase db = mContext.openOrCreateDatabase(
90                 DB_FILE, Context.MODE_ENABLE_WRITE_AHEAD_LOGGING, null);
91         db.execSQL("PRAGMA synchronous=FULL");
92         return db;
93     }
94 
assertTestTableExists(SQLiteDatabase db)95     private void assertTestTableExists(SQLiteDatabase db) {
96         assertEquals(1, DatabaseUtils.longForQuery(db, "SELECT count(*) FROM test", null));
97     }
98 
prepareDatabase()99     private SQLiteDatabase prepareDatabase() {
100         SQLiteDatabase db = openDatabase();
101 
102         db.execSQL("CREATE TABLE test (column TEXT);");
103         db.execSQL("INSERT INTO test (column) VALUES ("
104                 + "'12345678901234567890123456789012345678901234567890')");
105 
106         // Make sure all the 3 files exist and are bigger than 0 bytes.
107         assertTrue((new File(db.getPath())).exists());
108         assertTrue((new File(db.getPath() + SHM_SUFFIX)).exists());
109         assertTrue((new File(db.getPath() + WAL_SUFFIX)).exists());
110 
111         assertTrue((new File(db.getPath())).length() > 0);
112         assertTrue((new File(db.getPath() + SHM_SUFFIX)).length() > 0);
113         assertTrue((new File(db.getPath() + WAL_SUFFIX)).length() > 0);
114 
115         // Make sure the table has 1 row.
116         assertTestTableExists(db);
117 
118         return db;
119     }
120 
121     /**
122      * Open a WAL database when the WAL file size is bigger than the threshold, and make sure
123      * the file gets truncated.
124      */
testWalTruncate()125     public void testWalTruncate() throws Exception {
126         mContext.deleteDatabase(DB_FILE);
127 
128         // Truncate the WAL file if it's bigger than 1 byte.
129         setCompatibilityWalFlags(TRUNCATE_SIZE_KEY + "=1");
130         SQLiteCompatibilityWalFlags.reset();
131 
132         SQLiteDatabase db = doOperation("testWalTruncate");
133 
134         // Make sure the WAL file is truncated into 0 bytes.
135         assertEquals(0, (new File(db.getPath() + WAL_SUFFIX)).length());
136     }
137 
138     /**
139      * Open a WAL database when the WAL file size is smaller than the threshold, and make sure
140      * the file does *not* get truncated.
141      */
testWalNoTruncate()142     public void testWalNoTruncate() throws Exception {
143         mContext.deleteDatabase(DB_FILE);
144 
145         setCompatibilityWalFlags(TRUNCATE_SIZE_KEY + "=1000000");
146         SQLiteCompatibilityWalFlags.reset();
147 
148         SQLiteDatabase db = doOperation("testWalNoTruncate");
149 
150         assertTrue((new File(db.getPath() + WAL_SUFFIX)).length() > 0);
151     }
152 
153     /**
154      * When "truncate size" is set to 0, we don't truncate the wal file.
155      */
testWalTruncateDisabled()156     public void testWalTruncateDisabled() throws Exception {
157         mContext.deleteDatabase(DB_FILE);
158 
159         setCompatibilityWalFlags(TRUNCATE_SIZE_KEY + "=0");
160         SQLiteCompatibilityWalFlags.reset();
161 
162         SQLiteDatabase db = doOperation("testWalTruncateDisabled");
163 
164         assertTrue((new File(db.getPath() + WAL_SUFFIX)).length() > 0);
165     }
166 
doOperation(String message)167     private SQLiteDatabase doOperation(String message) throws Exception {
168         listFiles(message + ": start");
169 
170         SQLiteDatabase db = prepareDatabase();
171 
172         listFiles(message + ": DB created and prepared");
173 
174         // db.close() will remove the wal file, so back the files up.
175         backupFile(db.getPath());
176         backupFile(db.getPath() + SHM_SUFFIX);
177         backupFile(db.getPath() + WAL_SUFFIX);
178 
179         listFiles(message + ": backup created");
180 
181         // Close the DB, this will remove the WAL file.
182         db.close();
183 
184         listFiles(message + ": DB closed");
185 
186         // Restore the files.
187         restoreFile(db.getPath());
188         restoreFile(db.getPath() + SHM_SUFFIX);
189         restoreFile(db.getPath() + WAL_SUFFIX);
190 
191         listFiles(message + ": DB restored");
192 
193         // Open the DB again.
194         db = openDatabase();
195 
196         listFiles(message + ": DB re-opened");
197 
198         // Make sure the table still exists.
199         assertTestTableExists(db);
200 
201         return db;
202     }
203 
listFiles(String message)204     private void listFiles(String message) {
205         final File dir = mContext.getDatabasePath("a").getParentFile();
206         Log.i(TAG, "Listing files: " + message + " (" + dir.getAbsolutePath() + ")");
207 
208         final File[] files = mContext.getDatabasePath("a").getParentFile().listFiles();
209         if (files == null || files.length == 0) {
210             Log.i(TAG, "  No files found");
211             return;
212         }
213         for (File f : files) {
214             Log.i(TAG, "  file: " + f.getName() + " " + f.length() + " bytes");
215         }
216     }
217 }
218