diff --git a/build-logic/structure/src/main/kotlin/AndroidLibraryPlugin.kt b/build-logic/structure/src/main/kotlin/AndroidLibraryPlugin.kt index bc3294a1..73c4d044 100644 --- a/build-logic/structure/src/main/kotlin/AndroidLibraryPlugin.kt +++ b/build-logic/structure/src/main/kotlin/AndroidLibraryPlugin.kt @@ -42,8 +42,6 @@ class AndroidLibraryPlugin : Plugin { dependencies { add("implementation", kotlin2("stdlib", libs)) add("implementation", kotlin2("reflect", libs)) - add("testImplementation", kotlin2("test", libs)) - add("androidTestImplementation", kotlin2("test", libs)) } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5bc6145..8d3b626f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -76,6 +76,7 @@ hilt-android-testing = { group = "com.google.dagger", name = "hilt-android-testi leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version.ref = "leakcanary" } junit4 = { group = "junit", name = "junit", version.ref = "junit4" } jackson-core = { group = "com.fasterxml.jackson.core", name = "jackson-core", version.ref = "jackson" } +kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" } kotlinx-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } kotlinx-coroutines-guava = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-guava", version.ref = "kotlinxCoroutines" } @@ -124,4 +125,5 @@ looker-room = { id = "looker.room", version = "unspecified" } looker-serialization = { id = "looker.serialization", version = "unspecified" } [bundles] -test-ui = ["androidx-test-runner", "androidx-test-rules", "androidx-test-espresso-core", "androidx-test-uiautomator"] +test-android = ["androidx-test-runner", "androidx-test-rules", "androidx-test-ext", "androidx-test-espresso-core", "kotlinx-coroutines-test", "kotlin-test"] +test-ui = ["androidx-test-runner", "androidx-test-rules", "androidx-test-espresso-core", "androidx-test-uiautomator", "kotlinx-coroutines-test", "kotlin-test"] diff --git a/sync/fdroid/build.gradle.kts b/sync/fdroid/build.gradle.kts index 0d05562f..6c26d927 100644 --- a/sync/fdroid/build.gradle.kts +++ b/sync/fdroid/build.gradle.kts @@ -16,7 +16,5 @@ dependencies { ) implementation(libs.kotlinx.coroutines.core) - testImplementation(libs.ktor.mock) - testImplementation(libs.kotlinx.coroutines.test) - testRuntimeOnly(kotlin("test")) + androidTestImplementation(libs.bundles.test.android) } diff --git a/sync/fdroid/src/test/resources/izzy_diff.json b/sync/fdroid/src/androidTest/assets/izzy_diff.json similarity index 100% rename from sync/fdroid/src/test/resources/izzy_diff.json rename to sync/fdroid/src/androidTest/assets/izzy_diff.json diff --git a/sync/fdroid/src/test/resources/izzy_entry.jar b/sync/fdroid/src/androidTest/assets/izzy_entry.jar similarity index 100% rename from sync/fdroid/src/test/resources/izzy_entry.jar rename to sync/fdroid/src/androidTest/assets/izzy_entry.jar diff --git a/sync/fdroid/src/test/resources/izzy_entry.json b/sync/fdroid/src/androidTest/assets/izzy_entry.json similarity index 100% rename from sync/fdroid/src/test/resources/izzy_entry.json rename to sync/fdroid/src/androidTest/assets/izzy_entry.json diff --git a/sync/fdroid/src/test/resources/izzy_index_v1.jar b/sync/fdroid/src/androidTest/assets/izzy_index_v1.jar similarity index 100% rename from sync/fdroid/src/test/resources/izzy_index_v1.jar rename to sync/fdroid/src/androidTest/assets/izzy_index_v1.jar diff --git a/sync/fdroid/src/test/resources/izzy_index_v1.json b/sync/fdroid/src/androidTest/assets/izzy_index_v1.json similarity index 100% rename from sync/fdroid/src/test/resources/izzy_index_v1.json rename to sync/fdroid/src/androidTest/assets/izzy_index_v1.json diff --git a/sync/fdroid/src/test/resources/izzy_index_v2.json b/sync/fdroid/src/androidTest/assets/izzy_index_v2.json similarity index 100% rename from sync/fdroid/src/test/resources/izzy_index_v2.json rename to sync/fdroid/src/androidTest/assets/izzy_index_v2.json diff --git a/sync/fdroid/src/test/resources/izzy_index_v2_updated.json b/sync/fdroid/src/androidTest/assets/izzy_index_v2_updated.json similarity index 100% rename from sync/fdroid/src/test/resources/izzy_index_v2_updated.json rename to sync/fdroid/src/androidTest/assets/izzy_index_v2_updated.json diff --git a/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/Downloader.kt b/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/Downloader.kt new file mode 100644 index 00000000..16ac58ac --- /dev/null +++ b/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/Downloader.kt @@ -0,0 +1,48 @@ +package com.looker.sync.fdroid + +import com.looker.core.common.extension.writeTo +import com.looker.network.Downloader +import com.looker.network.NetworkResponse +import com.looker.network.ProgressListener +import com.looker.network.header.HeadersBuilder +import com.looker.network.validation.FileValidator +import com.looker.sync.fdroid.common.assets +import java.io.File +import java.net.Proxy + +val FakeDownloader = object : Downloader { + override fun setProxy(proxy: Proxy) { + TODO("Not yet implemented") + } + + override suspend fun headCall( + url: String, + headers: HeadersBuilder.() -> Unit + ): NetworkResponse { + TODO("Not yet implemented") + } + + override suspend fun downloadToFile( + url: String, + target: File, + validator: FileValidator?, + headers: HeadersBuilder.() -> Unit, + block: ProgressListener? + ): NetworkResponse { + return if (url.endsWith("fail")) NetworkResponse.Error.Unknown(Exception("You asked for it")) + else { + val index = when { + url.endsWith("index-v1.jar") -> assets("izzy_index_v1.jar") + url.endsWith("index-v2.json") -> assets("izzy_index_v2.json") + url.endsWith("entry.jar") -> assets("izzy_entry.jar") + url.endsWith("/diff/1725731263000.json") -> assets("izzy_diff.json") + // Just in case we try these in future + url.endsWith("index-v1.json") -> assets("izzy_index_v1.json") + url.endsWith("entry.json") -> assets("izzy_entry.json") + else -> error("Unknown URL: $url") + } + index.writeTo(target) + NetworkResponse.Success(200, null, null) + } + } +} diff --git a/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/EntrySyncableTest.kt b/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/EntrySyncableTest.kt new file mode 100644 index 00000000..28bb773c --- /dev/null +++ b/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/EntrySyncableTest.kt @@ -0,0 +1,107 @@ +package com.looker.sync.fdroid + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.looker.core.domain.model.Repo +import com.looker.sync.fdroid.common.Izzy +import com.looker.sync.fdroid.common.JsonParser +import com.looker.sync.fdroid.common.assets +import com.looker.sync.fdroid.common.memory +import com.looker.sync.fdroid.v2.EntrySyncable +import com.looker.sync.fdroid.v2.model.Entry +import com.looker.sync.fdroid.v2.model.IndexV2 +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.decodeFromStream +import org.junit.Before +import org.junit.runner.RunWith +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +@RunWith(AndroidJUnit4::class) +class EntrySyncableTest { + + private lateinit var dispatcher: CoroutineDispatcher + private lateinit var context: Context + private lateinit var syncable: Syncable + private lateinit var repo: Repo + private lateinit var newIndex: IndexV2 + + /** + * In this particular test 1 package is removed and 36 packages are updated + */ + + @OptIn(ExperimentalSerializationApi::class) + @Before + fun before() { + context = InstrumentationRegistry.getInstrumentation().context + dispatcher = StandardTestDispatcher() + syncable = EntrySyncable(context, FakeDownloader, dispatcher) + newIndex = JsonParser.parser.decodeFromStream(assets("izzy_index_v2_updated.json")) + repo = Izzy + } + + // Not very trustworthy + @Test + fun benchmark_sync_full() = runTest(dispatcher) { + memory("Full Benchmark") { + syncable.sync(repo) + } + memory("Diff Benchmark") { + syncable.sync(repo) + } + } + + @Test + fun check_if_patch_applies() = runTest(dispatcher) { + // Downloads old index file as the index file does not exist + val (fingerprint1, index1) = syncable.sync(repo) + assert(index1 != null) + // Downloads the diff as the index file exists and is older than entry version + val (fingerprint2, index2) = syncable.sync( + repo.copy( + versionInfo = repo.versionInfo.copy( + timestamp = index1!!.repo.timestamp + ) + ) + ) + assert(index2 != null) + // Does not download anything + val (fingerprint3, index3) = syncable.sync( + repo.copy( + versionInfo = repo.versionInfo.copy( + timestamp = index2!!.repo.timestamp + ) + ) + ) + assert(index3 == null) + + // Check if all the packages are same + assertContentEquals(newIndex.packages.keys.sorted(), index2.packages.keys.sorted()) + // Check if all the version hashes are same + assertContentEquals( + newIndex.packages.values.flatMap { it.versions.keys }.sorted(), + index2.packages.values.flatMap { it.versions.keys }.sorted(), + ) + + // Check if repo antifeatures are same + assertContentEquals( + newIndex.repo.antiFeatures.keys.sorted(), + index2.repo.antiFeatures.keys.sorted() + ) + + // Check if repo categories are same + assertContentEquals( + newIndex.repo.categories.keys.sorted(), + index2.repo.categories.keys.sorted() + ) + + assertEquals(fingerprint1, fingerprint2) + assertEquals(fingerprint2, fingerprint3) + } + +} diff --git a/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/IndexValidator.kt b/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/IndexValidator.kt new file mode 100644 index 00000000..41564db3 --- /dev/null +++ b/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/IndexValidator.kt @@ -0,0 +1,13 @@ +package com.looker.sync.fdroid + +import com.looker.core.domain.model.Fingerprint +import java.util.jar.JarEntry + +val FakeIndexValidator = object : IndexValidator { + override suspend fun validate( + jarEntry: JarEntry, + expectedFingerprint: Fingerprint? + ): Fingerprint { + return expectedFingerprint ?: Fingerprint("0".repeat(64)) + } +} diff --git a/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/common/Benchmark.kt b/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/common/Benchmark.kt new file mode 100644 index 00000000..3e995968 --- /dev/null +++ b/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/common/Benchmark.kt @@ -0,0 +1,24 @@ +package com.looker.sync.fdroid.common + +import com.looker.network.DataSize +import kotlin.time.measureTime + +internal inline fun memory( + extraMessage: String? = null, + block: () -> Unit, +) { + val runtime = Runtime.getRuntime() + if (extraMessage != null) { + println("=".repeat(50)) + println(extraMessage) + } + println("=".repeat(50)) + val initial = runtime.freeMemory() + val time = measureTime { + block() + } + val final = runtime.freeMemory() + println("Time Taken: ${time}, Usage: ${DataSize(initial - final)} / ${DataSize(runtime.maxMemory())}") + println("=".repeat(50)) + println() +} diff --git a/sync/fdroid/src/test/kotlin/com/looker/sync/fdroid/common/Repo.kt b/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/common/Repo.kt similarity index 89% rename from sync/fdroid/src/test/kotlin/com/looker/sync/fdroid/common/Repo.kt rename to sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/common/Repo.kt index 5c04500c..447ae0fc 100644 --- a/sync/fdroid/src/test/kotlin/com/looker/sync/fdroid/common/Repo.kt +++ b/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/common/Repo.kt @@ -4,7 +4,6 @@ import com.looker.core.domain.model.Authentication import com.looker.core.domain.model.Fingerprint import com.looker.core.domain.model.Repo import com.looker.core.domain.model.VersionInfo -import kotlin.math.truncate val Izzy = Repo( id = 1L, @@ -12,7 +11,7 @@ val Izzy = Repo( address = "https://apt.izzysoft.de/fdroid/repo", name = "IzzyOnDroid F-Droid Repo", description = "This is a repository of apps to be used with F-Droid. Applications in this repository are official binaries built by the original application developers, taken from their resp. repositories (mostly Github, GitLab, Codeberg). Updates for the apps are usually fetched daily, and you can expect daily index updates.", - fingerprint = Fingerprint("0".repeat(64)), + fingerprint = Fingerprint("3BF0D6ABFEAE2F401707B6D966BE743BF0EEE49C2561B9BA39073711F628937A"), authentication = Authentication("", ""), versionInfo = VersionInfo(0L, null), mirrors = emptyList(), diff --git a/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/common/Resource.kt b/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/common/Resource.kt new file mode 100644 index 00000000..f4618b9d --- /dev/null +++ b/sync/fdroid/src/androidTest/kotlin/com/looker/sync/fdroid/common/Resource.kt @@ -0,0 +1,14 @@ +package com.looker.sync.fdroid.common + +import androidx.test.platform.app.InstrumentationRegistry +import java.io.File +import java.io.InputStream + +fun getResource(name: String): File? { + val url = Thread.currentThread().contextClassLoader?.getResource(name) ?: return null + return File(url.file) +} + +fun assets(name: String): InputStream { + return InstrumentationRegistry.getInstrumentation().context.assets.open(name) +} diff --git a/sync/fdroid/src/test/kotlin/com/looker/sync/fdroid/V1ParserTest.kt b/sync/fdroid/src/test/kotlin/com/looker/sync/fdroid/V1ParserTest.kt deleted file mode 100644 index 71491242..00000000 --- a/sync/fdroid/src/test/kotlin/com/looker/sync/fdroid/V1ParserTest.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.looker.sync.fdroid - -import com.looker.core.domain.model.Fingerprint -import com.looker.network.DataSize -import com.looker.sync.fdroid.common.Izzy -import com.looker.sync.fdroid.common.getResource -import com.looker.sync.fdroid.common.toV2 -import com.looker.sync.fdroid.v1.V1Parser -import com.looker.sync.fdroid.v2.V2Parser -import kotlinx.coroutines.async -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.runTest -import kotlinx.serialization.json.Json -import java.util.jar.JarEntry -import kotlin.coroutines.cancellation.CancellationException -import kotlin.test.Test -import kotlin.test.assertContentEquals -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith -import kotlin.time.measureTime - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class V1ParserTest { - - private val dispatcher = StandardTestDispatcher() - private val json = Json { - ignoreUnknownKeys = true - prettyPrint = true - } - private val validator = object : IndexValidator { - override suspend fun validate( - jarEntry: JarEntry, - expectedFingerprint: Fingerprint? - ): Fingerprint { - return expectedFingerprint ?: Fingerprint("0".repeat(64)) - } - - } - private val v1Parser = V1Parser(dispatcher, json, validator) - private val v2Parser = V2Parser(dispatcher, json) - private val jarFile = getResource("izzy_index_v1.jar") - private val v2JsonFile = getResource("izzy_index_v2.json") - private val repo = Izzy - - @Test - fun `parse v1 json and compare with v2`() = runTest(dispatcher) { - requireNotNull(jarFile) - requireNotNull(v2JsonFile) - memory { - val (_, indexV1) = v1Parser.parse(jarFile, repo) - val convertedIndex = indexV1.toV2() - val (_, indexV2) = v2Parser.parse(v2JsonFile, repo) - assertEquals(indexV2.packages.size, convertedIndex.packages.size) - assertContentEquals( - indexV2.packages.keys.sorted(), - convertedIndex.packages.keys.sorted() - ) - } - } - - @Test - fun `check cancellation`() = runTest(dispatcher) { - requireNotNull(jarFile) - val job = async { v1Parser.parse(jarFile, repo) } - job.cancel() - assertFailsWith { job.await() } - } -} - -internal inline fun memory( - block: () -> Unit, -) { - val runtime = Runtime.getRuntime() - println("=".repeat(50)) - val initial = runtime.freeMemory() - val time = measureTime { - block() - } - val final = runtime.freeMemory() - println("Time Taken: ${time}, Usage: ${DataSize(initial - final)} / ${DataSize(runtime.maxMemory())}") - println("=".repeat(50)) - println() -} diff --git a/sync/fdroid/src/test/kotlin/com/looker/sync/fdroid/common/Resource.kt b/sync/fdroid/src/test/kotlin/com/looker/sync/fdroid/common/Resource.kt deleted file mode 100644 index 48913152..00000000 --- a/sync/fdroid/src/test/kotlin/com/looker/sync/fdroid/common/Resource.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.looker.sync.fdroid.common - -import java.io.File - -fun getResource(name: String): File? { - val url = Thread.currentThread().contextClassLoader.getResource(name) ?: return null - return File(url.file) -}