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