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.database.AbstractCursor
20 import android.database.Cursor
21 import android.database.CursorWrapper
22 import android.database.sqlite.SQLiteCursor
23 import android.database.sqlite.SQLiteDatabase
24 import android.database.sqlite.SQLiteStatement
25 import androidx.inspection.ArtTooling
26 import androidx.sqlite.inspection.SqliteInspectorProtocol.DatabasePossiblyChangedEvent
27 import androidx.sqlite.inspection.SqliteInspectorProtocol.Event.OneOfCase.DATABASE_POSSIBLY_CHANGED
28 import androidx.test.core.app.ApplicationProvider
29 import androidx.test.ext.junit.runners.AndroidJUnit4
30 import androidx.test.filters.FlakyTest
31 import androidx.test.filters.LargeTest
32 import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
33 import com.google.common.truth.Truth.assertThat
34 import kotlinx.coroutines.runBlocking
35 import org.junit.Ignore
36 import org.junit.Rule
37 import org.junit.Test
38 import org.junit.rules.TemporaryFolder
39 import org.junit.runner.RunWith
40 
41 @LargeTest
42 @RunWith(AndroidJUnit4::class)
43 class InvalidationTest {
44     @get:Rule val testEnvironment = SqliteInspectorTestEnvironment()
45 
46     @get:Rule val temporaryFolder = TemporaryFolder(getInstrumentation().context.cacheDir)
47 
48     @Test
49     @Ignore // b/159202455
50     fun test_exec_hook_methods() =
51         test_simple_hook_methods(
52             listOf("execute()V", "executeInsert()J", "executeUpdateDelete()I").map {
53                 it to SQLiteStatement::class.java
54             }
55         )
56 
57     @Test
58     fun test_end_transaction_hook_method() =
59         test_simple_hook_methods(listOf("endTransaction()V" to SQLiteDatabase::class.java))
60 
61     private fun test_simple_hook_methods(expectedHooks: List<Pair<String, Class<*>>>) =
62         runBlocking {
63             // Starting to track databases makes the inspector register hooks
64             testEnvironment.sendCommand(MessageFactory.createTrackDatabasesCommand())
65 
66             // Verification of hooks registration and triggering the DatabasePossiblyChangedEvent
67             testEnvironment.consumeRegisteredHooks().let { hooks ->
68                 expectedHooks.forEach { (method, clazz) ->
69                     val hook =
70                         hooks.filter { hook ->
71                             hook.originMethod == method && hook.originClass == clazz
72                         }
73                     assertThat(hook).hasSize(1)
74 
75                     testEnvironment.assertNoQueuedEvents()
76                     hook.first().asExitHook.onExit(null)
77                     testEnvironment.receiveEvent().let { event ->
78                         assertThat(event.oneOfCase == DATABASE_POSSIBLY_CHANGED)
79                         assertThat(event.databasePossiblyChanged)
80                             .isEqualTo(DatabasePossiblyChangedEvent.getDefaultInstance())
81                     }
82                     testEnvironment.assertNoQueuedEvents()
83                 }
84             }
85         }
86 
87     @Test
88     @FlakyTest // TODO: deflake
89     @Ignore
90     fun test_throttling(): Unit = runBlocking {
91         // Starting to track databases makes the inspector register hooks
92         testEnvironment.sendCommand(MessageFactory.createTrackDatabasesCommand())
93 
94         // Any hook that triggers invalidation
95         val hook =
96             testEnvironment
97                 .consumeRegisteredHooks()
98                 .first { it.originMethod == "executeInsert()J" }
99                 .asExitHook
100 
101         testEnvironment.assertNoQueuedEvents()
102 
103         // First invalidation triggering event
104         hook.onExit(null)
105         val event1 = testEnvironment.receiveEvent()
106 
107         // Shortly followed by many invalidation triggering events
108         repeat(50) { hook.onExit(null) }
109         val event2 = testEnvironment.receiveEvent()
110 
111         // Event validation
112         listOf(event1, event2).forEach {
113             assertThat(it.oneOfCase).isEqualTo(DATABASE_POSSIBLY_CHANGED)
114         }
115 
116         // Only two invalidation events received
117         testEnvironment.assertNoQueuedEvents()
118     }
119 
120     @Test
121     fun test_cursor_methods(): Unit = runBlocking {
122         // Starting to track databases makes the inspector register hooks
123         testEnvironment.sendCommand(MessageFactory.createTrackDatabasesCommand())
124 
125         // Hook method signatures
126         val rawQueryMethodSignature =
127             "rawQueryWithFactory(" +
128                 "Landroid/database/sqlite/SQLiteDatabase\$CursorFactory;" +
129                 "Ljava/lang/String;" +
130                 "[Ljava/lang/String;" +
131                 "Ljava/lang/String;" +
132                 "Landroid/os/CancellationSignal;" +
133                 ")Landroid/database/Cursor;"
134         val closeMethodSignature = "close()V"
135 
136         val hooks: List<Hook> = testEnvironment.consumeRegisteredHooks()
137 
138         // Check for hooks being registered
139         val hooksByClass = hooks.groupBy { it.originClass }
140         val rawQueryHooks =
141             hooksByClass[SQLiteDatabase::class.java]!!
142                 .filter { it.originMethod == rawQueryMethodSignature }
143                 .map { it::class }
144 
145         assertThat(rawQueryHooks).containsExactly(Hook.EntryHook::class, Hook.ExitHook::class)
146         val hook = hooksByClass[SQLiteCursor::class.java]!!.single()
147         assertThat(hook).isInstanceOf(Hook.EntryHook::class.java)
148         assertThat(hook.originMethod).isEqualTo(closeMethodSignature)
149         // Check for hook behaviour
150         fun wrap(cursor: Cursor): Cursor = object : CursorWrapper(cursor) {}
151         fun noOp(c: Cursor): Cursor = c
152         listOf(::wrap, ::noOp).forEach { wrap ->
153             listOf("insert into t1 values (1)" to true, "select * from sqlite_master" to false)
154                 .forEach { (query, shouldCauseInvalidation) ->
155                     testEnvironment.assertNoQueuedEvents()
156 
157                     val cursor = cursorForQuery(query)
158                     hooks.entryHookFor(rawQueryMethodSignature).onEntry(null, listOf(null, query))
159                     hooks.exitHookFor(rawQueryMethodSignature).onExit(wrap(wrap(cursor)))
160                     hooks.entryHookFor(closeMethodSignature).onEntry(cursor, emptyList())
161 
162                     if (shouldCauseInvalidation) {
163                         testEnvironment.receiveEvent()
164                     }
165                     testEnvironment.assertNoQueuedEvents()
166                 }
167         }
168 
169         // no crash for unknown cursor class
170         hooks.entryHookFor(rawQueryMethodSignature).onEntry(null, listOf(null, "select * from t1"))
171         hooks
172             .exitHookFor(rawQueryMethodSignature)
173             .onExit(
174                 object : AbstractCursor() {
175                     override fun getLong(column: Int): Long = 0
176 
177                     override fun getCount(): Int = 0
178 
179                     override fun getColumnNames(): Array<String> = emptyArray()
180 
181                     override fun getShort(column: Int): Short = 0
182 
183                     override fun getFloat(column: Int): Float = 0f
184 
185                     override fun getDouble(column: Int): Double = 0.0
186 
187                     override fun isNull(column: Int): Boolean = false
188 
189                     override fun getInt(column: Int): Int = 0
190 
191                     override fun getString(column: Int): String = ""
192                 }
193             )
194 
195         Unit
196     }
197 
198     private fun cursorForQuery(query: String): SQLiteCursor {
199         val db =
200             Database("ignored", Table("t1", Column("c1", "int"))).createInstance(temporaryFolder)
201         val cursor = db.rawQuery(query, null)
202         val context = ApplicationProvider.getApplicationContext() as android.content.Context
203         context.deleteDatabase(db.path)
204         return cursor as SQLiteCursor
205     }
206 
207     private fun List<Hook>.entryHookFor(m: String): ArtTooling.EntryHook =
208         this.first { it.originMethod == m && it is Hook.EntryHook }.asEntryHook
209 
210     @Suppress("UNCHECKED_CAST")
211     private fun List<Hook>.exitHookFor(m: String): ArtTooling.ExitHook<Any> =
212         this.first { it.originMethod == m && it is Hook.ExitHook }.asExitHook
213             as ArtTooling.ExitHook<Any>
214 }
215