Cache folder contents in K/V backup/restore

This speeds up things significantly and was needed due to poor
performance of call log backup.
This commit is contained in:
Torsten Grote 2020-09-14 13:49:59 -03:00 committed by Chirayu Desai
parent 02438c91d3
commit 15969e0d88
9 changed files with 110 additions and 72 deletions

View file

@ -202,7 +202,6 @@ class PluginTest : KoinComponent {
val record3 = Pair(getRandomBase64(128), getRandomByteArray(5 * 1024 * 1024)) val record3 = Pair(getRandomBase64(128), getRandomByteArray(5 * 1024 * 1024))
// write first record // write first record
kvBackup.ensureRecordStorageForPackage(packageInfo)
kvBackup.getOutputStreamForRecord(packageInfo, record1.first).writeAndClose(record1.second) kvBackup.getOutputStreamForRecord(packageInfo, record1.first).writeAndClose(record1.second)
// data is now available for current token and given package, but not for different token // data is now available for current token and given package, but not for different token
@ -220,11 +219,11 @@ class PluginTest : KoinComponent {
) )
// write second and third record // write second and third record
kvBackup.ensureRecordStorageForPackage(packageInfo)
kvBackup.getOutputStreamForRecord(packageInfo, record2.first).writeAndClose(record2.second) kvBackup.getOutputStreamForRecord(packageInfo, record2.first).writeAndClose(record2.second)
kvBackup.getOutputStreamForRecord(packageInfo, record3.first).writeAndClose(record3.second) kvBackup.getOutputStreamForRecord(packageInfo, record3.first).writeAndClose(record3.second)
// all records for package are found and returned properly // all records for package are found and returned properly
assertTrue(kvRestore.hasDataForPackage(token, packageInfo))
records = kvRestore.listRecords(token, packageInfo) records = kvRestore.listRecords(token, packageInfo)
assertEquals(listOf(record1.first, record2.first, record3.first).sorted(), records.sorted()) assertEquals(listOf(record1.first, record2.first, record3.first).sorted(), records.sorted())
assertReadEquals( assertReadEquals(
@ -242,6 +241,7 @@ class PluginTest : KoinComponent {
// delete record3 and ensure that the other two are still found // delete record3 and ensure that the other two are still found
kvBackup.deleteRecord(packageInfo, record3.first) kvBackup.deleteRecord(packageInfo, record3.first)
assertTrue(kvRestore.hasDataForPackage(token, packageInfo))
records = kvRestore.listRecords(token, packageInfo) records = kvRestore.listRecords(token, packageInfo)
assertEquals(listOf(record1.first, record2.first).sorted(), records.sorted()) assertEquals(listOf(record1.first, record2.first).sorted(), records.sorted())
@ -259,6 +259,7 @@ class PluginTest : KoinComponent {
// initialize storage with given token // initialize storage with given token
initStorage(token) initStorage(token)
assertFalse(kvBackup.hasDataForPackage(packageInfo))
// FIXME get Nextcloud to have the same limit // FIXME get Nextcloud to have the same limit
// Since Nextcloud is using WebDAV and that seems to have undefined lower file name limits // Since Nextcloud is using WebDAV and that seems to have undefined lower file name limits
@ -271,7 +272,6 @@ class PluginTest : KoinComponent {
val recordOver = Pair(getRandomBase64(maxOver), getRandomByteArray(1024)) val recordOver = Pair(getRandomBase64(maxOver), getRandomByteArray(1024))
// write max record // write max record
kvBackup.ensureRecordStorageForPackage(packageInfo)
kvBackup.getOutputStreamForRecord(packageInfo, recordMax.first) kvBackup.getOutputStreamForRecord(packageInfo, recordMax.first)
.writeAndClose(recordMax.second) .writeAndClose(recordMax.second)
@ -281,7 +281,6 @@ class PluginTest : KoinComponent {
assertEquals(listOf(recordMax.first), records) assertEquals(listOf(recordMax.first), records)
// write exceeding key length record // write exceeding key length record
kvBackup.ensureRecordStorageForPackage(packageInfo)
if (isNextcloud()) { if (isNextcloud()) {
// Nextcloud simply refuses to write long filenames // Nextcloud simply refuses to write long filenames
coAssertThrows(IOException::class.java) { coAssertThrows(IOException::class.java) {

View file

@ -19,39 +19,21 @@ internal class DocumentsProviderKVBackup(
) : KVBackupPlugin { ) : KVBackupPlugin {
private var packageFile: DocumentFile? = null private var packageFile: DocumentFile? = null
private var packageChildren: List<DocumentFile>? = null
override fun getQuota(): Long = DEFAULT_QUOTA_KEY_VALUE_BACKUP override fun getQuota(): Long = DEFAULT_QUOTA_KEY_VALUE_BACKUP
@Throws(IOException::class) @Throws(IOException::class)
override suspend fun hasDataForPackage(packageInfo: PackageInfo): Boolean { override suspend fun hasDataForPackage(packageInfo: PackageInfo): Boolean {
val packageFile = // get the folder for the package (or create it) and all files in it
storage.currentKvBackupDir?.findFileBlocking(context, packageInfo.packageName) val dir =
?: return false
return packageFile.listFilesBlocking(context).isNotEmpty()
}
@Throws(IOException::class)
override suspend fun ensureRecordStorageForPackage(packageInfo: PackageInfo) {
// remember package file for subsequent operations
packageFile =
storage.getOrCreateKVBackupDir().createOrGetDirectory(context, packageInfo.packageName) storage.getOrCreateKVBackupDir().createOrGetDirectory(context, packageInfo.packageName)
} val children = dir.listFilesBlocking(context)
// cache package file for subsequent operations
@Throws(IOException::class) packageFile = dir
override suspend fun removeDataOfPackage(packageInfo: PackageInfo) { // also cache children as doing this for every record is super slow
// we cannot use the cached this.packageFile here, packageChildren = children
// because this can be called before [ensureRecordStorageForPackage] return children.isNotEmpty()
val packageFile =
storage.currentKvBackupDir?.findFileBlocking(context, packageInfo.packageName) ?: return
packageFile.delete()
}
@Throws(IOException::class)
override suspend fun deleteRecord(packageInfo: PackageInfo, key: String) {
val packageFile = this.packageFile ?: throw AssertionError()
packageFile.assertRightFile(packageInfo)
val keyFile = packageFile.findFileBlocking(context, key) ?: return
keyFile.delete()
} }
@Throws(IOException::class) @Throws(IOException::class)
@ -59,6 +41,7 @@ internal class DocumentsProviderKVBackup(
packageInfo: PackageInfo, packageInfo: PackageInfo,
key: String key: String
): OutputStream { ): OutputStream {
// check maximum key lengths
check(key.length <= MAX_KEY_LENGTH) { check(key.length <= MAX_KEY_LENGTH) {
"Key $key for ${packageInfo.packageName} is too long: ${key.length} chars." "Key $key for ${packageInfo.packageName} is too long: ${key.length} chars."
} }
@ -68,10 +51,55 @@ internal class DocumentsProviderKVBackup(
"Key $key for ${packageInfo.packageName} is too long: ${key.length} chars." "Key $key for ${packageInfo.packageName} is too long: ${key.length} chars."
) )
} }
val packageFile = this.packageFile ?: throw AssertionError() // get dir and children from cache
val packageFile = this.packageFile
?: throw AssertionError("No cached packageFile for ${packageInfo.packageName}")
packageFile.assertRightFile(packageInfo) packageFile.assertRightFile(packageInfo)
val keyFile = packageFile.createOrGetFile(context, key) val children = packageChildren
?: throw AssertionError("No cached children for ${packageInfo.packageName}")
// get file for key from cache,
val keyFile = children.find { it.name == key } // try cache first
?: packageFile.createFile(MIME_TYPE, key) // assume it doesn't exist, create it
?: packageFile.createOrGetFile(context, key) // cache was stale, so try to find it
check(keyFile.name == key) { "Key file named ${keyFile.name}, but should be $key" }
return storage.getOutputStream(keyFile) return storage.getOutputStream(keyFile)
} }
@Throws(IOException::class)
override suspend fun deleteRecord(packageInfo: PackageInfo, key: String) {
val packageFile = this.packageFile
?: throw AssertionError("No cached packageFile for ${packageInfo.packageName}")
packageFile.assertRightFile(packageInfo)
val children = packageChildren
?: throw AssertionError("No cached children for ${packageInfo.packageName}")
// try to find file for given key and delete it if found
val keyFile = children.find { it.name == key } // try to find in cache
?: packageFile.findFileBlocking(context, key) // fall-back to provider
?: return // not found, nothing left to do
keyFile.delete()
// we don't update the children cache as deleted records
// are not expected to get re-added in the same backup pass
}
@Throws(IOException::class)
override suspend fun removeDataOfPackage(packageInfo: PackageInfo) {
val packageFile = this.packageFile
?: throw AssertionError("No cached packageFile for ${packageInfo.packageName}")
packageFile.assertRightFile(packageInfo)
// We are not using the cached children here in case they are stale.
// This operation isn't frequent, so we don't need to heavily optimize it.
packageFile.deleteContents(context)
// clear children cache
packageChildren = ArrayList()
}
override fun packageFinished(packageInfo: PackageInfo) {
packageFile = null
packageChildren = null
}
} }

View file

@ -14,13 +14,19 @@ internal class DocumentsProviderKVRestorePlugin(
) : KVRestorePlugin { ) : KVRestorePlugin {
private var packageDir: DocumentFile? = null private var packageDir: DocumentFile? = null
private var packageChildren: List<DocumentFile>? = null
override suspend fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean { override suspend fun hasDataForPackage(token: Long, packageInfo: PackageInfo): Boolean {
return try { return try {
val backupDir = storage.getKVBackupDir(token) ?: return false val backupDir = storage.getKVBackupDir(token) ?: return false
val dir = backupDir.findFileBlocking(context, packageInfo.packageName) ?: return false
val children = dir.listFilesBlocking(context)
// remember package file for subsequent operations // remember package file for subsequent operations
packageDir = backupDir.findFileBlocking(context, packageInfo.packageName) packageDir = dir
packageDir != null // remember package children for subsequent operations
packageChildren = children
// we have data if we have a non-empty list of children
children.isNotEmpty()
} catch (e: IOException) { } catch (e: IOException) {
false false
} }
@ -28,11 +34,13 @@ internal class DocumentsProviderKVRestorePlugin(
@Throws(IOException::class) @Throws(IOException::class)
override suspend fun listRecords(token: Long, packageInfo: PackageInfo): List<String> { override suspend fun listRecords(token: Long, packageInfo: PackageInfo): List<String> {
val packageDir = this.packageDir ?: throw AssertionError() val packageDir = this.packageDir
?: throw AssertionError("No cached packageDir for ${packageInfo.packageName}")
packageDir.assertRightFile(packageInfo) packageDir.assertRightFile(packageInfo)
return packageDir.listFilesBlocking(context) return packageChildren
.filter { file -> file.name != null } ?.filter { file -> file.name != null }
.map { file -> file.name!! } ?.map { file -> file.name!! }
?: throw AssertionError("No cached children for ${packageInfo.packageName}")
} }
@Throws(IOException::class) @Throws(IOException::class)
@ -41,9 +49,12 @@ internal class DocumentsProviderKVRestorePlugin(
packageInfo: PackageInfo, packageInfo: PackageInfo,
key: String key: String
): InputStream { ): InputStream {
val packageDir = this.packageDir ?: throw AssertionError() val packageDir = this.packageDir
?: throw AssertionError("No cached packageDir for ${packageInfo.packageName}")
packageDir.assertRightFile(packageInfo) packageDir.assertRightFile(packageInfo)
val keyFile = packageDir.findFileBlocking(context, key) ?: throw IOException() val keyFile = packageChildren?.find { it.name == key }
?: packageDir.findFileBlocking(context, key)
?: throw IOException()
return storage.getInputStream(keyFile) return storage.getInputStream(keyFile)
} }

View file

@ -32,7 +32,7 @@ const val DIRECTORY_FULL_BACKUP = "full"
const val DIRECTORY_KEY_VALUE_BACKUP = "kv" const val DIRECTORY_KEY_VALUE_BACKUP = "kv"
const val FILE_BACKUP_METADATA = ".backup.metadata" const val FILE_BACKUP_METADATA = ".backup.metadata"
const val FILE_NO_MEDIA = ".nomedia" const val FILE_NO_MEDIA = ".nomedia"
private const val MIME_TYPE = "application/octet-stream" const val MIME_TYPE = "application/octet-stream"
private val TAG = DocumentsStorage::class.java.simpleName private val TAG = DocumentsStorage::class.java.simpleName

View file

@ -91,14 +91,6 @@ internal class KVBackup(
} }
} }
// ensure there's a place to store K/V for the given package
try {
plugin.ensureRecordStorageForPackage(packageInfo)
} catch (e: IOException) {
Log.e(TAG, "Error ensuring storage for ${packageInfo.packageName}.", e)
return backupError(TRANSPORT_ERROR)
}
// parse and store the K/V updates // parse and store the K/V updates
return storeRecords(packageInfo, data) return storeRecords(packageInfo, data)
} }
@ -221,6 +213,7 @@ internal class KVBackup(
fun finishBackup(): Int { fun finishBackup(): Int {
Log.i(TAG, "Finish K/V Backup of ${state!!.packageInfo.packageName}") Log.i(TAG, "Finish K/V Backup of ${state!!.packageInfo.packageName}")
plugin.packageFinished(state!!.packageInfo)
state = null state = null
return TRANSPORT_OK return TRANSPORT_OK
} }
@ -233,6 +226,7 @@ internal class KVBackup(
"Resetting state because of K/V Backup error of ${state!!.packageInfo.packageName}".let { "Resetting state because of K/V Backup error of ${state!!.packageInfo.packageName}".let {
Log.i(TAG, it) Log.i(TAG, it)
} }
plugin.packageFinished(state!!.packageInfo)
state = null state = null
return result return result
} }

View file

@ -14,18 +14,13 @@ interface KVBackupPlugin {
// TODO consider using a salted hash for the package name (and key) to not leak it to the storage server // TODO consider using a salted hash for the package name (and key) to not leak it to the storage server
/** /**
* Return true if there are records stored for the given package. * Return true if there are records stored for the given package.
*/ * This is always called first per [PackageInfo], before subsequent methods.
@Throws(IOException::class)
suspend fun hasDataForPackage(packageInfo: PackageInfo): Boolean
/**
* This marks the beginning of a backup operation.
* *
* Make sure that there is a place to store K/V pairs for the given package. * Independent of the return value, the storage should now be prepared to store K/V pairs.
* E.g. file-based plugins should a create a directory for the package, if none exists. * E.g. file-based plugins should a create a directory for the package, if none exists.
*/ */
@Throws(IOException::class) @Throws(IOException::class)
suspend fun ensureRecordStorageForPackage(packageInfo: PackageInfo) suspend fun hasDataForPackage(packageInfo: PackageInfo): Boolean
/** /**
* Return an [OutputStream] for the given package and key * Return an [OutputStream] for the given package and key
@ -41,9 +36,16 @@ interface KVBackupPlugin {
suspend fun deleteRecord(packageInfo: PackageInfo, key: String) suspend fun deleteRecord(packageInfo: PackageInfo, key: String)
/** /**
* Remove all data associated with the given package. * Remove all data associated with the given package,
* but be prepared to receive new records afterwards with [getOutputStreamForRecord].
*/ */
@Throws(IOException::class) @Throws(IOException::class)
suspend fun removeDataOfPackage(packageInfo: PackageInfo) suspend fun removeDataOfPackage(packageInfo: PackageInfo)
/**
* The package finished backup.
* This can be an opportunity to clear existing caches or to do other clean-up work.
*/
fun packageFinished(packageInfo: PackageInfo)
} }

View file

@ -15,7 +15,8 @@ interface KVRestorePlugin {
/** /**
* Return all record keys for the given token and package. * Return all record keys for the given token and package.
* *
* Note: Implementations might expect that you call [hasDataForPackage] before. * Note: Implementations usually expect that you call [hasDataForPackage]
* with the same parameters before.
* *
* For file-based plugins, this is usually a list of file names in the package directory. * For file-based plugins, this is usually a list of file names in the package directory.
*/ */

View file

@ -126,7 +126,6 @@ internal class CoordinatorIntegrationTest : TransportTest() {
// read one key/value record and write it to output stream // read one key/value record and write it to output stream
coEvery { kvBackupPlugin.hasDataForPackage(packageInfo) } returns false coEvery { kvBackupPlugin.hasDataForPackage(packageInfo) } returns false
coEvery { kvBackupPlugin.ensureRecordStorageForPackage(packageInfo) } just Runs
every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput
every { backupDataInput.readNextHeader() } returns true andThen true andThen false every { backupDataInput.readNextHeader() } returns true andThen true andThen false
every { backupDataInput.key } returns key andThen key2 every { backupDataInput.key } returns key andThen key2
@ -151,6 +150,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
key264 key264
) )
} returns bOutputStream2 } returns bOutputStream2
every { kvBackupPlugin.packageFinished(packageInfo) } just Runs
coEvery { coEvery {
apkBackup.backupApkIfNecessary( apkBackup.backupApkIfNecessary(
packageInfo, packageInfo,
@ -219,7 +219,6 @@ internal class CoordinatorIntegrationTest : TransportTest() {
// read one key/value record and write it to output stream // read one key/value record and write it to output stream
coEvery { kvBackupPlugin.hasDataForPackage(packageInfo) } returns false coEvery { kvBackupPlugin.hasDataForPackage(packageInfo) } returns false
coEvery { kvBackupPlugin.ensureRecordStorageForPackage(packageInfo) } just Runs
every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput every { inputFactory.getBackupDataInput(fileDescriptor) } returns backupDataInput
every { backupDataInput.readNextHeader() } returns true andThen false every { backupDataInput.readNextHeader() } returns true andThen false
every { backupDataInput.key } returns key every { backupDataInput.key } returns key
@ -234,6 +233,7 @@ internal class CoordinatorIntegrationTest : TransportTest() {
key64 key64
) )
} returns bOutputStream } returns bOutputStream
every { kvBackupPlugin.packageFinished(packageInfo) } just Runs
coEvery { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns null coEvery { apkBackup.backupApkIfNecessary(packageInfo, UNKNOWN_ERROR, any()) } returns null
coEvery { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream coEvery { backupPlugin.getMetadataOutputStream() } returns metadataOutputStream
every { metadataManager.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs every { metadataManager.onPackageBackedUp(packageInfo, metadataOutputStream) } just Runs

View file

@ -90,6 +90,9 @@ internal class KVBackupTest : BackupTest() {
assertEquals(TRANSPORT_OK, backup.performBackup(pmPackageInfo, data, 0)) assertEquals(TRANSPORT_OK, backup.performBackup(pmPackageInfo, data, 0))
assertTrue(backup.hasState()) assertTrue(backup.hasState())
every { plugin.packageFinished(pmPackageInfo) } just Runs
assertEquals(TRANSPORT_OK, backup.finishBackup()) assertEquals(TRANSPORT_OK, backup.finishBackup())
assertFalse(backup.hasState()) assertFalse(backup.hasState())
@ -103,6 +106,7 @@ internal class KVBackupTest : BackupTest() {
@Test @Test
fun `incremental backup with no data gets rejected`() = runBlocking { fun `incremental backup with no data gets rejected`() = runBlocking {
coEvery { plugin.hasDataForPackage(packageInfo) } returns false coEvery { plugin.hasDataForPackage(packageInfo) } returns false
every { plugin.packageFinished(packageInfo) } just Runs
assertEquals( assertEquals(
TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED, TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED,
@ -114,6 +118,7 @@ internal class KVBackupTest : BackupTest() {
@Test @Test
fun `check for existing data throws exception`() = runBlocking { fun `check for existing data throws exception`() = runBlocking {
coEvery { plugin.hasDataForPackage(packageInfo) } throws IOException() coEvery { plugin.hasDataForPackage(packageInfo) } throws IOException()
every { plugin.packageFinished(packageInfo) } just Runs
assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0)) assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0))
assertFalse(backup.hasState()) assertFalse(backup.hasState())
@ -145,20 +150,12 @@ internal class KVBackupTest : BackupTest() {
assertFalse(backup.hasState()) assertFalse(backup.hasState())
} }
@Test
fun `ensuring storage throws exception`() = runBlocking {
coEvery { plugin.hasDataForPackage(packageInfo) } returns false
coEvery { plugin.ensureRecordStorageForPackage(packageInfo) } throws IOException()
assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0))
assertFalse(backup.hasState())
}
@Test @Test
fun `exception while reading next header`() = runBlocking { fun `exception while reading next header`() = runBlocking {
initPlugin(false) initPlugin(false)
createBackupDataInput() createBackupDataInput()
every { dataInput.readNextHeader() } throws IOException() every { dataInput.readNextHeader() } throws IOException()
every { plugin.packageFinished(packageInfo) } just Runs
assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0)) assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0))
assertFalse(backup.hasState()) assertFalse(backup.hasState())
@ -172,6 +169,7 @@ internal class KVBackupTest : BackupTest() {
every { dataInput.key } returns key every { dataInput.key } returns key
every { dataInput.dataSize } returns value.size every { dataInput.dataSize } returns value.size
every { dataInput.readEntityData(any(), 0, value.size) } throws IOException() every { dataInput.readEntityData(any(), 0, value.size) } throws IOException()
every { plugin.packageFinished(packageInfo) } just Runs
assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0)) assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0))
assertFalse(backup.hasState()) assertFalse(backup.hasState())
@ -181,6 +179,7 @@ internal class KVBackupTest : BackupTest() {
fun `no data records`() = runBlocking { fun `no data records`() = runBlocking {
initPlugin(false) initPlugin(false)
getDataInput(listOf(false)) getDataInput(listOf(false))
every { plugin.packageFinished(packageInfo) } just Runs
assertEquals(TRANSPORT_OK, backup.performBackup(packageInfo, data, 0)) assertEquals(TRANSPORT_OK, backup.performBackup(packageInfo, data, 0))
assertTrue(backup.hasState()) assertTrue(backup.hasState())
@ -195,6 +194,7 @@ internal class KVBackupTest : BackupTest() {
coEvery { plugin.getOutputStreamForRecord(packageInfo, key64) } returns outputStream coEvery { plugin.getOutputStreamForRecord(packageInfo, key64) } returns outputStream
every { headerWriter.writeVersion(outputStream, versionHeader) } throws IOException() every { headerWriter.writeVersion(outputStream, versionHeader) } throws IOException()
every { outputStream.close() } just Runs every { outputStream.close() } just Runs
every { plugin.packageFinished(packageInfo) } just Runs
assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0)) assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0))
assertFalse(backup.hasState()) assertFalse(backup.hasState())
@ -211,6 +211,7 @@ internal class KVBackupTest : BackupTest() {
every { headerWriter.writeVersion(outputStream, versionHeader) } just Runs every { headerWriter.writeVersion(outputStream, versionHeader) } just Runs
every { crypto.encryptMultipleSegments(outputStream, any()) } throws IOException() every { crypto.encryptMultipleSegments(outputStream, any()) } throws IOException()
every { outputStream.close() } just Runs every { outputStream.close() } just Runs
every { plugin.packageFinished(packageInfo) } just Runs
assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0)) assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0))
assertFalse(backup.hasState()) assertFalse(backup.hasState())
@ -226,6 +227,7 @@ internal class KVBackupTest : BackupTest() {
every { outputStream.write(value) } just Runs every { outputStream.write(value) } just Runs
every { outputStream.flush() } throws IOException() every { outputStream.flush() } throws IOException()
every { outputStream.close() } just Runs every { outputStream.close() } just Runs
every { plugin.packageFinished(packageInfo) } just Runs
assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0)) assertEquals(TRANSPORT_ERROR, backup.performBackup(packageInfo, data, 0))
assertFalse(backup.hasState()) assertFalse(backup.hasState())
@ -241,6 +243,7 @@ internal class KVBackupTest : BackupTest() {
every { outputStream.write(value) } just Runs every { outputStream.write(value) } just Runs
every { outputStream.flush() } just Runs every { outputStream.flush() } just Runs
every { outputStream.close() } throws IOException() every { outputStream.close() } throws IOException()
every { plugin.packageFinished(packageInfo) } just Runs
assertEquals(TRANSPORT_OK, backup.performBackup(packageInfo, data, 0)) assertEquals(TRANSPORT_OK, backup.performBackup(packageInfo, data, 0))
assertTrue(backup.hasState()) assertTrue(backup.hasState())
@ -255,11 +258,11 @@ internal class KVBackupTest : BackupTest() {
every { outputStream.write(value) } just Runs every { outputStream.write(value) } just Runs
every { outputStream.flush() } just Runs every { outputStream.flush() } just Runs
every { outputStream.close() } just Runs every { outputStream.close() } just Runs
every { plugin.packageFinished(packageInfo) } just Runs
} }
private fun initPlugin(hasDataForPackage: Boolean = false, pi: PackageInfo = packageInfo) { private fun initPlugin(hasDataForPackage: Boolean = false, pi: PackageInfo = packageInfo) {
coEvery { plugin.hasDataForPackage(pi) } returns hasDataForPackage coEvery { plugin.hasDataForPackage(pi) } returns hasDataForPackage
coEvery { plugin.ensureRecordStorageForPackage(pi) } just Runs
} }
private fun createBackupDataInput() { private fun createBackupDataInput() {