Add unit tests for Checker
This commit is contained in:
parent
beedafd042
commit
166f81b3a8
2 changed files with 318 additions and 13 deletions
|
@ -50,24 +50,29 @@ internal class Checker(
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
suspend fun getBackupSize(): Long? {
|
suspend fun getBackupSize(): Long? {
|
||||||
// get all snapshots
|
return try {
|
||||||
val folder = TopLevelFolder(crypto.repoId)
|
getBackupSizeInt()
|
||||||
val handles = mutableListOf<AppBackupFileType.Snapshot>()
|
|
||||||
try {
|
|
||||||
backendManager.backend.list(folder, AppBackupFileType.Snapshot::class) { fileInfo ->
|
|
||||||
handles.add(fileInfo.fileHandle as AppBackupFileType.Snapshot)
|
|
||||||
}
|
|
||||||
val snapshots = snapshotManager.onSnapshotsLoaded(handles)
|
|
||||||
this.snapshots = snapshots // remember loaded snapshots
|
|
||||||
this.handleSize = handles.size // remember number of snapshot handles we had
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error(e) { "Error loading snapshots: " }
|
log.error(e) { "Error loading snapshots: " }
|
||||||
// we swallow this exception, because an error will be shown in the next step
|
// we swallow this exception, because an error will be shown in the next step
|
||||||
return null
|
null
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getBackupSizeInt(): Long {
|
||||||
|
// get all snapshots
|
||||||
|
val folder = TopLevelFolder(crypto.repoId)
|
||||||
|
val handles = mutableListOf<AppBackupFileType.Snapshot>()
|
||||||
|
backendManager.backend.list(folder, AppBackupFileType.Snapshot::class) { fileInfo ->
|
||||||
|
handles.add(fileInfo.fileHandle as AppBackupFileType.Snapshot)
|
||||||
|
}
|
||||||
|
val snapshots = snapshotManager.onSnapshotsLoaded(handles)
|
||||||
|
this.snapshots = snapshots // remember loaded snapshots
|
||||||
|
this.handleSize = handles.size // remember number of snapshot handles we had
|
||||||
|
|
||||||
// get total disk space used by snapshots
|
// get total disk space used by snapshots
|
||||||
val sizeMap = mutableMapOf<String, Int>()
|
val sizeMap = mutableMapOf<String, Int>()
|
||||||
snapshots?.forEach { snapshot ->
|
snapshots.forEach { snapshot ->
|
||||||
// add sizes to a map first, so we don't double count
|
// add sizes to a map first, so we don't double count
|
||||||
snapshot.blobsMap.forEach { (chunkId, blob) -> sizeMap[chunkId] = blob.length }
|
snapshot.blobsMap.forEach { (chunkId, blob) -> sizeMap[chunkId] = blob.length }
|
||||||
}
|
}
|
||||||
|
@ -79,10 +84,11 @@ internal class Checker(
|
||||||
check(percent in 0..100) { "Percent $percent out of bounds." }
|
check(percent in 0..100) { "Percent $percent out of bounds." }
|
||||||
|
|
||||||
if (snapshots == null) try {
|
if (snapshots == null) try {
|
||||||
getBackupSize() // just get size again to be sure we get snapshots
|
getBackupSizeInt() // just get size again to be sure we get snapshots
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
nm.onCheckFinishedWithError(0, 0)
|
nm.onCheckFinishedWithError(0, 0)
|
||||||
checkerResult = CheckerResult.GeneralError(e)
|
checkerResult = CheckerResult.GeneralError(e)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
val snapshots = snapshots ?: error("Snapshots still null")
|
val snapshots = snapshots ?: error("Snapshots still null")
|
||||||
val handleSize = handleSize ?: error("Handle size still null")
|
val handleSize = handleSize ?: error("Handle size still null")
|
||||||
|
|
299
app/src/test/java/com/stevesoltys/seedvault/repo/CheckerTest.kt
Normal file
299
app/src/test/java/com/stevesoltys/seedvault/repo/CheckerTest.kt
Normal file
|
@ -0,0 +1,299 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2024 The Calyx Institute
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.stevesoltys.seedvault.repo
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString
|
||||||
|
import com.stevesoltys.seedvault.backend.BackendManager
|
||||||
|
import com.stevesoltys.seedvault.getRandomByteArray
|
||||||
|
import com.stevesoltys.seedvault.proto.Snapshot
|
||||||
|
import com.stevesoltys.seedvault.proto.SnapshotKt.blob
|
||||||
|
import com.stevesoltys.seedvault.proto.copy
|
||||||
|
import com.stevesoltys.seedvault.transport.TransportTest
|
||||||
|
import com.stevesoltys.seedvault.ui.notification.BackupNotificationManager
|
||||||
|
import io.mockk.Runs
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.coVerify
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.calyxos.seedvault.core.backends.AppBackupFileType
|
||||||
|
import org.calyxos.seedvault.core.backends.Backend
|
||||||
|
import org.calyxos.seedvault.core.backends.FileInfo
|
||||||
|
import org.calyxos.seedvault.core.backends.TopLevelFolder
|
||||||
|
import org.calyxos.seedvault.core.toHexString
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Assertions.assertInstanceOf
|
||||||
|
import org.junit.jupiter.api.Assertions.assertNull
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.security.GeneralSecurityException
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
internal class CheckerTest : TransportTest() {
|
||||||
|
|
||||||
|
private val backendManager: BackendManager = mockk()
|
||||||
|
private val snapshotManager: SnapshotManager = mockk()
|
||||||
|
private val loader: Loader = mockk()
|
||||||
|
private val nm: BackupNotificationManager = mockk()
|
||||||
|
private val backend: Backend = mockk()
|
||||||
|
|
||||||
|
private val checker = Checker(crypto, backendManager, snapshotManager, loader, nm)
|
||||||
|
private val folder = TopLevelFolder(repoId)
|
||||||
|
|
||||||
|
private val snapshotHandle1 =
|
||||||
|
AppBackupFileType.Snapshot(repoId, getRandomByteArray(32).toHexString())
|
||||||
|
private val snapshotHandle2 =
|
||||||
|
AppBackupFileType.Snapshot(repoId, getRandomByteArray(32).toHexString())
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getBackupSize returns 0 for no data`() = runBlocking {
|
||||||
|
expectLoadingSnapshots(emptyMap())
|
||||||
|
|
||||||
|
assertEquals(0, checker.getBackupSize())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getBackupSize returns null on error`() = runBlocking {
|
||||||
|
every { crypto.repoId } returns repoId
|
||||||
|
every { backendManager.backend } returns backend
|
||||||
|
coEvery {
|
||||||
|
backend.list(folder, AppBackupFileType.Snapshot::class, callback = captureLambda())
|
||||||
|
} throws IOException()
|
||||||
|
|
||||||
|
assertNull(checker.getBackupSize())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `getBackupSize returns size without double counting blobs`() = runBlocking {
|
||||||
|
val snapshotMap = mapOf(
|
||||||
|
snapshotHandle1 to snapshot.copy { token = 1 },
|
||||||
|
snapshotHandle2 to snapshot.copy { token = 2 },
|
||||||
|
)
|
||||||
|
val expectedSize = blob1.length.toLong() + blob2.length.toLong()
|
||||||
|
expectLoadingSnapshots(snapshotMap)
|
||||||
|
|
||||||
|
assertEquals(expectedSize, checker.getBackupSize())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `check works even with no backup data`() = runBlocking {
|
||||||
|
expectLoadingSnapshots(emptyMap())
|
||||||
|
|
||||||
|
every { nm.onCheckFinishedWithError(0, 0) } just Runs
|
||||||
|
|
||||||
|
assertNull(checker.checkerResult)
|
||||||
|
checker.check(100)
|
||||||
|
assertInstanceOf(CheckerResult.Error::class.java, checker.checkerResult)
|
||||||
|
val result = checker.checkerResult as CheckerResult.Error
|
||||||
|
assertEquals(emptyList<Snapshot>(), result.snapshots)
|
||||||
|
assertEquals(0, result.existingSnapshots)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `check returns error when loading snapshots fails`() = runBlocking {
|
||||||
|
every { crypto.repoId } returns repoId
|
||||||
|
every { backendManager.backend } returns backend
|
||||||
|
coEvery {
|
||||||
|
backend.list(folder, AppBackupFileType.Snapshot::class, callback = captureLambda())
|
||||||
|
} throws IOException("foo")
|
||||||
|
every { nm.onCheckFinishedWithError(0, 0) } just Runs
|
||||||
|
|
||||||
|
assertNull(checker.checkerResult)
|
||||||
|
checker.check(100)
|
||||||
|
// assert the right exception gets passed on in error result
|
||||||
|
assertInstanceOf(CheckerResult.GeneralError::class.java, checker.checkerResult)
|
||||||
|
val result = checker.checkerResult as CheckerResult.GeneralError
|
||||||
|
assertInstanceOf(IOException::class.java, result.e)
|
||||||
|
assertEquals("foo", result.e.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `check raises error for wrong chunkIDs`() = runBlocking {
|
||||||
|
val snapshotMap = mapOf(
|
||||||
|
snapshotHandle1 to snapshot.copy { token = 1 },
|
||||||
|
snapshotHandle2 to snapshot.copy { token = 2 },
|
||||||
|
)
|
||||||
|
expectLoadingSnapshots(snapshotMap)
|
||||||
|
|
||||||
|
val data = ByteArray(0)
|
||||||
|
coEvery { loader.loadFile(blobHandle1, null) } returns ByteArrayInputStream(data)
|
||||||
|
coEvery { loader.loadFile(blobHandle2, null) } returns ByteArrayInputStream(data)
|
||||||
|
|
||||||
|
every { nm.onCheckFinishedWithError(any(), any()) } just Runs
|
||||||
|
|
||||||
|
assertNull(checker.checkerResult)
|
||||||
|
checker.check(100)
|
||||||
|
assertInstanceOf(CheckerResult.Error::class.java, checker.checkerResult)
|
||||||
|
val result = checker.checkerResult as CheckerResult.Error
|
||||||
|
assertEquals(snapshotMap.values.toSet(), result.snapshots.toSet())
|
||||||
|
assertEquals(snapshotMap.values.toSet(), result.badSnapshots.toSet())
|
||||||
|
assertEquals(emptyList<Snapshot>(), result.goodSnapshots)
|
||||||
|
assertEquals(snapshotMap.size, result.existingSnapshots)
|
||||||
|
assertEquals(setOf(chunkId1, chunkId2), result.errorChunkIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `check raises error for loader failure`() = runBlocking {
|
||||||
|
// chunkId is "real"
|
||||||
|
val data1 = getRandomByteArray()
|
||||||
|
val chunkId1 = MessageDigest.getInstance("SHA-256").digest(data1).toHexString()
|
||||||
|
// each snapshot gets a different blob
|
||||||
|
val apk1 = apk.copy {
|
||||||
|
splits.clear()
|
||||||
|
splits.add(baseSplit.copy {
|
||||||
|
this.chunkIds.clear()
|
||||||
|
chunkIds.add(ByteString.fromHex(chunkId1))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
val apk2 = apk.copy {
|
||||||
|
splits.clear()
|
||||||
|
splits.add(baseSplit.copy {
|
||||||
|
this.chunkIds.clear()
|
||||||
|
chunkIds.add(ByteString.fromHex(chunkId2))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
val snapshotMap = mapOf(
|
||||||
|
snapshotHandle1 to snapshot.copy {
|
||||||
|
token = 1
|
||||||
|
apps[packageName] = app.copy { this.apk = apk1 }
|
||||||
|
blobs.clear()
|
||||||
|
blobs[chunkId1] = blob1
|
||||||
|
},
|
||||||
|
snapshotHandle2 to snapshot.copy {
|
||||||
|
token = 2
|
||||||
|
apps[packageName] = app.copy { this.apk = apk2 }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expectLoadingSnapshots(snapshotMap)
|
||||||
|
|
||||||
|
coEvery { loader.loadFile(blobHandle1, null) } returns ByteArrayInputStream(data1)
|
||||||
|
coEvery { loader.loadFile(blobHandle2, null) } throws GeneralSecurityException()
|
||||||
|
|
||||||
|
every { nm.onCheckFinishedWithError(any(), any()) } just Runs
|
||||||
|
|
||||||
|
assertNull(checker.checkerResult)
|
||||||
|
checker.check(100)
|
||||||
|
assertInstanceOf(CheckerResult.Error::class.java, checker.checkerResult)
|
||||||
|
val result = checker.checkerResult as CheckerResult.Error
|
||||||
|
assertEquals(snapshotMap.values.toSet(), result.snapshots.toSet())
|
||||||
|
assertEquals(listOf(snapshotMap[snapshotHandle1]), result.goodSnapshots)
|
||||||
|
assertEquals(listOf(snapshotMap[snapshotHandle2]), result.badSnapshots)
|
||||||
|
assertEquals(snapshotMap.size, result.existingSnapshots)
|
||||||
|
assertEquals(setOf(chunkId2), result.errorChunkIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `check with 100 percent works`() = runBlocking {
|
||||||
|
// get "real" data for blobs
|
||||||
|
val messageDigest = MessageDigest.getInstance("SHA-256")
|
||||||
|
val data1 = getRandomByteArray()
|
||||||
|
val data2 = getRandomByteArray()
|
||||||
|
val chunkId1 = messageDigest.digest(data1).toHexString()
|
||||||
|
val chunkId2 = messageDigest.digest(data2).toHexString()
|
||||||
|
val apk = apk.copy {
|
||||||
|
splits.clear()
|
||||||
|
splits.add(baseSplit.copy {
|
||||||
|
this.chunkIds.clear()
|
||||||
|
chunkIds.add(ByteString.fromHex(chunkId1))
|
||||||
|
})
|
||||||
|
splits.add(apkSplit.copy {
|
||||||
|
this.chunkIds.clear()
|
||||||
|
chunkIds.add(ByteString.fromHex(chunkId2))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
val snapshot = snapshot.copy {
|
||||||
|
apps[packageName] = app.copy { this.apk = apk }
|
||||||
|
blobs[chunkId1] = blob1
|
||||||
|
blobs[chunkId2] = blob2
|
||||||
|
}
|
||||||
|
val snapshotMap = mapOf(
|
||||||
|
snapshotHandle1 to snapshot.copy { token = 1 },
|
||||||
|
snapshotHandle2 to snapshot.copy { token = 2 },
|
||||||
|
)
|
||||||
|
val expectedSize = blob1.length.toLong() + blob2.length.toLong()
|
||||||
|
|
||||||
|
expectLoadingSnapshots(snapshotMap)
|
||||||
|
|
||||||
|
coEvery { loader.loadFile(blobHandle1, null) } returns ByteArrayInputStream(data1)
|
||||||
|
coEvery { loader.loadFile(blobHandle2, null) } returns ByteArrayInputStream(data2)
|
||||||
|
|
||||||
|
every { nm.onCheckComplete(expectedSize, any()) } just Runs
|
||||||
|
|
||||||
|
assertNull(checker.checkerResult)
|
||||||
|
checker.check(100)
|
||||||
|
assertInstanceOf(CheckerResult.Success::class.java, checker.checkerResult)
|
||||||
|
val result = checker.checkerResult as CheckerResult.Success
|
||||||
|
assertEquals(snapshotMap.values.toSet(), result.snapshots.toSet())
|
||||||
|
assertEquals(100, result.percent)
|
||||||
|
assertEquals(expectedSize, result.size)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
nm.onCheckComplete(any(), any())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `check prefers app data over APKs`() = runBlocking {
|
||||||
|
val appDataBlob = blob {
|
||||||
|
id = ByteString.copyFrom(Random.nextBytes(32))
|
||||||
|
length = Random.nextInt(0, Int.MAX_VALUE)
|
||||||
|
uncompressedLength = Random.nextInt(0, Int.MAX_VALUE)
|
||||||
|
}
|
||||||
|
val appDataBlobHandle1 = AppBackupFileType.Blob(repoId, appDataBlob.id.hexFromProto())
|
||||||
|
val appDataChunkId = Random.nextBytes(32).toHexString()
|
||||||
|
|
||||||
|
val snapshotMap = mapOf(
|
||||||
|
snapshotHandle1 to snapshot.copy {
|
||||||
|
token = 1
|
||||||
|
apps[packageName] = app.copy { chunkIds.add(ByteString.fromHex(appDataChunkId)) }
|
||||||
|
blobs[appDataChunkId] = appDataBlob
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expectLoadingSnapshots(snapshotMap)
|
||||||
|
|
||||||
|
// only loading app data, not other blobs
|
||||||
|
coEvery { loader.loadFile(appDataBlobHandle1, null) } throws SecurityException()
|
||||||
|
|
||||||
|
every { nm.onCheckFinishedWithError(appDataBlob.length.toLong(), any()) } just Runs
|
||||||
|
|
||||||
|
assertNull(checker.checkerResult)
|
||||||
|
checker.check(1) // 1% to minimize chance of selecting a non-app random blob
|
||||||
|
assertInstanceOf(CheckerResult.Error::class.java, checker.checkerResult)
|
||||||
|
val result = checker.checkerResult as CheckerResult.Error
|
||||||
|
assertEquals(snapshotMap.values.toSet(), result.snapshots.toSet())
|
||||||
|
assertEquals(snapshotMap.values.toSet(), result.badSnapshots.toSet())
|
||||||
|
assertEquals(snapshotMap.size, result.existingSnapshots)
|
||||||
|
assertEquals(setOf(appDataChunkId), result.errorChunkIds)
|
||||||
|
|
||||||
|
coVerify(exactly = 0) {
|
||||||
|
loader.loadFile(blobHandle1, null)
|
||||||
|
loader.loadFile(blobHandle2, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun expectLoadingSnapshots(
|
||||||
|
snapshots: Map<AppBackupFileType.Snapshot, Snapshot>,
|
||||||
|
) {
|
||||||
|
every { crypto.repoId } returns repoId
|
||||||
|
every { backendManager.backend } returns backend
|
||||||
|
coEvery {
|
||||||
|
backend.list(folder, AppBackupFileType.Snapshot::class, callback = captureLambda())
|
||||||
|
} answers {
|
||||||
|
snapshots.keys.forEach {
|
||||||
|
val fileInfo = FileInfo(it, Random.nextLong(Long.MAX_VALUE))
|
||||||
|
lambda<(FileInfo) -> Unit>().captured.invoke(fileInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coEvery {
|
||||||
|
snapshotManager.onSnapshotsLoaded(snapshots.keys.toList())
|
||||||
|
} returns snapshots.values.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue