1 /*
<lambda>null2  * Copyright 2020 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 androidx.sqlite.inspection.test
18 
19 import android.app.Application
20 import android.database.sqlite.SQLiteClosable
21 import android.database.sqlite.SQLiteDatabase
22 import android.os.Build
23 import androidx.inspection.ArtTooling.ExitHook
24 import androidx.sqlite.inspection.SqliteInspectorProtocol.Event
25 import androidx.sqlite.inspection.SqliteInspectorProtocol.Response
26 import androidx.sqlite.inspection.test.MessageFactory.createKeepDatabasesOpenCommand
27 import androidx.sqlite.inspection.test.MessageFactory.createKeepDatabasesOpenResponse
28 import androidx.sqlite.inspection.test.MessageFactory.createTrackDatabasesCommand
29 import androidx.sqlite.inspection.test.MessageFactory.createTrackDatabasesResponse
30 import androidx.test.ext.junit.runners.AndroidJUnit4
31 import androidx.test.filters.LargeTest
32 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
33 import com.google.common.truth.Truth.assertThat
34 import java.io.File
35 import kotlinx.coroutines.runBlocking
36 import org.junit.Rule
37 import org.junit.Test
38 import org.junit.rules.TemporaryFolder
39 import org.junit.runner.RunWith
40 
41 private const val OPEN_DATABASE_COMMAND_SIGNATURE_API11: String =
42     "openDatabase" +
43         "(" +
44         "Ljava/lang/String;" +
45         "Landroid/database/sqlite/SQLiteDatabase\$CursorFactory;" +
46         "I" +
47         "Landroid/database/DatabaseErrorHandler;" +
48         ")" +
49         "Landroid/database/sqlite/SQLiteDatabase;"
50 
51 private const val OPEN_DATABASE_COMMAND_SIGNATURE_API27: String =
52     "openDatabase" +
53         "(" +
54         "Ljava/io/File;" +
55         "Landroid/database/sqlite/SQLiteDatabase\$OpenParams;" +
56         ")" +
57         "Landroid/database/sqlite/SQLiteDatabase;"
58 
59 private const val CREATE_IN_MEMORY_DATABASE_COMMAND_SIGNATURE_API27 =
60     "createInMemory" +
61         "(" +
62         "Landroid/database/sqlite/SQLiteDatabase\$OpenParams;" +
63         ")" +
64         "Landroid/database/sqlite/SQLiteDatabase;"
65 
66 private const val ALL_REFERENCES_RELEASED_COMMAND_SIGNATURE = "onAllReferencesReleased()V"
67 
68 private const val RELEASE_REFERENCE_COMMAND_SIGNATURE = "releaseReference()V"
69 
70 @LargeTest
71 @RunWith(AndroidJUnit4::class)
72 class TrackDatabasesTest {
73     @get:Rule val testEnvironment = SqliteInspectorTestEnvironment()
74 
75     @get:Rule val temporaryFolder = TemporaryFolder(getInstrumentation().context.cacheDir)
76 
77     @Test
78     fun test_track_databases() = runBlocking {
79         val alreadyOpenDatabases =
80             listOf(
81                 Database("db1").createInstance(temporaryFolder),
82                 Database("db2").createInstance(temporaryFolder)
83             )
84 
85         testEnvironment.registerAlreadyOpenDatabases(alreadyOpenDatabases)
86 
87         testEnvironment.sendCommand(createTrackDatabasesCommand()).let { response ->
88             assertThat(response).isEqualTo(createTrackDatabasesResponse())
89         }
90 
91         // evaluate 'already-open' instances are found
92         alreadyOpenDatabases.let { expected ->
93             val actual = expected.indices.map { testEnvironment.receiveEvent().databaseOpened }
94             testEnvironment.assertNoQueuedEvents()
95             assertThat(actual.map { it.databaseId }.distinct()).hasSize(expected.size)
96             expected.forEachIndexed { ix, _ ->
97                 assertThat(actual[ix].path).isEqualTo(expected[ix].displayName)
98             }
99         }
100 
101         // evaluate registered hooks
102         val possibleSignatures =
103             listOf(
104                 OPEN_DATABASE_COMMAND_SIGNATURE_API11,
105                 OPEN_DATABASE_COMMAND_SIGNATURE_API27,
106                 CREATE_IN_MEMORY_DATABASE_COMMAND_SIGNATURE_API27
107             )
108         val wantedSignatures =
109             when {
110                 Build.VERSION.SDK_INT < 27 -> listOf(OPEN_DATABASE_COMMAND_SIGNATURE_API11)
111                 else ->
112                     listOf(
113                         OPEN_DATABASE_COMMAND_SIGNATURE_API11,
114                         OPEN_DATABASE_COMMAND_SIGNATURE_API27,
115                         CREATE_IN_MEMORY_DATABASE_COMMAND_SIGNATURE_API27
116                     )
117             }
118 
119         val hookEntries =
120             testEnvironment.consumeRegisteredHooks().filter {
121                 possibleSignatures.contains(it.originMethod)
122             }
123         assertThat(hookEntries).hasSize(wantedSignatures.size)
124         assertThat(hookEntries.map { it.originMethod }.containsAll(wantedSignatures)).isTrue()
125         hookEntries.forEachIndexed { ix, entry ->
126             // expect one exit hook tracking database open events
127             assertThat(entry).isInstanceOf(Hook.ExitHook::class.java)
128             assertThat(entry.originClass.name).isEqualTo(SQLiteDatabase::class.java.name)
129 
130             // verify that executing the registered hook will result in tracking events
131             testEnvironment.assertNoQueuedEvents()
132             @Suppress("UNCHECKED_CAST") val exitHook = entry.asExitHook as ExitHook<SQLiteDatabase>
133             val database = Database("db3_$ix").createInstance(temporaryFolder)
134             assertThat(exitHook.onExit(database)).isSameInstanceAs(database)
135             testEnvironment.receiveEvent().let { event ->
136                 assertThat(event.databaseOpened.path).isEqualTo(database.displayName)
137             }
138         }
139 
140         assertThat(testEnvironment.consumeRegisteredHooks()).isEmpty()
141     }
142 
143     @Test
144     fun test_track_databases_the_same_database_opened_multiple_times() = runBlocking {
145         // given
146         testEnvironment.sendCommand(createTrackDatabasesCommand())
147         val onOpenHook =
148             testEnvironment.consumeRegisteredHooks().first {
149                 it.originMethod == OPEN_DATABASE_COMMAND_SIGNATURE_API11
150             }
151         @Suppress("UNCHECKED_CAST")
152         val onOpen = (onOpenHook.asExitHook as ExitHook<SQLiteDatabase>)::onExit
153 
154         val seenDbIds = mutableSetOf<Int>()
155 
156         fun checkDbOpenedEvent(event: Event, database: SQLiteDatabase) {
157             assertThat(event.hasDatabaseOpened()).isEqualTo(true)
158             val isNewId = seenDbIds.add(event.databaseOpened.databaseId)
159             assertThat(isNewId).isEqualTo(true)
160             assertThat(event.databaseOpened.path).isEqualTo(database.displayName)
161         }
162 
163         // file based db: first open
164         val fileDbPath = "db1"
165         val fileDb = Database(fileDbPath).createInstance(temporaryFolder)
166         onOpen(fileDb)
167         checkDbOpenedEvent(testEnvironment.receiveEvent(), fileDb)
168 
169         // file based db: same instance
170         onOpen(fileDb)
171         testEnvironment.assertNoQueuedEvents()
172 
173         // file based db: same path
174         onOpen(Database(fileDbPath).createInstance(temporaryFolder))
175         testEnvironment.assertNoQueuedEvents()
176 
177         // in-memory database: first open
178         val inMemDb = Database(null).createInstance(temporaryFolder)
179         onOpen(inMemDb)
180         checkDbOpenedEvent(testEnvironment.receiveEvent(), inMemDb)
181 
182         // in-memory database: same instance
183         onOpen(inMemDb)
184         testEnvironment.assertNoQueuedEvents()
185 
186         // in-memory database: new instances (same path = :memory:)
187         repeat(3) {
188             val db = Database(null).createInstance(temporaryFolder)
189             assertThat(db.path).isEqualTo(":memory:")
190             onOpen(db)
191             checkDbOpenedEvent(testEnvironment.receiveEvent(), db)
192         }
193     }
194 
195     @Test
196     fun test_track_databases_keep_db_open_toggle() = runBlocking {
197         // given
198         val hooks = startTracking()
199 
200         // without inspecting
201         Database("db1").createInstance(temporaryFolder).let { db ->
202             db.close()
203             assertClosed(db)
204         }
205 
206         // with inspecting (initially keepOpen = false)
207         assertNoQueuedEvents()
208         listOf("db2", null).forEach { path ->
209             openDatabase(path, hooks).let { db ->
210                 val id = receiveOpenedEventId(db)
211                 closeDatabase(db, hooks)
212                 receiveClosedEvent(id, db.displayName)
213                 assertClosed(db)
214             }
215         }
216         assertNoQueuedEvents()
217 
218         // toggle keepOpen = true
219         issueKeepDatabasesOpenCommand(true)
220         assertNoQueuedEvents()
221 
222         // with inspecting (now keepOpen = true)
223         val dbs =
224             listOf("db3", null).map { path ->
225                 val db = openDatabase(path, hooks)
226                 val id = receiveOpenedEventId(db)
227                 id to db
228             }
229         dbs.forEach { (_, db) ->
230             closeDatabase(db, hooks)
231             assertOpen(db) // keep-open has worked
232         }
233         assertNoQueuedEvents()
234 
235         // toggle keepOpen = false
236         issueKeepDatabasesOpenCommand(false)
237         assertNoQueuedEvents()
238         dbs.forEach { (id, db) ->
239             assertClosed(db)
240             hooks.triggerOnAllReferencesReleased(db)
241             receiveClosedEvent(id, db.displayName)
242         }
243         assertNoQueuedEvents()
244 
245         // keepOpen = true with some of the same databases as before (they are not revived)
246         issueKeepDatabasesOpenCommand(true)
247         dbs.forEach { (_, db) -> assertClosed(db) }
248         assertNoQueuedEvents()
249 
250         // keepOpen = false with a database with more than one reference
251         issueKeepDatabasesOpenCommand(false)
252         openDatabase("db4", hooks).let { db ->
253             db.acquireReference() // extra reference
254 
255             closeDatabase(db, hooks)
256             assertOpen(db)
257 
258             closeDatabase(db, hooks)
259             assertClosed(db)
260         }
261     }
262 
263     @Test
264     fun test_on_closed_notification() = runBlocking {
265         // given
266         val hooks = startTracking()
267 
268         // simple flow
269         assertNoQueuedEvents()
270         openDatabase("db1", hooks).let { db ->
271             val id = receiveOpenedEventId(db)
272             closeDatabase(db, hooks)
273             receiveClosedEvent(id, db.displayName)
274             assertClosed(db)
275             assertNoQueuedEvents()
276         }
277 
278         // test that doesn't fire on each db.closed()
279         assertNoQueuedEvents()
280         openDatabase("db2", hooks).let { db ->
281             val id = receiveOpenedEventId(db)
282 
283             db.acquireReference() // extra reference
284 
285             // pass 1
286             closeDatabase(db, hooks)
287             assertOpen(db)
288             assertNoQueuedEvents()
289 
290             // pass 2
291             closeDatabase(db, hooks)
292             assertClosed(db)
293             receiveClosedEvent(id, db.displayName)
294             assertNoQueuedEvents()
295         }
296     }
297 
298     @Test
299     fun test_findInstances_closed() = runBlocking {
300         val db1a = Database("db1").createInstance(temporaryFolder)
301         val db2 = Database("db2").createInstance(temporaryFolder)
302         assertOpen(db1a)
303         assertOpen(db2)
304         db1a.close()
305         assertClosed(db1a)
306 
307         // given
308         testEnvironment.registerAlreadyOpenDatabases(listOf(db1a, db2))
309         val hooks = startTracking()
310         val id1 = receiveClosedEventId(db1a)
311         val id2 = receiveOpenedEventId(db2)
312         assertNoQueuedEvents()
313 
314         val db1b = openDatabase("db1", hooks)
315         assertThat(receiveOpenedEventId(db1a)).isEqualTo(id1)
316         assertNoQueuedEvents()
317 
318         closeDatabase(db1b, hooks)
319         receiveClosedEvent(id1, db1a.displayName)
320 
321         closeDatabase(db2, hooks)
322         receiveClosedEvent(id2, db2.displayName)
323     }
324 
325     @Test
326     fun test_findInstances_disk() = runBlocking {
327         val db1a = Database("db1").createInstance(temporaryFolder)
328         val db2 = Database("db2").createInstance(temporaryFolder)
329         val application =
330             object : Application() {
331                 override fun databaseList(): Array<String> =
332                     arrayOf(db1a.absolutePath, db2.absolutePath)
333 
334                 override fun getDatabasePath(name: String?) =
335                     getInstrumentation().context.getDatabasePath(name)
336             }
337 
338         testEnvironment.registerApplication(application)
339         val hooks = startTracking()
340 
341         val id1 = receiveClosedEventId(db1a.absolutePath)
342         val id2 = receiveClosedEventId(db2.absolutePath)
343         assertNoQueuedEvents()
344 
345         val db1b = openDatabase("db1", hooks)
346         receiveOpenedEvent(id1, db1a.absolutePath)
347         assertNoQueuedEvents()
348 
349         openDatabase("db2", hooks)
350         receiveOpenedEvent(id2, db2.absolutePath)
351         assertNoQueuedEvents()
352 
353         closeDatabase(db1b, hooks)
354         receiveClosedEvent(id1, db1a.absolutePath)
355         assertNoQueuedEvents()
356     }
357 
358     @Test
359     fun test_findInstances_disk_filters_helper_files() = runBlocking {
360         val db = Database("db1").createInstance(temporaryFolder, false)
361 
362         val application =
363             object : Application() {
364                 override fun databaseList(): Array<String> =
365                     temporaryFolder.root.list() as Array<String>
366 
367                 override fun getDatabasePath(name: String) = File(temporaryFolder.root, name)
368             }
369 
370         // trigger some query to establish connection
371         val cursor = db.rawQuery("select * from sqlite_master", emptyArray())
372         cursor.count
373         cursor.close()
374 
375         testEnvironment.registerApplication(application)
376         testEnvironment.registerAlreadyOpenDatabases(listOf(db))
377         val hooks = startTracking()
378 
379         val id = receiveOpenedEventId(db)
380         assertNoQueuedEvents()
381 
382         closeDatabase(db, hooks)
383         receiveClosedEvent(id, db.absolutePath)
384         assertNoQueuedEvents()
385     }
386 
387     @Test
388     fun test_on_closed_and_reopened() = runBlocking {
389         // given
390         val hooks = startTracking()
391 
392         // simple flow
393         val databaseName = "db1"
394 
395         assertNoQueuedEvents()
396         var id: Int
397         openDatabase(databaseName, hooks).let { db ->
398             id = receiveOpenedEventId(db)
399             closeDatabase(db, hooks)
400             receiveClosedEvent(id, db.displayName)
401             assertClosed(db)
402         }
403         testEnvironment.assertNoQueuedEvents()
404 
405         openDatabase(databaseName, hooks).let { db ->
406             assertThat(receiveOpenedEventId(db)).isEqualTo(id)
407             closeDatabase(db, hooks)
408             receiveClosedEvent(id, db.displayName)
409             assertClosed(db)
410         }
411         testEnvironment.assertNoQueuedEvents()
412     }
413 
414     @Test
415     fun test_temporary_databases_same_path_different_database() {
416         // given
417         val db1 = Database(null).createInstance(temporaryFolder)
418         val db2 = Database(null).createInstance(temporaryFolder)
419         fun queryTableCount(db: SQLiteDatabase): Long =
420             db.compileStatement("select count(*) from sqlite_master").simpleQueryForLong()
421         assertThat(queryTableCount(db1)).isEqualTo(1) // android_metadata sole table
422         assertThat(queryTableCount(db2)).isEqualTo(1) // android_metadata sole table
423         assertThat(db1.path).isEqualTo(db2.path)
424         assertThat(db1.path).isEqualTo(":memory:")
425 
426         // when
427         db1.execSQL("create table t1 (c1 int)")
428 
429         // then
430         assertThat(queryTableCount(db1)).isEqualTo(2)
431         assertThat(queryTableCount(db2)).isEqualTo(1)
432     }
433 
434     @Test
435     fun test_three_references_edge_ones_closed() = runBlocking {
436         val hooks = startTracking()
437 
438         val db1a = openDatabase("path1", hooks)
439         val id1a = receiveOpenedEventId(db1a)
440 
441         val db1b = openDatabase("path1", hooks)
442         assertNoQueuedEvents()
443 
444         val db1c = openDatabase("path1", hooks)
445         assertNoQueuedEvents()
446 
447         closeDatabase(db1a, hooks)
448         assertNoQueuedEvents()
449 
450         closeDatabase(db1c, hooks)
451         assertNoQueuedEvents()
452 
453         closeDatabase(db1b, hooks)
454         receiveClosedEvent(id1a, db1a.displayName)
455     }
456 
457     @Test
458     fun test_keep_open_while_user_attempts_to_close() = runBlocking {
459         val hooks = startTracking()
460         assertNoQueuedEvents()
461 
462         val db = openDatabase("db", hooks)
463         val id = receiveOpenedEventId(db)
464         assertNoQueuedEvents()
465 
466         issueKeepDatabasesOpenCommand(true)
467         assertNoQueuedEvents()
468 
469         var count = 0
470         closeDatabase(db, hooks)
471         count++
472         closeDatabase(db, hooks)
473         count++
474         closeDatabase(db, hooks)
475         count++
476         assertNoQueuedEvents()
477 
478         issueKeepDatabasesOpenCommand(false)
479         repeat(count) {
480             hooks.triggerReleaseReference(db)
481             if (!db.isOpen) {
482                 hooks.triggerOnAllReferencesReleased(db)
483             }
484         }
485 
486         receiveClosedEvent(id, db.displayName)
487         assertClosed(db)
488         assertNoQueuedEvents()
489     }
490 
491     /**
492      * #dbRef -- the number of references as seen by the SQLiteDatabase object #kpoRef=0 -- the
493      * number of references acquired by KeepOpen objects #usrRef=1 -- the 'balance' of references
494      * the user owns
495      */
496     @Test
497     fun test_keep_open_keeps_count() = runBlocking {
498         val hooks = startTracking()
499         assertNoQueuedEvents()
500 
501         val db = openDatabase("db", hooks) // #dbRef=1 | #kpoRef=0 | #usrRef=1
502         receiveOpenedEventId(db)
503         assertThat(db.referenceCount).isEqualTo(1)
504         assertNoQueuedEvents()
505 
506         issueKeepDatabasesOpenCommand(true) // #dbRef=1 | #kpoRef=0 | #usrRef=1
507         assertThat(db.referenceCount).isEqualTo(1)
508         assertNoQueuedEvents()
509 
510         var count = 0
511         closeDatabase(db, hooks)
512         count++ // #dbRef=1 | #kpoRef=1 | #usrRef=0
513         assertThat(db.referenceCount).isEqualTo(1)
514         closeDatabase(db, hooks)
515         count++ // #dbRef=1 | #kpoRef=2 | #usrRef=-1
516         assertThat(db.referenceCount).isEqualTo(1)
517         closeDatabase(db, hooks)
518         count++ // #dbRef=1 | #kpoRef=3 | #usrRef=-2
519         assertThat(db.referenceCount).isEqualTo(1)
520         assertNoQueuedEvents()
521 
522         repeat(count) {
523             db.acquireReference() // user offsetting the closed calls they made
524         } // #dbRef=4 | #kpoRef=3 | #usrRef=1
525         assertThat(db.referenceCount).isEqualTo(4)
526 
527         issueKeepDatabasesOpenCommand(false) // #dbRef=1 | #kpoRef=0 | #usrRef=1
528         repeat(count + 1) {
529             hooks.triggerReleaseReference(db)
530             assertOpen(db)
531         } // #dbRef=1 | #kpoRef=0 | #usrRef=1
532         assertThat(db.referenceCount).isEqualTo(1)
533 
534         assertOpen(db)
535         assertNoQueuedEvents()
536     }
537 
538     /**
539      * #dbRef -- the number of references as seen by the SQLiteDatabase object #kpoRef=0 -- the
540      * number of references acquired by KeepOpen objects #usrRef=1 -- the 'balance' of references
541      * the user owns
542      */
543     @Test
544     fun test_keep_open_off_on_off() = runBlocking {
545         val hooks = startTracking()
546         assertNoQueuedEvents()
547 
548         // keep-open = false (default)
549 
550         val db1 = openDatabase("db1", hooks)
551         val db2 = openDatabase("db2", hooks)
552 
553         assertThat(db1.referenceCount).isEqualTo(1) // #dbRef=1 | #kpoRef=0 | #usrRef=1
554         val id1 = receiveOpenedEventId(db1)
555         val id2 = receiveOpenedEventId(db2)
556         assertThat(db1.referenceCount).isEqualTo(1)
557         assertThat(db2.referenceCount).isEqualTo(1)
558         assertNoQueuedEvents()
559 
560         closeDatabase(db1, hooks) // #dbRef=0 | #kpoRef=0 | #usrRef=0
561         assertThat(db1.referenceCount).isEqualTo(0)
562         assertThat(db2.referenceCount).isEqualTo(1)
563         receiveClosedEvent(id1, db1.displayName)
564         assertNoQueuedEvents()
565 
566         // keep-open = true
567 
568         issueKeepDatabasesOpenCommand(true)
569         assertNoQueuedEvents()
570         assertThat(db2.referenceCount).isEqualTo(1) // #dbRef=1 | #kpoRef=0 | #usrRef=1
571         assertNoQueuedEvents()
572 
573         closeDatabase(db2, hooks) // #dbRef=1 | #kpoRef=1 | #usrRef=0
574         assertThat(db2.referenceCount).isEqualTo(1)
575         assertNoQueuedEvents()
576 
577         db2.acquireReference() // #dbRef=2 | #kpoRef=1 | #usrRef=1
578         assertThat(db2.referenceCount).isEqualTo(2)
579 
580         // keep-open = false
581 
582         issueKeepDatabasesOpenCommand(false)
583         hooks.triggerReleaseReference(db2) // #dbRef=1 | #kpoRef=0 | #usrRef=1
584         assertThat(db2.referenceCount).isEqualTo(1)
585         assertNoQueuedEvents()
586 
587         closeDatabase(db2, hooks) // #dbRef=0 | #kpoRef=0 | #usrRef=0
588         assertThat(db2.referenceCount).isEqualTo(0)
589         receiveClosedEvent(id2, db2.displayName)
590         assertNoQueuedEvents()
591     }
592 
593     private val SQLiteClosable.referenceCount: Int
594         get() = getFieldValue(SQLiteClosable::class.java, "mReferenceCount", this)
595 
596     @Suppress("SameParameterValue")
597     private fun <T> getFieldValue(clazz: Class<*>, fieldName: String, target: Any?): T {
598         val field = clazz.declaredFields.first { it.name == fieldName }
599         field.isAccessible = true
600         val result = field.get(target)
601         @Suppress("UNCHECKED_CAST") return result as T
602     }
603 
604     private fun assertNoQueuedEvents() {
605         testEnvironment.assertNoQueuedEvents()
606     }
607 
608     private suspend fun startTracking(): List<Hook> {
609         testEnvironment.sendCommand(createTrackDatabasesCommand())
610         return testEnvironment.consumeRegisteredHooks()
611     }
612 
613     private fun openDatabase(path: String?, hooks: List<Hook>): SQLiteDatabase =
614         Database(path).createInstance(temporaryFolder).also { hooks.triggerOnOpened(it) }
615 
616     private fun closeDatabase(database: SQLiteDatabase, hooks: List<Hook>) {
617         hooks.triggerReleaseReference(database)
618         database.close()
619         if (!database.isOpen) {
620             hooks.triggerOnAllReferencesReleased(database)
621         }
622     }
623 
624     private suspend fun issueKeepDatabasesOpenCommand(setEnabled: Boolean) {
625         testEnvironment.sendCommand(createKeepDatabasesOpenCommand(setEnabled)).let { response ->
626             assertThat(response.oneOfCase).isEqualTo(Response.OneOfCase.KEEP_DATABASES_OPEN)
627             assertThat(response).isEqualTo(createKeepDatabasesOpenResponse())
628         }
629     }
630 
631     private suspend fun receiveOpenedEventId(database: SQLiteDatabase): Int =
632         receiveOpenedEventId(database.displayName)
633 
634     private suspend fun receiveOpenedEventId(displayName: String): Int =
635         testEnvironment.receiveEvent().let {
636             assertThat(it.oneOfCase).isEqualTo(Event.OneOfCase.DATABASE_OPENED)
637             assertThat(it.databaseOpened.path).isEqualTo(displayName)
638             it.databaseOpened.databaseId
639         }
640 
641     private suspend fun receiveClosedEventId(displayName: String): Int =
642         testEnvironment.receiveEvent().let {
643             assertThat(it.oneOfCase).isEqualTo(Event.OneOfCase.DATABASE_CLOSED)
644             assertThat(it.databaseClosed.path).isEqualTo(displayName)
645             it.databaseClosed.databaseId
646         }
647 
648     private suspend fun receiveClosedEventId(database: SQLiteDatabase): Int =
649         receiveClosedEventId(database.displayName)
650 
651     private suspend fun receiveOpenedEvent(id: Int, path: String) =
652         testEnvironment.receiveEvent().let {
653             assertThat(it.oneOfCase).isEqualTo(Event.OneOfCase.DATABASE_OPENED)
654             assertThat(it.databaseOpened.databaseId).isEqualTo(id)
655             assertThat(it.databaseOpened.path).isEqualTo(path)
656         }
657 
658     private suspend fun receiveClosedEvent(id: Int, path: String) =
659         testEnvironment.receiveEvent().let {
660             assertThat(it.oneOfCase).isEqualTo(Event.OneOfCase.DATABASE_CLOSED)
661             assertThat(it.databaseClosed.databaseId).isEqualTo(id)
662             assertThat(it.databaseClosed.path).isEqualTo(path)
663         }
664 
665     @Suppress("UNCHECKED_CAST")
666     private fun List<Hook>.triggerOnOpened(db: SQLiteDatabase) {
667         val onOpen = filter { it.originMethod == OPEN_DATABASE_COMMAND_SIGNATURE_API11 }
668         assertThat(onOpen).hasSize(1)
669         (onOpen.first().asExitHook as ExitHook<SQLiteDatabase>).onExit(db)
670     }
671 
672     private fun List<Hook>.triggerReleaseReference(db: SQLiteDatabase) {
673         val onOpen = filter { it.originMethod == RELEASE_REFERENCE_COMMAND_SIGNATURE }
674         assertThat(onOpen).hasSize(1)
675         onOpen.first().asEntryHook.onEntry(db, emptyList())
676     }
677 
678     private fun List<Hook>.triggerOnAllReferencesReleased(db: SQLiteDatabase) {
679         val onReleasedHooks =
680             this.filter { it.originMethod == ALL_REFERENCES_RELEASED_COMMAND_SIGNATURE }
681         assertThat(onReleasedHooks).hasSize(2)
682         val onReleasedEntry = (onReleasedHooks.first { it is Hook.EntryHook }.asEntryHook)::onEntry
683         val onReleasedExit = (onReleasedHooks.first { it is Hook.ExitHook }.asExitHook)::onExit
684         onReleasedEntry(db, emptyList())
685         onReleasedExit(null)
686     }
687 
688     private fun assertOpen(db: SQLiteDatabase) {
689         assertThat(db.isOpen).isTrue()
690     }
691 
692     private fun assertClosed(db: SQLiteDatabase) {
693         assertThat(db.isOpen).isFalse()
694     }
695 }
696