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