1 /*
2 * 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
18
19 import android.database.sqlite.SQLiteDatabase
20 import androidx.inspection.ArtTooling
21 import androidx.inspection.testing.DefaultTestInspectorEnvironment
22 import androidx.inspection.testing.InspectorTester
23 import androidx.inspection.testing.TestInspectorExecutors
24 import androidx.room.Database
25 import androidx.room.Entity
26 import androidx.room.InvalidationTracker
27 import androidx.room.PrimaryKey
28 import androidx.room.Room
29 import androidx.room.RoomDatabase
30 import androidx.test.core.app.ApplicationProvider
31 import androidx.test.ext.junit.runners.AndroidJUnit4
32 import com.google.common.truth.Truth.assertWithMessage
33 import java.util.concurrent.Executors
34 import java.util.concurrent.TimeUnit
35 import kotlinx.coroutines.CompletableDeferred
36 import kotlinx.coroutines.Job
37 import kotlinx.coroutines.runBlocking
38 import org.junit.After
39 import org.junit.Before
40 import org.junit.Test
41 import org.junit.runner.RunWith
42
43 @RunWith(AndroidJUnit4::class)
44 class RoomInvalidationHookTest {
45 private lateinit var db: TestDatabase
46
47 private val testJob = Job()
48 private val ioExecutor = Executors.newSingleThreadExecutor()
49 private val testInspectorExecutors = TestInspectorExecutors(testJob, ioExecutor)
50
51 @Before
initDbnull52 fun initDb() {
53 db =
54 Room.inMemoryDatabaseBuilder(
55 ApplicationProvider.getApplicationContext(),
56 TestDatabase::class.java
57 )
58 .setQueryExecutor { it.run() }
59 .setTransactionExecutor { it.run() }
60 .build()
61 }
62
63 @After
closeDbnull64 fun closeDb() {
65 testJob.complete()
66 ioExecutor.shutdown()
67 assertWithMessage("inspector should not have any leaking tasks")
68 .that(ioExecutor.awaitTermination(10, TimeUnit.SECONDS))
69 .isTrue()
70
71 testInspectorExecutors.handler().looper.thread.join(10_000)
72 db.close()
73 }
74
75 /**
76 * A full integration test where we send a query via the inspector and assert that an
77 * invalidation observer on the Room side is invoked.
78 */
79 @Test
invalidationHooknull80 fun invalidationHook() =
81 runBlocking<Unit>(testJob) {
82 val testArtTI = TestArtTooling(roomDatabase = db, sqliteDb = db.getSqliteDb())
83
84 val testEnv =
85 DefaultTestInspectorEnvironment(
86 artTooling = testArtTI,
87 testInspectorExecutors = testInspectorExecutors
88 )
89 val tester =
90 InspectorTester(inspectorId = "androidx.sqlite.inspection", environment = testEnv)
91 val invalidatedTables = CompletableDeferred<List<String>>()
92 db.invalidationTracker.addObserver(
93 object : InvalidationTracker.Observer("TestEntity") {
94 override fun onInvalidated(tables: Set<String>) {
95 invalidatedTables.complete(tables.toList())
96 }
97 }
98 )
99 val startTrackingCommand =
100 SqliteInspectorProtocol.Command.newBuilder()
101 .setTrackDatabases(
102 SqliteInspectorProtocol.TrackDatabasesCommand.getDefaultInstance()
103 )
104 .build()
105 tester.sendCommand(startTrackingCommand.toByteArray())
106 // no invalidation yet
107 assertWithMessage("test sanity. no invalidation should happen yet")
108 .that(invalidatedTables.isActive)
109 .isTrue()
110 // send a write query
111 val insertQuery = """INSERT INTO TestEntity VALUES(1, "foo")"""
112 val insertCommand =
113 SqliteInspectorProtocol.Command.newBuilder()
114 .setQuery(
115 SqliteInspectorProtocol.QueryCommand.newBuilder()
116 .setDatabaseId(1)
117 .setQuery(insertQuery)
118 .build()
119 )
120 .build()
121 val responseBytes = tester.sendCommand(insertCommand.toByteArray())
122 val response = SqliteInspectorProtocol.Response.parseFrom(responseBytes)
123 assertWithMessage("test sanity, insert query should succeed")
124 .that(response.hasErrorOccurred())
125 .isFalse()
126
127 assertWithMessage("writing into db should trigger the table observer")
128 .that(invalidatedTables.await())
129 .containsExactly("TestEntity")
130 }
131 }
132
133 /** extract the framework sqlite database instance from a room database via reflection. */
RoomDatabasenull134 private fun RoomDatabase.getSqliteDb(): SQLiteDatabase {
135 val supportDb = this.openHelper.writableDatabase
136 // this runs with defaults so we can extract db from it until inspection supports support
137 // instances directly
138 return supportDb::class.java.getDeclaredField("delegate").let {
139 it.isAccessible = true
140 it.get(supportDb)
141 } as SQLiteDatabase
142 }
143
144 @Suppress("UNCHECKED_CAST")
145 class TestArtTooling(private val roomDatabase: RoomDatabase, private val sqliteDb: SQLiteDatabase) :
146 ArtTooling {
registerEntryHooknull147 override fun registerEntryHook(
148 originClass: Class<*>,
149 originMethod: String,
150 entryHook: ArtTooling.EntryHook
151 ) {
152 // no-op
153 }
154
findInstancesnull155 override fun <T : Any?> findInstances(clazz: Class<T>): List<T> {
156 if (clazz.isAssignableFrom(InvalidationTracker::class.java)) {
157 return listOf(roomDatabase.invalidationTracker as T)
158 } else if (clazz.isAssignableFrom(SQLiteDatabase::class.java)) {
159 return listOf(sqliteDb as T)
160 }
161 return emptyList()
162 }
163
registerExitHooknull164 override fun <T : Any?> registerExitHook(
165 originClass: Class<*>,
166 originMethod: String,
167 exitHook: ArtTooling.ExitHook<T>
168 ) {
169 // no-op
170 }
171 }
172
173 @Database(exportSchema = false, entities = [TestEntity::class], version = 1)
174 abstract class TestDatabase : RoomDatabase()
175
176 @Entity data class TestEntity(@PrimaryKey(autoGenerate = true) val id: Long, val value: String)
177