• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertFalse;
21 import static org.junit.Assert.assertNotNull;
22 import static org.junit.Assert.assertTrue;
23 import static org.junit.Assert.fail;
24 
25 import android.content.Context;
26 import android.database.Cursor;
27 import android.database.DatabaseUtils;
28 import android.database.DefaultDatabaseErrorHandler;
29 import android.platform.test.annotations.RequiresFlagsEnabled;
30 import android.platform.test.flag.junit.CheckFlagsRule;
31 import android.platform.test.flag.junit.DeviceFlagsValueProvider;
32 import android.util.Printer;
33 
34 import androidx.test.ext.junit.runners.AndroidJUnit4;
35 import androidx.test.filters.SmallTest;
36 import androidx.test.platform.app.InstrumentationRegistry;
37 
38 import org.junit.After;
39 import org.junit.Before;
40 import org.junit.Rule;
41 import org.junit.Test;
42 import org.junit.runner.RunWith;
43 
44 import java.io.File;
45 import java.util.ArrayList;
46 import java.util.Collections;
47 import java.util.List;
48 import java.util.Vector;
49 import java.util.concurrent.ExecutorService;
50 import java.util.concurrent.Executors;
51 import java.util.concurrent.Phaser;
52 import java.util.concurrent.TimeUnit;
53 import java.util.regex.Matcher;
54 import java.util.regex.Pattern;
55 
56 @RunWith(AndroidJUnit4.class)
57 @SmallTest
58 public class SQLiteDatabaseTest {
59 
60     @Rule
61     public final CheckFlagsRule mCheckFlagsRule =
62             DeviceFlagsValueProvider.createCheckFlagsRule();
63 
64     private static final String TAG = "SQLiteDatabaseTest";
65 
66     private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
67 
68     private SQLiteDatabase mDatabase;
69     private File mDatabaseFile;
70     private static final String DATABASE_FILE_NAME = "database_test.db";
71 
72     @Before
setUp()73     public void setUp() throws Exception {
74         assertNotNull(mContext);
75         mContext.deleteDatabase(DATABASE_FILE_NAME);
76         mDatabaseFile = mContext.getDatabasePath(DATABASE_FILE_NAME);
77         mDatabaseFile.getParentFile().mkdirs(); // directory may not exist
78         mDatabase = SQLiteDatabase.openOrCreateDatabase(mDatabaseFile, null);
79         assertNotNull(mDatabase);
80     }
81 
82     @After
tearDown()83     public void tearDown() throws Exception {
84         closeAndDeleteDatabase();
85     }
86 
closeAndDeleteDatabase()87     private void closeAndDeleteDatabase() {
88         mDatabase.close();
89         SQLiteDatabase.deleteDatabase(mDatabaseFile);
90     }
91 
92     @Test
testStatementDDLEvictsCache()93     public void testStatementDDLEvictsCache() {
94         // The following will be cached (key is SQL string)
95         String selectQuery = "SELECT * FROM t1";
96 
97         mDatabase.beginTransaction();
98         mDatabase.execSQL("CREATE TABLE `t1` (`c1` INTEGER NOT NULL PRIMARY KEY, data TEXT)");
99         try (Cursor c = mDatabase.rawQuery(selectQuery, null)) {
100             assertEquals(2, c.getColumnCount());
101         }
102         // Alter the schema in such a way that if the cached query is used it would produce wrong
103         // results due to the change in column amounts.
104         mDatabase.execSQL("ALTER TABLE `t1` RENAME TO `t1_old`");
105         mDatabase.execSQL("CREATE TABLE `t1` (`c1` INTEGER NOT NULL PRIMARY KEY)");
106         // Execute cached query (that should have been evicted), validating it sees the new schema.
107         try (Cursor c = mDatabase.rawQuery(selectQuery, null)) {
108             assertEquals(1, c.getColumnCount());
109         }
110         mDatabase.setTransactionSuccessful();
111         mDatabase.endTransaction();
112     }
113 
114     @Test
testStressDDLEvicts()115     public void testStressDDLEvicts() {
116         mDatabase.enableWriteAheadLogging();
117         mDatabase.execSQL("CREATE TABLE `t1` (`c1` INTEGER NOT NULL PRIMARY KEY, data TEXT)");
118         final int iterations = 1000;
119         ExecutorService exec = Executors.newFixedThreadPool(2);
120         exec.execute(() -> {
121                     boolean pingPong = true;
122                     for (int i = 0; i < iterations; i++) {
123                         mDatabase.beginTransaction();
124                         if (pingPong) {
125                             mDatabase.execSQL("ALTER TABLE `t1` RENAME TO `t1_old`");
126                             mDatabase.execSQL("CREATE TABLE `t1` (`c1` INTEGER NOT NULL "
127                                 + "PRIMARY KEY)");
128                             pingPong = false;
129                         } else {
130                             mDatabase.execSQL("DROP TABLE `t1`");
131                             mDatabase.execSQL("ALTER TABLE `t1_old` RENAME TO `t1`");
132                             pingPong = true;
133                         }
134                         mDatabase.setTransactionSuccessful();
135                         mDatabase.endTransaction();
136                     }
137                 });
138         exec.execute(() -> {
139                     for (int i = 0; i < iterations; i++) {
140                         try (Cursor c = mDatabase.rawQuery("SELECT * FROM t1", null)) {
141                             c.getCount();
142                         }
143                     }
144                 });
145         try {
146             exec.shutdown();
147             assertTrue(exec.awaitTermination(1, TimeUnit.MINUTES));
148         } catch (InterruptedException e) {
149             fail("Timed out");
150         }
151     }
152 
153     /**
154      * Create a database with one table with three columns.
155      */
createComplexDatabase()156     private void createComplexDatabase() {
157         mDatabase.beginTransaction();
158         try {
159             mDatabase.execSQL("CREATE TABLE t1 (i int, d double, t text);");
160             mDatabase.setTransactionSuccessful();
161         } finally {
162             mDatabase.endTransaction();
163         }
164     }
165 
166     /**
167      * A three-value insert for the complex database.
168      */
createComplexInsert()169     private String createComplexInsert() {
170         return "INSERT INTO t1 (i, d, t) VALUES (?1, ?2, ?3)";
171     }
172 
173     @Test
testAutomaticCounters()174     public void testAutomaticCounters() {
175         final int size = 10;
176 
177         createComplexDatabase();
178 
179         // Put 10 lines in the database.
180         mDatabase.beginTransaction();
181         try {
182             try (SQLiteRawStatement s = mDatabase.createRawStatement(createComplexInsert())) {
183                 for (int i = 0; i < size; i++) {
184                     int vi = i * 3;
185                     double vd = i * 2.5;
186                     String vt = String.format("text%02dvalue", i);
187                     s.bindInt(1, vi);
188                     s.bindDouble(2, vd);
189                     s.bindText(3, vt);
190                     boolean r = s.step();
191                     // No row is returned by this query.
192                     assertFalse(r);
193                     s.reset();
194                     assertEquals(i + 1, mDatabase.getLastInsertRowId());
195                     assertEquals(1, mDatabase.getLastChangedRowCount());
196                     assertEquals(i + 2, mDatabase.getTotalChangedRowCount());
197                 }
198             }
199             mDatabase.setTransactionSuccessful();
200         } finally {
201             mDatabase.endTransaction();
202         }
203 
204         // Put a second 10 lines in the database.
205         mDatabase.beginTransaction();
206         try {
207             try (SQLiteRawStatement s = mDatabase.createRawStatement(createComplexInsert())) {
208                 for (int i = 0; i < size; i++) {
209                     int vi = i * 3;
210                     double vd = i * 2.5;
211                     String vt = String.format("text%02dvalue", i);
212                     s.bindInt(1, vi);
213                     s.bindDouble(2, vd);
214                     s.bindText(3, vt);
215                     boolean r = s.step();
216                     // No row is returned by this query.
217                     assertFalse(r);
218                     s.reset();
219                     assertEquals(size + i + 1, mDatabase.getLastInsertRowId());
220                     assertEquals(1, mDatabase.getLastChangedRowCount());
221                     assertEquals(size + i + 2, mDatabase.getTotalChangedRowCount());
222                 }
223             }
224             mDatabase.setTransactionSuccessful();
225         } finally {
226             mDatabase.endTransaction();
227         }
228     }
229 
230     @Test
testAutomaticCountersOutsideTransactions()231     public void testAutomaticCountersOutsideTransactions() {
232         try {
233             mDatabase.getLastChangedRowCount();
234             fail("getLastChangedRowCount() succeeded outside a transaction");
235         } catch (IllegalStateException e) {
236             // This exception is expected.
237         }
238 
239         try {
240             mDatabase.getTotalChangedRowCount();
241             fail("getTotalChangedRowCount() succeeded outside a transaction");
242         } catch (IllegalStateException e) {
243             // This exception is expected.
244         }
245     }
246 
247     /**
248      * Count the number of rows in the database <count> times.  The answer must match <expected>
249      * every time.  Any errors are reported back to the main thread through the <errors>
250      * array. The ticker forces the database reads to be interleaved with database operations from
251      * the sibling threads.
252      */
concurrentReadOnlyReader(SQLiteDatabase database, int count, long expected, List<Throwable> errors, Phaser ticker)253     private void concurrentReadOnlyReader(SQLiteDatabase database, int count, long expected,
254             List<Throwable> errors, Phaser ticker) {
255 
256         final String query = "--comment\nSELECT count(*) from t1";
257 
258         database.beginTransactionReadOnly();
259         try {
260             for (int i = count; i > 0; i--) {
261                 ticker.arriveAndAwaitAdvance();
262                 long r = DatabaseUtils.longForQuery(database, query, null);
263                 if (r != expected) {
264                     // The type of the exception is not important.  Only the message matters.
265                     throw new RuntimeException(
266                         String.format("concurrentRead expected %d, got %d", expected, r));
267                 }
268             }
269         } catch (Throwable t) {
270             errors.add(t);
271         } finally {
272             database.endTransaction();
273             ticker.arriveAndDeregister();
274         }
275     }
276 
277     /**
278      * Insert a new row <count> times.  Any errors are reported back to the main thread through
279      * the <errors> array. The ticker forces the database reads to be interleaved with database
280      * operations from the sibling threads.
281      */
concurrentImmediateWriter(SQLiteDatabase database, int count, List<Throwable> errors, Phaser ticker)282     private void concurrentImmediateWriter(SQLiteDatabase database, int count,
283             List<Throwable> errors, Phaser ticker) {
284         database.beginTransaction();
285         try {
286             int n = 100;
287             for (int i = count; i > 0; i--) {
288                 ticker.arriveAndAwaitAdvance();
289                 database.execSQL(String.format("INSERT INTO t1 (i) VALUES (%d)", n++));
290             }
291             database.setTransactionSuccessful();
292         } catch (Throwable t) {
293             errors.add(t);
294         } finally {
295             database.endTransaction();
296             ticker.arriveAndDeregister();
297         }
298     }
299 
300     /**
301      * This test verifies that a read-only transaction can be started, and it is deferred.  A
302      * deferred transaction does not take a database locks until the database is accessed.  This
303      * test verifies that the implicit connection selection process correctly identifies
304      * read-only transactions even when they are preceded by a comment.
305      */
306     @Test
testReadOnlyTransaction()307     public void testReadOnlyTransaction() throws Exception {
308         // Enable WAL for concurrent read and write transactions.
309         mDatabase.enableWriteAheadLogging();
310 
311         // Create the t1 table and put some data in it.
312         mDatabase.beginTransaction();
313         try {
314             mDatabase.execSQL("CREATE TABLE t1 (i int);");
315             mDatabase.execSQL("INSERT INTO t1 (i) VALUES (2)");
316             mDatabase.execSQL("INSERT INTO t1 (i) VALUES (3)");
317             mDatabase.setTransactionSuccessful();
318         } finally {
319             mDatabase.endTransaction();
320         }
321 
322         // Threads install errors in this array.
323         final List<Throwable> errors = Collections.synchronizedList(new ArrayList<Throwable>());
324 
325         // This forces the read and write threads to execute in a lock-step, round-robin fashion.
326         Phaser ticker = new Phaser(3);
327 
328         // Create three threads that will perform transactions.  One thread is a writer and two
329         // are readers.  The intent is that the readers begin before the writer commits, so the
330         // readers always see a database with two rows.
331         Thread readerA = new Thread(() -> {
332               concurrentReadOnlyReader(mDatabase, 4, 2, errors, ticker);
333         });
334         Thread readerB = new Thread(() -> {
335               concurrentReadOnlyReader(mDatabase, 4, 2, errors, ticker);
336         });
337         Thread writerC = new Thread(() -> {
338               concurrentImmediateWriter(mDatabase, 4, errors, ticker);
339         });
340 
341         readerA.start();
342         readerB.start();
343         writerC.start();
344 
345         // All three threads should have completed.  Give the total set 1s.  The 10ms delay for
346         // the second and third threads is just a small, positive number.
347         readerA.join(1000);
348         assertFalse(readerA.isAlive());
349         readerB.join(10);
350         assertFalse(readerB.isAlive());
351         writerC.join(10);
352         assertFalse(writerC.isAlive());
353 
354         // The writer added 4 rows to the database.
355         long r = DatabaseUtils.longForQuery(mDatabase, "SELECT count(*) from t1", null);
356         assertEquals(6, r);
357 
358         assertTrue("ReadThread failed with errors: " + errors, errors.isEmpty());
359     }
360 
361     @Test
testTempTable()362     public void testTempTable() {
363         boolean allowed;
364         allowed = true;
365         mDatabase.beginTransactionReadOnly();
366         try {
367             mDatabase.execSQL("CREATE TEMP TABLE t1 (i int, j int);");
368             mDatabase.execSQL("INSERT INTO t1 (i, j) VALUES (2, 20)");
369             mDatabase.execSQL("INSERT INTO t1 (i, j) VALUES (3, 30)");
370 
371             final String sql = "SELECT i FROM t1 WHERE j = 30";
372             try (SQLiteRawStatement s = mDatabase.createRawStatement(sql)) {
373                 assertTrue(s.step());
374                 assertEquals(3, s.getColumnInt(0));
375             }
376 
377             mDatabase.execSQL("DROP TABLE t1");
378 
379         } catch (SQLiteException e) {
380             allowed = false;
381         } finally {
382             mDatabase.endTransaction();
383         }
384         assertTrue(allowed);
385 
386         // Repeat the test on the main schema.
387         allowed = true;
388         mDatabase.beginTransactionReadOnly();
389         try {
390             mDatabase.execSQL("CREATE TABLE t2 (i int, j int);");
391             mDatabase.execSQL("INSERT INTO t2 (i, j) VALUES (2, 20)");
392             mDatabase.execSQL("INSERT INTO t2 (i, j) VALUES (3, 30)");
393 
394             final String sql = "SELECT i FROM t2 WHERE j = 30";
395             try (SQLiteRawStatement s = mDatabase.createRawStatement(sql)) {
396                 assertTrue(s.step());
397                 assertEquals(3, s.getColumnInt(0));
398             }
399 
400         } catch (SQLiteException e) {
401             allowed = false;
402         } finally {
403             mDatabase.endTransaction();
404         }
405         assertFalse(allowed);
406     }
407 
408     /** Dumpsys information about a single database. */
409 
410     /**
411      * Collect and parse dumpsys output.  This is not a full parser.  It is only enough to support
412      * the unit tests.
413      */
414     private static class Dumpsys {
415         // Regular expressions for parsing the output.  Reportedly, regular expressions are
416         // expensive, so these are created only if a dumpsys object is created.
417         private static final Object sLock = new Object();
418         static Pattern mPool;
419         static Pattern mConnection;
420         static Pattern mEntry;
421         static Pattern mSingleWord;
422         static Pattern mNone;
423 
424         // The raw strings read from dumpsys.  Once loaded, this list never changes.
425         final ArrayList<String> mRaw = new ArrayList<>();
426 
427         // Parsed dumpsys.  This contains only the bits that are being tested.
428         static class Connection {
429             ArrayList<String> mRecent = new ArrayList<>();
430             ArrayList<String> mLong = new ArrayList<>();
431         }
432         static class Database {
433             String mPath;
434             ArrayList<Connection> mConnection = new ArrayList<>();
435         }
436         ArrayList<Database> mDatabase;
437         ArrayList<String> mConcurrent;
438 
Dumpsys()439         Dumpsys() {
440             SQLiteDebug.dump(
441                 new Printer() { public void println(String x) { mRaw.add(x); } },
442                 new String[0]);
443             parse();
444         }
445 
446         /** Parse the raw text. Return true if no errors were detected. */
parse()447         boolean parse() {
448             initialize();
449 
450             // Reset the parsed information.  This method may be called repeatedly.
451             mDatabase = new ArrayList<>();
452             mConcurrent = new ArrayList<>();
453 
454             Database current = null;
455             Connection connection = null;
456             Matcher matcher;
457             for (int i = 0; i < mRaw.size(); i++) {
458                 final String line = mRaw.get(i);
459                 matcher = mPool.matcher(line);
460                 if (matcher.lookingAt()) {
461                     current = new Database();
462                     mDatabase.add(current);
463                     current.mPath = matcher.group(1);
464                     continue;
465                 }
466                 matcher = mConnection.matcher(line);
467                 if (matcher.lookingAt()) {
468                     connection = new Connection();
469                     current.mConnection.add(connection);
470                     continue;
471                 }
472 
473                 if (line.contains("Most recently executed operations")) {
474                     i += readTable(connection.mRecent, i, mEntry);
475                     continue;
476                 }
477 
478                 if (line.contains("Operations exceeding 2000ms")) {
479                     i += readTable(connection.mLong, i, mEntry);
480                     continue;
481                 }
482                 if (line.contains("Concurrently opened database files")) {
483                     i += readTable(mConcurrent, i, mSingleWord);
484                     continue;
485                 }
486             }
487             return true;
488         }
489 
490         /**
491          * Read a series of lines following a header.  Return the number of lines read.  The input
492          * line number is the number of the header.
493          */
readTable(List<String> s, int header, Pattern p)494         private int readTable(List<String> s, int header, Pattern p) {
495             // Special case: if the first line is "<none>" then there are no more lines to the
496             // table.
497             if (lookingAt(header+1, mNone)) return 1;
498 
499             int i;
500             for (i = header + 1; i < mRaw.size() && lookingAt(i, p); i++) {
501                 s.add(mRaw.get(i).trim());
502             }
503             return i - header;
504         }
505 
506         /** Return true if the n'th raw line matches the pattern. */
lookingAt(int n, Pattern p)507         boolean lookingAt(int n, Pattern p) {
508             return p.matcher(mRaw.get(n)).lookingAt();
509         }
510 
511         /** Compile the regular expressions the first time. */
initialize()512         private static void initialize() {
513             synchronized (sLock) {
514                 if (mPool != null) return;
515                 mPool = Pattern.compile("Connection pool for (\\S+):");
516                 mConnection = Pattern.compile("\\s+Connection #(\\d+):");
517                 mEntry = Pattern.compile("\\s+(\\d+): ");
518                 mSingleWord = Pattern.compile("  (\\S+)$");
519                 mNone = Pattern.compile("\\s+<none>$");
520             }
521         }
522     }
523 
524     @Test
testDumpsys()525     public void testDumpsys() throws Exception {
526         Dumpsys dumpsys = new Dumpsys();
527 
528         assertEquals(1, dumpsys.mDatabase.size());
529         // Note: cannot test mConcurrent because that attribute is not hermitic with respect to
530         // the tests.
531 
532         Dumpsys.Database db = dumpsys.mDatabase.get(0);
533 
534         // Work with normalized paths.
535         String wantPath = mDatabaseFile.toPath().toRealPath().toString();
536         String realPath = new File(db.mPath).toPath().toRealPath().toString();
537         assertEquals(wantPath, realPath);
538 
539         assertEquals(1, db.mConnection.size());
540     }
541 
542     // Create and open the database, allowing or disallowing double-quoted strings.
createDatabase(boolean noDoubleQuotedStrs)543     private void createDatabase(boolean noDoubleQuotedStrs) throws Exception {
544         // The open-flags that do not change in this test.
545         int flags = SQLiteDatabase.CREATE_IF_NECESSARY | SQLiteDatabase.OPEN_READWRITE;
546 
547         // The flag to be tested.
548         int flagUnderTest = SQLiteDatabase.NO_DOUBLE_QUOTED_STRS;
549 
550         if (noDoubleQuotedStrs) {
551             flags |= flagUnderTest;
552         } else {
553             flags &= ~flagUnderTest;
554         }
555         mDatabase = SQLiteDatabase.openDatabase(mDatabaseFile.getPath(), null, flags, null);
556     }
557 
558     /**
559      * This test verifies that the NO_DOUBLE_QUOTED_STRS flag works as expected when opening a
560      * database.  This does not test that the flag is initialized as expected from the system
561      * properties.
562      */
563     @Test
testNoDoubleQuotedStrings()564     public void testNoDoubleQuotedStrings() throws Exception {
565         closeAndDeleteDatabase();
566         createDatabase(/* noDoubleQuotedStrs */ false);
567 
568         mDatabase.beginTransaction();
569         try {
570             mDatabase.execSQL("CREATE TABLE t1 (t text);");
571             // Insert a value in double-quotes.  This is invalid but accepted.
572             mDatabase.execSQL("INSERT INTO t1 (t) VALUES (\"foo\")");
573         } finally {
574             mDatabase.endTransaction();
575         }
576 
577         closeAndDeleteDatabase();
578         createDatabase(/* noDoubleQuotedStrs */ true);
579 
580         mDatabase.beginTransaction();
581         try {
582             mDatabase.execSQL("CREATE TABLE t1 (t text);");
583             try {
584                 // Insert a value in double-quotes.  This is invalid and must throw.
585                 mDatabase.execSQL("INSERT INTO t1 (t) VALUES (\"foo\")");
586                 fail("expected an exception");
587             } catch (SQLiteException e) {
588                 assertTrue(e.toString().contains("no such column"));
589             }
590         } finally {
591             mDatabase.endTransaction();
592         }
593         closeAndDeleteDatabase();
594     }
595 
596     @Test
testCloseCorruptionReport()597     public void testCloseCorruptionReport() throws Exception {
598         mDatabase.beginTransaction();
599         try {
600             mDatabase.execSQL("CREATE TABLE t2 (i int, j int);");
601             mDatabase.execSQL("INSERT INTO t2 (i, j) VALUES (2, 20)");
602             mDatabase.execSQL("INSERT INTO t2 (i, j) VALUES (3, 30)");
603             mDatabase.setTransactionSuccessful();
604         } finally {
605             mDatabase.endTransaction();
606         }
607 
608         // Start a transaction and announce that the DB is corrupted.
609         DefaultDatabaseErrorHandler errorHandler = new DefaultDatabaseErrorHandler();
610 
611         // Do not bother with endTransaction; the database will have been closed in the corruption
612         // handler.
613         mDatabase.beginTransaction();
614         try {
615             errorHandler.onCorruption(mDatabase);
616             mDatabase.execSQL("INSERT INTO t2 (i, j) VALUES (4, 40)");
617             fail("expected an exception");
618         } catch (IllegalStateException e) {
619             final Throwable cause = e.getCause();
620             assertNotNull(cause);
621             boolean found = false;
622             for (StackTraceElement s : cause.getStackTrace()) {
623                 if (s.getMethodName().contains("onCorruption")) {
624                     found = true;
625                 }
626             }
627             assertTrue(found);
628         }
629     }
630 
631     @Test
testCloseReport()632     public void testCloseReport() throws Exception {
633         mDatabase.beginTransaction();
634         try {
635             mDatabase.execSQL("CREATE TABLE t2 (i int, j int);");
636             mDatabase.execSQL("INSERT INTO t2 (i, j) VALUES (2, 20)");
637             mDatabase.execSQL("INSERT INTO t2 (i, j) VALUES (3, 30)");
638             mDatabase.setTransactionSuccessful();
639         } finally {
640             mDatabase.endTransaction();
641         }
642 
643         mDatabase.close();
644         try {
645             // Do not bother with endTransaction; the database has already been close.
646             mDatabase.beginTransaction();
647             fail("expected an exception");
648         } catch (IllegalStateException e) {
649             assertTrue(e.toString().contains("attempt to re-open an already-closed object"));
650             final Throwable cause = e.getCause();
651             assertNotNull(cause);
652             boolean found = false;
653             for (StackTraceElement s : cause.getStackTrace()) {
654                 if (s.getMethodName().contains("testCloseReport")) {
655                     found = true;
656                 }
657             }
658             assertTrue(found);
659         }
660     }
661 }
662