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