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.SQLiteDatabase
21 import androidx.inspection.ArtTooling
22 import androidx.inspection.testing.DefaultTestInspectorEnvironment
23 import androidx.inspection.testing.InspectorTester
24 import androidx.inspection.testing.TestInspectorExecutors
25 import androidx.sqlite.inspection.SqliteInspectorProtocol
26 import androidx.sqlite.inspection.SqliteInspectorProtocol.Command
27 import androidx.sqlite.inspection.SqliteInspectorProtocol.DatabaseOpenedEvent
28 import androidx.sqlite.inspection.SqliteInspectorProtocol.Event
29 import androidx.sqlite.inspection.SqliteInspectorProtocol.Response
30 import com.google.common.truth.Truth.assertThat
31 import java.util.concurrent.Executor
32 import kotlinx.coroutines.ExperimentalCoroutinesApi
33 import kotlinx.coroutines.Job
34 import kotlinx.coroutines.cancelAndJoin
35 import kotlinx.coroutines.runBlocking
36 import org.junit.rules.ExternalResource
37
38 private const val SQLITE_INSPECTOR_ID = "androidx.sqlite.inspection"
39
40 class SqliteInspectorTestEnvironment(val ioExecutorOverride: Executor? = null) :
41 ExternalResource() {
42 private lateinit var inspectorTester: InspectorTester
43 private lateinit var artTooling: FakeArtTooling
44 private val job = Job()
45
46 override fun before() {
47
48 artTooling = FakeArtTooling()
49 inspectorTester = runBlocking {
50 InspectorTester(
51 inspectorId = SQLITE_INSPECTOR_ID,
52 environment =
53 DefaultTestInspectorEnvironment(
54 TestInspectorExecutors(job, ioExecutorOverride),
55 artTooling
56 )
57 )
58 }
59 }
60
61 override fun after() {
62 inspectorTester.dispose()
63 runBlocking { job.cancelAndJoin() }
64 }
65
66 @OptIn(ExperimentalCoroutinesApi::class)
67 fun assertNoQueuedEvents() {
68 assertThat(inspectorTester.channel.isEmpty).isTrue()
69 }
70
71 suspend fun sendCommand(command: Command): Response {
72 inspectorTester.sendCommand(command.toByteArray()).let { responseBytes ->
73 assertThat(responseBytes).isNotEmpty()
74 return Response.parseFrom(responseBytes)
75 }
76 }
77
78 suspend fun receiveEvent(): Event {
79 inspectorTester.channel.receive().let { responseBytes ->
80 assertThat(responseBytes).isNotEmpty()
81 return Event.parseFrom(responseBytes)
82 }
83 }
84
85 fun registerAlreadyOpenDatabases(databases: List<SQLiteDatabase>) {
86 artTooling.registerInstancesToFind(databases)
87 }
88
89 fun registerApplication(application: Application) {
90 artTooling.registerInstancesToFind(listOf(application))
91 }
92
93 fun consumeRegisteredHooks(): List<Hook> = artTooling.consumeRegisteredHooks()
94
95 /** Assumes an event with the relevant database will be fired. */
96 suspend fun awaitDatabaseOpenedEvent(databasePath: String): DatabaseOpenedEvent {
97 while (true) {
98 val event = receiveEvent().databaseOpened
99 if (event.path == databasePath) {
100 return event
101 }
102 }
103 }
104 }
105
issueQuerynull106 suspend fun SqliteInspectorTestEnvironment.issueQuery(
107 databaseId: Int,
108 command: String,
109 queryParams: List<String?>? = null
110 ): SqliteInspectorProtocol.QueryResponse =
111 sendCommand(MessageFactory.createQueryCommand(databaseId, command, queryParams)).query
112
113 suspend fun SqliteInspectorTestEnvironment.inspectDatabase(databaseInstance: SQLiteDatabase): Int {
114 registerAlreadyOpenDatabases(listOf(databaseInstance))
115 sendCommand(MessageFactory.createTrackDatabasesCommand())
116 return awaitDatabaseOpenedEvent(databaseInstance.displayName).databaseId
117 }
118
119 /**
120 * Fake inspector environment with the following behaviour:
121 * - [findInstances] returns pre-registered values from [registerInstancesToFind].
122 * - [registerEntryHook] and [registerExitHook] record the calls which can later be retrieved in
123 * [consumeRegisteredHooks].
124 */
125 private class FakeArtTooling : ArtTooling {
126 private val instancesToFind = mutableListOf<Any>()
127 private val registeredHooks = mutableListOf<Hook>()
128
registerInstancesToFindnull129 fun registerInstancesToFind(instances: List<Any>) {
130 instancesToFind.addAll(instances)
131 }
132
133 /**
134 * Returns instances pre-registered in [registerInstancesToFind]. By design crashes in case of
135 * the wrong setup - indicating an issue with test code.
136 */
137 @Suppress("UNCHECKED_CAST")
138 // TODO: implement actual findInstances behaviour
findInstancesnull139 override fun <T : Any?> findInstances(clazz: Class<T>): MutableList<T> =
140 instancesToFind.filter { clazz.isInstance(it) }.map { it as T }.toMutableList()
141
registerEntryHooknull142 override fun registerEntryHook(
143 originClass: Class<*>,
144 originMethod: String,
145 entryHook: ArtTooling.EntryHook
146 ) {
147 // TODO: implement actual registerEntryHook behaviour
148 registeredHooks.add(Hook.EntryHook(originClass, originMethod, entryHook))
149 }
150
registerExitHooknull151 override fun <T : Any?> registerExitHook(
152 originClass: Class<*>,
153 originMethod: String,
154 exitHook: ArtTooling.ExitHook<T>
155 ) {
156 // TODO: implement actual registerExitHook behaviour
157 registeredHooks.add(Hook.ExitHook(originClass, originMethod, exitHook))
158 }
159
consumeRegisteredHooksnull160 fun consumeRegisteredHooks(): List<Hook> =
161 registeredHooks.toList().also { registeredHooks.clear() }
162 }
163
164 sealed class Hook(val originClass: Class<*>, val originMethod: String) {
165 class ExitHook(
166 originClass: Class<*>,
167 originMethod: String,
168 val exitHook: ArtTooling.ExitHook<*>
169 ) : Hook(originClass, originMethod)
170
171 class EntryHook(
172 originClass: Class<*>,
173 originMethod: String,
174 @Suppress("unused") val entryHook: ArtTooling.EntryHook
175 ) : Hook(originClass, originMethod)
176 }
177
178 val Hook.asEntryHook
179 get() = (this as Hook.EntryHook).entryHook
180 val Hook.asExitHook
181 get() = (this as Hook.ExitHook).exitHook
182