mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-04-20 03:24:50 +00:00
Unittests and fixes for dbstore
This commit is contained in:
parent
f3c9e0196e
commit
662e94bcee
8 changed files with 273 additions and 24 deletions
|
@ -0,0 +1,159 @@
|
|||
package com.futo.platformplayer
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.futo.platformplayer.stores.db.ManagedDBDescriptor
|
||||
import com.futo.platformplayer.stores.db.ManagedDBStore
|
||||
import com.futo.platformplayer.testing.DBTOs
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentMap
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
class ManagedDBStoreTests {
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext;
|
||||
|
||||
@Test
|
||||
fun startup() {
|
||||
val store = ManagedDBStore.create("test", Descriptor())
|
||||
.load(context, true);
|
||||
store.deleteAll();
|
||||
|
||||
store.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insert() {
|
||||
val store = ManagedDBStore.create("test", Descriptor())
|
||||
.load(context, true);
|
||||
store.deleteAll();
|
||||
|
||||
val testObj = DBTOs.TestObject();
|
||||
createAndAssert(store, testObj);
|
||||
|
||||
store.shutdown();
|
||||
}
|
||||
@Test
|
||||
fun update() {
|
||||
val store = ManagedDBStore.create("test", Descriptor())
|
||||
.load(context, true);
|
||||
store.deleteAll();
|
||||
|
||||
val testObj = DBTOs.TestObject();
|
||||
val obj = createAndAssert(store, testObj);
|
||||
|
||||
testObj.someStr = "Testing";
|
||||
store.update(obj.id!!, testObj);
|
||||
val obj2 = store.get(obj.id!!);
|
||||
assertIndexEquals(obj2, testObj);
|
||||
|
||||
store.shutdown();
|
||||
}
|
||||
@Test
|
||||
fun delete() {
|
||||
val store = ManagedDBStore.create("test", Descriptor())
|
||||
.load(context, true);
|
||||
store.deleteAll();
|
||||
|
||||
val testObj = DBTOs.TestObject();
|
||||
val obj = createAndAssert(store, testObj);
|
||||
store.delete(obj.id!!);
|
||||
|
||||
Assert.assertEquals(store.count(), 0);
|
||||
Assert.assertNull(store.getOrNull(obj.id!!));
|
||||
|
||||
store.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
fun withIndex() {
|
||||
val index = ConcurrentHashMap<Any, DBTOs.TestIndex>();
|
||||
val store = ManagedDBStore.create("test", Descriptor())
|
||||
.withIndex({it.someString}, index, true)
|
||||
.load(context, true);
|
||||
store.deleteAll();
|
||||
|
||||
val testObj1 = DBTOs.TestObject();
|
||||
val testObj2 = DBTOs.TestObject();
|
||||
val testObj3 = DBTOs.TestObject();
|
||||
val obj1 = createAndAssert(store, testObj1);
|
||||
val obj2 = createAndAssert(store, testObj2);
|
||||
val obj3 = createAndAssert(store, testObj3);
|
||||
Assert.assertEquals(store.count(), 3);
|
||||
|
||||
Assert.assertTrue(index.containsKey(testObj1.someStr));
|
||||
Assert.assertTrue(index.containsKey(testObj2.someStr));
|
||||
Assert.assertTrue(index.containsKey(testObj3.someStr));
|
||||
Assert.assertEquals(index.size, 3);
|
||||
|
||||
val oldStr = testObj1.someStr;
|
||||
testObj1.someStr = UUID.randomUUID().toString();
|
||||
store.update(obj1.id!!, testObj1);
|
||||
|
||||
Assert.assertEquals(index.size, 3);
|
||||
Assert.assertFalse(index.containsKey(oldStr));
|
||||
Assert.assertTrue(index.containsKey(testObj1.someStr));
|
||||
Assert.assertTrue(index.containsKey(testObj2.someStr));
|
||||
Assert.assertTrue(index.containsKey(testObj3.someStr));
|
||||
|
||||
store.delete(obj2.id!!);
|
||||
Assert.assertEquals(index.size, 2);
|
||||
|
||||
Assert.assertFalse(index.containsKey(oldStr));
|
||||
Assert.assertTrue(index.containsKey(testObj1.someStr));
|
||||
Assert.assertFalse(index.containsKey(testObj2.someStr));
|
||||
Assert.assertTrue(index.containsKey(testObj3.someStr));
|
||||
store.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
fun withUnique() {
|
||||
val index = ConcurrentHashMap<Any, DBTOs.TestIndex>();
|
||||
val store = ManagedDBStore.create("test", Descriptor())
|
||||
.withIndex({it.someString}, index, false, true)
|
||||
.load(context, true);
|
||||
store.deleteAll();
|
||||
|
||||
val testObj1 = DBTOs.TestObject();
|
||||
val testObj2 = DBTOs.TestObject();
|
||||
val testObj3 = DBTOs.TestObject();
|
||||
val obj1 = createAndAssert(store, testObj1);
|
||||
val obj2 = createAndAssert(store, testObj2);
|
||||
|
||||
testObj3.someStr = testObj2.someStr;
|
||||
Assert.assertEquals(store.insert(testObj3), obj2.id!!);
|
||||
Assert.assertEquals(store.count(), 2);
|
||||
|
||||
store.shutdown();
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun createAndAssert(store: ManagedDBStore<DBTOs.TestIndex, DBTOs.TestObject, DBTOs.DB, DBTOs.DBDAO>, obj: DBTOs.TestObject): DBTOs.TestIndex {
|
||||
val id = store.insert(obj);
|
||||
Assert.assertTrue(id > 0);
|
||||
|
||||
val dbObj = store.get(id);
|
||||
assertIndexEquals(dbObj, obj);
|
||||
return dbObj;
|
||||
}
|
||||
|
||||
private fun assertObjectEquals(obj1: DBTOs.TestObject, obj2: DBTOs.TestObject) {
|
||||
Assert.assertEquals(obj1.someStr, obj2.someStr);
|
||||
Assert.assertEquals(obj1.someNum, obj2.someNum);
|
||||
}
|
||||
private fun assertIndexEquals(obj1: DBTOs.TestIndex, obj2: DBTOs.TestObject) {
|
||||
Assert.assertEquals(obj1.someString, obj2.someStr);
|
||||
Assert.assertEquals(obj1.someNum, obj2.someNum);
|
||||
assertObjectEquals(obj1.obj, obj2);
|
||||
}
|
||||
|
||||
|
||||
class Descriptor: ManagedDBDescriptor<DBTOs.TestObject, DBTOs.TestIndex, DBTOs.DB, DBTOs.DBDAO>() {
|
||||
override val table_name: String = "testing";
|
||||
override fun indexClass(): KClass<DBTOs.TestIndex> = DBTOs.TestIndex::class;
|
||||
override fun dbClass(): KClass<DBTOs.DB> = DBTOs.DB::class;
|
||||
override fun create(obj: DBTOs.TestObject): DBTOs.TestIndex = DBTOs.TestIndex(obj);
|
||||
}
|
||||
}
|
|
@ -58,7 +58,7 @@ class StatePlaylists {
|
|||
|
||||
val historyIndex: ConcurrentMap<Any, DBHistory.Index> = ConcurrentHashMap();
|
||||
val _historyDBStore = ManagedDBStore.create("history", DBHistory.Descriptor())
|
||||
.withIndex({ it.url }, historyIndex, true)
|
||||
.withIndex({ it.url }, historyIndex, false, true)
|
||||
.load();
|
||||
|
||||
val playlistShareDir = FragmentedStorage.getOrCreateDirectory("shares");
|
||||
|
|
|
@ -15,6 +15,8 @@ interface ManagedDBDAOBase<T, I: ManagedDBIndex<T>> {
|
|||
@RawQuery
|
||||
fun get(query: SupportSQLiteQuery): I;
|
||||
@RawQuery
|
||||
fun getNullable(query: SupportSQLiteQuery): I?;
|
||||
@RawQuery
|
||||
fun getMultiple(query: SupportSQLiteQuery): List<I>;
|
||||
|
||||
@RawQuery
|
||||
|
|
|
@ -13,5 +13,12 @@ open class ManagedDBIndex<T> {
|
|||
var serialized: ByteArray? = null;
|
||||
|
||||
@Ignore
|
||||
var obj: T? = null;
|
||||
private var _obj: T? = null;
|
||||
|
||||
@get:Ignore
|
||||
val obj: T get() = _obj ?: throw IllegalStateException("Attempted to access serialized object on a index-only instance");
|
||||
|
||||
fun setInstance(obj: T) {
|
||||
this._obj = obj;
|
||||
}
|
||||
}
|
|
@ -1,15 +1,13 @@
|
|||
package com.futo.platformplayer.stores.db
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Room
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||
import com.futo.platformplayer.api.media.structures.AdhocPager
|
||||
import com.futo.platformplayer.api.media.structures.IPager
|
||||
import com.futo.platformplayer.assume
|
||||
import com.futo.platformplayer.models.HistoryVideo
|
||||
import com.futo.platformplayer.states.StateApp
|
||||
import com.futo.platformplayer.stores.db.types.DBHistory
|
||||
import com.futo.platformplayer.stores.v2.JsonStoreSerializer
|
||||
import com.futo.platformplayer.stores.v2.StoreSerializer
|
||||
import kotlinx.serialization.KSerializer
|
||||
|
@ -37,6 +35,7 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
|||
private val _columnInfo: List<ColumnMetadata>;
|
||||
|
||||
private val _sqlGet: (Long)-> SimpleSQLiteQuery;
|
||||
private val _sqlGetIndex: (Long)-> SimpleSQLiteQuery;
|
||||
private val _sqlGetAll: (LongArray)-> SimpleSQLiteQuery;
|
||||
private val _sqlAll: SimpleSQLiteQuery;
|
||||
private val _sqlCount: SimpleSQLiteQuery;
|
||||
|
@ -49,7 +48,7 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
|||
|
||||
val name: String;
|
||||
|
||||
private val _indexes: ArrayList<Pair<(I)->Any, ConcurrentMap<Any, I>>> = arrayListOf();
|
||||
private val _indexes: ArrayList<IndexDescriptor<I>> = arrayListOf();
|
||||
private val _indexCollection = ConcurrentHashMap<Long, I>();
|
||||
|
||||
private var _withUnique: Pair<(I)->Any, ConcurrentMap<Any, I>>? = null;
|
||||
|
@ -76,6 +75,7 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
|||
else "";
|
||||
|
||||
_sqlGet = { SimpleSQLiteQuery("SELECT * FROM ${_dbDescriptor.table_name} WHERE id = ?", arrayOf(it)) };
|
||||
_sqlGetIndex = { SimpleSQLiteQuery("SELECT ${indexColumnNames.joinToString(", ")} FROM ${_dbDescriptor.table_name} WHERE id = ?", arrayOf(it)) };
|
||||
_sqlGetAll = { SimpleSQLiteQuery("SELECT * FROM ${_dbDescriptor.table_name} WHERE id IN (?)", arrayOf(it)) };
|
||||
_sqlAll = SimpleSQLiteQuery("SELECT * FROM ${_dbDescriptor.table_name} ${orderSQL}");
|
||||
_sqlCount = SimpleSQLiteQuery("SELECT COUNT(id) FROM ${_dbDescriptor.table_name}");
|
||||
|
@ -90,10 +90,10 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
|||
}
|
||||
}
|
||||
|
||||
fun withIndex(keySelector: (I)->Any, indexContainer: ConcurrentMap<Any, I>, withUnique: Boolean = false): ManagedDBStore<I, T, D, DA> {
|
||||
fun withIndex(keySelector: (I)->Any, indexContainer: ConcurrentMap<Any, I>, allowChange: Boolean = false, withUnique: Boolean = false): ManagedDBStore<I, T, D, DA> {
|
||||
if(_sqlIndexed == null)
|
||||
throw IllegalStateException("Can only create indexes if sqlIndexOnly is implemented");
|
||||
_indexes.add(Pair(keySelector, indexContainer));
|
||||
_indexes.add(IndexDescriptor(keySelector, indexContainer, allowChange));
|
||||
|
||||
if(withUnique)
|
||||
withUnique(keySelector, indexContainer);
|
||||
|
@ -108,8 +108,11 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
|||
return this;
|
||||
}
|
||||
|
||||
fun load(): ManagedDBStore<I, T, D, DA> {
|
||||
_db = Room.databaseBuilder(StateApp.instance.context, _dbDescriptor.dbClass().java, _name)
|
||||
fun load(context: Context? = null, inMemory: Boolean = false): ManagedDBStore<I, T, D, DA> {
|
||||
_db = (if(!inMemory)
|
||||
Room.databaseBuilder(context ?: StateApp.instance.context, _dbDescriptor.dbClass().java, _name)
|
||||
else
|
||||
Room.inMemoryDatabaseBuilder(context ?: StateApp.instance.context, _dbDescriptor.dbClass().java))
|
||||
.fallbackToDestructiveMigration()
|
||||
.allowMainThreadQueries()
|
||||
.build()
|
||||
|
@ -117,11 +120,17 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
|||
if(_indexes.any()) {
|
||||
val allItems = _dbDaoBase!!.getMultiple(_sqlIndexed!!);
|
||||
for(index in _indexes)
|
||||
index.second.putAll(allItems.associateBy(index.first));
|
||||
index.collection.putAll(allItems.associateBy(index.keySelector));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
fun shutdown() {
|
||||
val db = _db;
|
||||
_db = null;
|
||||
_dbDaoBase = null;
|
||||
db?.close();
|
||||
}
|
||||
|
||||
fun getUnique(obj: I): I? {
|
||||
if(_withUnique == null)
|
||||
|
@ -142,9 +151,12 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
|||
|
||||
fun insert(obj: T): Long {
|
||||
val newIndex = _dbDescriptor.create(obj);
|
||||
val unique = getUnique(newIndex);
|
||||
if(unique != null)
|
||||
return unique.id!!;
|
||||
|
||||
if(_withUnique != null) {
|
||||
val unique = getUnique(newIndex);
|
||||
if (unique != null)
|
||||
return unique.id!!;
|
||||
}
|
||||
|
||||
newIndex.serialized = serialize(obj);
|
||||
newIndex.id = dbDaoBase.insert(newIndex);
|
||||
|
@ -153,13 +165,15 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
|||
|
||||
if(!_indexes.isEmpty()) {
|
||||
for (index in _indexes) {
|
||||
val key = index.first(newIndex);
|
||||
index.second.put(key, newIndex);
|
||||
val key = index.keySelector(newIndex);
|
||||
index.collection.put(key, newIndex);
|
||||
}
|
||||
}
|
||||
return newIndex.id!!;
|
||||
}
|
||||
fun update(id: Long, obj: T) {
|
||||
val existing = if(_indexes.any { it.checkChange }) _dbDaoBase!!.getNullable(_sqlGetIndex(id)) else null
|
||||
|
||||
val newIndex = _dbDescriptor.create(obj);
|
||||
newIndex.id = id;
|
||||
newIndex.serialized = serialize(obj);
|
||||
|
@ -168,8 +182,13 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
|||
|
||||
if(!_indexes.isEmpty()) {
|
||||
for (index in _indexes) {
|
||||
val key = index.first(newIndex);
|
||||
index.second.put(key, newIndex);
|
||||
val key = index.keySelector(newIndex);
|
||||
if(index.checkChange && existing != null) {
|
||||
val keyExisting = index.keySelector(existing);
|
||||
if(keyExisting != key)
|
||||
index.collection.remove(keyExisting);
|
||||
}
|
||||
index.collection.put(key, newIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -189,6 +208,15 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
|||
fun get(id: Long): I {
|
||||
return deserializeIndex(dbDaoBase.get(_sqlGet(id)));
|
||||
}
|
||||
fun getOrNull(id: Long): I? {
|
||||
val result = dbDaoBase.getNullable(_sqlGet(id));
|
||||
if(result == null)
|
||||
return null;
|
||||
return deserializeIndex(result);
|
||||
}
|
||||
fun getIndexOnlyOrNull(id: Long): I? {
|
||||
return dbDaoBase.get(_sqlGetIndex(id));
|
||||
}
|
||||
|
||||
fun getAllObjects(vararg id: Long): List<T> = getAll(*id).map { it.obj!! };
|
||||
fun getAll(vararg id: Long): List<I> {
|
||||
|
@ -217,19 +245,20 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
|||
dbDaoBase.delete(item);
|
||||
|
||||
for(index in _indexes)
|
||||
index.second.remove(index.first(item));
|
||||
index.collection.remove(index.keySelector(item));
|
||||
}
|
||||
fun delete(id: Long) {
|
||||
dbDaoBase.action(_sqlDeleteById(id));
|
||||
|
||||
for(index in _indexes)
|
||||
index.second.values.removeIf { it.id == id }
|
||||
index.collection.values.removeIf { it.id == id }
|
||||
}
|
||||
fun deleteAll() {
|
||||
dbDaoBase.action(_sqlDeleteAll);
|
||||
|
||||
_indexCollection.clear();
|
||||
for(index in _indexes)
|
||||
index.second.clear();
|
||||
index.collection.clear();
|
||||
}
|
||||
|
||||
fun convertObject(index: I): T? {
|
||||
|
@ -241,7 +270,7 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
|||
fun deserializeIndex(index: I): I {
|
||||
if(index.serialized == null) throw IllegalStateException("Cannot deserialize index-only items from [${name}]");
|
||||
val obj = _serializer.deserialize(_class, index.serialized!!);
|
||||
index.obj = obj;
|
||||
index.setInstance(obj);
|
||||
index.serialized = null;
|
||||
return index;
|
||||
}
|
||||
|
@ -260,6 +289,13 @@ class ManagedDBStore<I: ManagedDBIndex<T>, T, D: ManagedDBDatabase<T, I, DA>, DA
|
|||
= ManagedDBStore(name, descriptor, kotlin.reflect.typeOf<T>(), JsonStoreSerializer.create(serializer));
|
||||
}
|
||||
|
||||
//Pair<(I)->Any, ConcurrentMap<Any, I>>
|
||||
class IndexDescriptor<I>(
|
||||
val keySelector: (I) -> Any,
|
||||
val collection: ConcurrentMap<Any, I>,
|
||||
val checkChange: Boolean
|
||||
)
|
||||
|
||||
class ColumnMetadata(
|
||||
val field: Field,
|
||||
val info: ColumnIndex,
|
||||
|
|
|
@ -24,7 +24,6 @@ class DBChannelCache {
|
|||
constructor(sCache: SerializedPlatformContent) {
|
||||
id = null;
|
||||
serialized = null;
|
||||
obj = sCache;
|
||||
channelUrl = sCache.author.url;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,7 +66,6 @@ class DBHistory {
|
|||
url = historyVideo.video.url;
|
||||
position = historyVideo.position;
|
||||
date = historyVideo.date.toEpochSecond();
|
||||
obj = historyVideo;
|
||||
}
|
||||
}
|
||||
}
|
47
app/src/main/java/com/futo/platformplayer/testing/DBTOs.kt
Normal file
47
app/src/main/java/com/futo/platformplayer/testing/DBTOs.kt
Normal file
|
@ -0,0 +1,47 @@
|
|||
package com.futo.platformplayer.testing
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Database
|
||||
import androidx.room.Entity
|
||||
import com.futo.platformplayer.stores.db.ColumnIndex
|
||||
import com.futo.platformplayer.stores.db.ColumnOrdered
|
||||
import com.futo.platformplayer.stores.db.ManagedDBDAOBase
|
||||
import com.futo.platformplayer.stores.db.ManagedDBDatabase
|
||||
import com.futo.platformplayer.stores.db.ManagedDBIndex
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.Random
|
||||
import java.util.UUID
|
||||
|
||||
class DBTOs {
|
||||
@Dao
|
||||
interface DBDAO: ManagedDBDAOBase<TestObject, TestIndex> {}
|
||||
@Database(entities = [TestIndex::class], version = 2)
|
||||
abstract class DB: ManagedDBDatabase<TestObject, TestIndex, DBDAO>() {
|
||||
abstract override fun base(): DBDAO;
|
||||
}
|
||||
|
||||
|
||||
@Entity("testing")
|
||||
class TestIndex(): ManagedDBIndex<TestObject>() {
|
||||
|
||||
@ColumnIndex
|
||||
var someString: String = "";
|
||||
@ColumnIndex
|
||||
@ColumnOrdered(0)
|
||||
var someNum: Int = 0;
|
||||
|
||||
constructor(obj: TestObject, customInt: Int? = null) : this() {
|
||||
someString = obj.someStr;
|
||||
someNum = customInt ?: obj.someNum;
|
||||
}
|
||||
}
|
||||
@Serializable
|
||||
class TestObject {
|
||||
var someStr = UUID.randomUUID().toString();
|
||||
var someNum = random.nextInt();
|
||||
}
|
||||
|
||||
companion object {
|
||||
val random = Random();
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue