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
|
||||
suspend fun getBackupSize(): Long? {
|
||||
// get all snapshots
|
||||
val folder = TopLevelFolder(crypto.repoId)
|
||||
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
|
||||
return try {
|
||||
getBackupSizeInt()
|
||||
} catch (e: Exception) {
|
||||
log.error(e) { "Error loading snapshots: " }
|
||||
// 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
|
||||
val sizeMap = mutableMapOf<String, Int>()
|
||||
snapshots?.forEach { snapshot ->
|
||||
snapshots.forEach { snapshot ->
|
||||
// add sizes to a map first, so we don't double count
|
||||
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." }
|
||||
|
||||
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) {
|
||||
nm.onCheckFinishedWithError(0, 0)
|
||||
checkerResult = CheckerResult.GeneralError(e)
|
||||
return
|
||||
}
|
||||
val snapshots = snapshots ?: error("Snapshots 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