From 2c20fbd15d42cab3a2d8a51190d2e51e7c0bea0d Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Thu, 16 May 2024 23:50:28 +0300 Subject: [PATCH 01/14] RUM-4098: Add TLVFormat DataStore persistence --- dd-sdk-android-core/api/apiSurface | 3 + .../api/dd-sdk-android-core.api | 3 + .../android/api/feature/FeatureScope.kt | 37 ++ .../android/core/internal/CoreFeature.kt | 33 ++ .../android/core/internal/SdkFeature.kt | 33 ++ .../datastore/DataStoreContents.kt | 13 + .../datastore/DataStoreFileHelper.kt | 26 + .../persistence/datastore/DataStoreHandler.kt | 28 ++ .../datastore/FileDataStoreHandler.kt | 289 +++++++++++ .../datastore/NoOpDataStoreHandler.kt | 31 ++ .../persistence/datastore/ext/ByteArrayExt.kt | 27 + .../persistence/datastore/ext/IntExt.kt | 17 + .../persistence/datastore/ext/LongExt.kt | 17 + .../core/internal/persistence/file/FileExt.kt | 7 + .../tlvformat/FileTLVBlockReader.kt | 126 +++++ .../persistence/tlvformat/TLVBlock.kt | 39 ++ .../persistence/tlvformat/TLVBlockType.kt | 21 + .../android/core/internal/CoreFeatureTest.kt | 36 +- .../android/core/internal/SdkFeatureTest.kt | 70 +++ .../datastore/FileDataStoreHandlerTest.kt | 464 ++++++++++++++++++ .../tlvformat/FileTLVBlockReaderTest.kt | 145 ++++++ .../persistence/tlvformat/TLVBlockTest.kt | 81 +++ .../persistence/tlvformat/TLVBlockTypeTest.kt | 44 ++ detekt_custom.yml | 7 + 24 files changed, 1596 insertions(+), 1 deletion(-) create mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreContents.kt create mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt create mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreHandler.kt create mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/FileDataStoreHandler.kt create mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt create mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/ext/ByteArrayExt.kt create mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/ext/IntExt.kt create mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/ext/LongExt.kt create mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/FileTLVBlockReader.kt create mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt create mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockType.kt create mode 100644 dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/FileDataStoreHandlerTest.kt create mode 100644 dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/FileTLVBlockReaderTest.kt create mode 100644 dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt create mode 100644 dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTypeTest.kt diff --git a/dd-sdk-android-core/api/apiSurface b/dd-sdk-android-core/api/apiSurface index ce50f06645..1e9920c61e 100644 --- a/dd-sdk-android-core/api/apiSurface +++ b/dd-sdk-android-core/api/apiSurface @@ -103,6 +103,9 @@ interface com.datadog.android.api.feature.FeatureEventReceiver fun onReceive(Any) interface com.datadog.android.api.feature.FeatureScope fun withWriteContext(Boolean = false, (com.datadog.android.api.context.DatadogContext, com.datadog.android.api.storage.EventBatchWriter) -> Unit) + fun writeToDataStore(String, String, com.datadog.android.core.persistence.Serializer, T) + fun readFromDataStore(String, String, com.datadog.android.core.internal.persistence.Deserializer, Int): T? + fun getDataStoreCurrentVersion(): Int fun sendEvent(Any) fun unwrap(): T fun com.datadog.android.api.InternalLogger.measureMethodCallPerf(Class<*>, String, Float = 100f, () -> R): R diff --git a/dd-sdk-android-core/api/dd-sdk-android-core.api b/dd-sdk-android-core/api/dd-sdk-android-core.api index b80ced97fa..3d198e74df 100644 --- a/dd-sdk-android-core/api/dd-sdk-android-core.api +++ b/dd-sdk-android-core/api/dd-sdk-android-core.api @@ -325,9 +325,12 @@ public abstract interface class com/datadog/android/api/feature/FeatureEventRece } public abstract interface class com/datadog/android/api/feature/FeatureScope { + public abstract fun getDataStoreCurrentVersion ()I + public abstract fun readFromDataStore (Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Deserializer;I)Ljava/lang/Object; public abstract fun sendEvent (Ljava/lang/Object;)V public abstract fun unwrap ()Lcom/datadog/android/api/feature/Feature; public abstract fun withWriteContext (ZLkotlin/jvm/functions/Function2;)V + public abstract fun writeToDataStore (Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/core/persistence/Serializer;Ljava/lang/Object;)V } public final class com/datadog/android/api/feature/FeatureScope$DefaultImpls { diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt index 9557c3d660..2eac046ffe 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt @@ -9,6 +9,8 @@ package com.datadog.android.api.feature import androidx.annotation.AnyThread import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.storage.EventBatchWriter +import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.persistence.Serializer /** * Represents a Datadog feature. @@ -30,6 +32,41 @@ interface FeatureScope { callback: (DatadogContext, EventBatchWriter) -> Unit ) + /** + * Write data to the datastore. + * + * @param dataStoreFileName name of the datastore file as there could be multiple such files per feature. + * @param featureName of the calling feature, to determine the path to the datastore file. + * @param serializer to use to serialize the data. + * @param data to write. + */ + fun writeToDataStore( + dataStoreFileName: String, + featureName: String, + serializer: Serializer, + data: T + ) + + /** + * Read data from the datastore. + * + * @param dataStoreFileName name of the datastore file as there could be multiple such files per feature. + * @param featureName of the calling feature, to determine the path to the datastore file. + * @param deserializer to use to deserialize the data. + * @param version to use when reading from the datastore (to support migrations). + */ + fun readFromDataStore( + dataStoreFileName: String, + featureName: String, + deserializer: Deserializer, + version: Int + ): T? + + /** + * Return the current version of the datastore. + */ + fun getDataStoreCurrentVersion(): Int + /** * Send event to a given feature. It will be sent in a synchronous way. * diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt index 7eccf027a2..10d9198cba 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt @@ -35,6 +35,10 @@ import com.datadog.android.core.internal.net.info.NetworkInfoDeserializer import com.datadog.android.core.internal.net.info.NetworkInfoProvider import com.datadog.android.core.internal.net.info.NoOpNetworkInfoProvider import com.datadog.android.core.internal.persistence.JsonObjectDeserializer +import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper +import com.datadog.android.core.internal.persistence.datastore.DataStoreHandler +import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler +import com.datadog.android.core.internal.persistence.datastore.NoOpDataStoreHandler import com.datadog.android.core.internal.persistence.file.FileMover import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig import com.datadog.android.core.internal.persistence.file.FileReaderWriter @@ -45,6 +49,7 @@ import com.datadog.android.core.internal.persistence.file.deleteSafe import com.datadog.android.core.internal.persistence.file.existsSafe import com.datadog.android.core.internal.persistence.file.readTextSafe import com.datadog.android.core.internal.persistence.file.writeTextSafe +import com.datadog.android.core.internal.persistence.tlvformat.FileTLVBlockReader import com.datadog.android.core.internal.privacy.ConsentProvider import com.datadog.android.core.internal.privacy.NoOpConsentProvider import com.datadog.android.core.internal.privacy.TrackingConsentProvider @@ -137,6 +142,7 @@ internal class CoreFeature( internal var uploadFrequency: UploadFrequency = UploadFrequency.AVERAGE internal var batchProcessingLevel: BatchProcessingLevel = BatchProcessingLevel.MEDIUM internal var ndkCrashHandler: NdkCrashHandler = NoOpNdkCrashHandler() + internal var dataStoreHandler: DataStoreHandler = NoOpDataStoreHandler() internal var site: DatadogSite = DatadogSite.US1 internal var appBuildId: String? = null @@ -225,6 +231,10 @@ internal class CoreFeature( val nativeSourceOverride = configuration.additionalConfig[Datadog.DD_NATIVE_SOURCE_TYPE] as? String prepareNdkCrashData(nativeSourceOverride) setupInfoProviders(appContext, consent) + prepareDataStoreHandler( + sdkInstanceId = sdkInstanceId, + configuration = configuration.coreConfig + ) initialized.set(true) contextProvider = DatadogContextProvider(this) } @@ -260,6 +270,7 @@ internal class CoreFeature( initialized.set(false) ndkCrashHandler = NoOpNdkCrashHandler() + dataStoreHandler = NoOpDataStoreHandler() trackingConsentProvider = NoOpConsentProvider() contextProvider = NoOpContextProvider() } @@ -369,6 +380,28 @@ internal class CoreFeature( } } + private fun prepareDataStoreHandler( + sdkInstanceId: String, + configuration: Configuration.Core + ) { + val fileReaderWriter = FileReaderWriter.create( + internalLogger, + configuration.encryption + ) + + dataStoreHandler = FileDataStoreHandler( + sdkInstanceId = sdkInstanceId, + storageDir = storageDir, + internalLogger = internalLogger, + fileReaderWriter = fileReaderWriter, + fileTLVBlockReader = FileTLVBlockReader( + internalLogger = internalLogger, + fileReaderWriter = fileReaderWriter + ), + dataStoreFileHelper = DataStoreFileHelper() + ) + } + private fun prepareNdkCrashData(nativeSourceType: String?) { if (isMainProcess) { ndkCrashHandler = DatadogNdkCrashHandler( diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt index fc59686add..bbf40c6be5 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt @@ -34,8 +34,10 @@ import com.datadog.android.core.internal.metrics.MetricsDispatcher import com.datadog.android.core.internal.metrics.NoOpMetricsDispatcher import com.datadog.android.core.internal.persistence.AbstractStorage import com.datadog.android.core.internal.persistence.ConsentAwareStorage +import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.internal.persistence.NoOpStorage import com.datadog.android.core.internal.persistence.Storage +import com.datadog.android.core.internal.persistence.datastore.CURRENT_DATASTORE_VERSION import com.datadog.android.core.internal.persistence.file.FileMover import com.datadog.android.core.internal.persistence.file.FileOrchestrator import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig @@ -44,6 +46,7 @@ import com.datadog.android.core.internal.persistence.file.NoOpFileOrchestrator import com.datadog.android.core.internal.persistence.file.advanced.FeatureFileOrchestrator import com.datadog.android.core.internal.persistence.file.batch.BatchFileReaderWriter import com.datadog.android.core.persistence.PersistenceStrategy +import com.datadog.android.core.persistence.Serializer import com.datadog.android.privacy.TrackingConsentProviderCallback import java.util.Collections import java.util.Locale @@ -143,6 +146,36 @@ internal class SdkFeature( // region FeatureScope + override fun writeToDataStore( + dataStoreFileName: String, + featureName: String, + serializer: Serializer, + data: T + ) { + coreFeature.dataStoreHandler.write( + dataStoreFileName = dataStoreFileName, + featureName = featureName, + serializer = serializer, + data = data + ) + } + + override fun readFromDataStore( + dataStoreFileName: String, + featureName: String, + deserializer: Deserializer, + version: Int + ): T? = + coreFeature.dataStoreHandler.read( + dataStoreFileName = dataStoreFileName, + featureName = featureName, + deserializer = deserializer, + version = version + ) + + override fun getDataStoreCurrentVersion(): Int = + CURRENT_DATASTORE_VERSION + override fun withWriteContext( forceNewBatch: Boolean, callback: (DatadogContext, EventBatchWriter) -> Unit diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreContents.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreContents.kt new file mode 100644 index 0000000000..20c042ba6a --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreContents.kt @@ -0,0 +1,13 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore + +internal data class DataStoreContents( + val lastUpdateDate: Long, + val versionCode: Int, + val data: T? +) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt new file mode 100644 index 0000000000..dd11add533 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt @@ -0,0 +1,26 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore + +import java.io.File + +internal class DataStoreFileHelper { + internal fun getDataStoreDirectory( + sdkInstanceId: String, + storageDir: File, + featureName: String, + folderName: String + ): File = File( + storageDir, + "$sdkInstanceId/$featureName/$folderName" + ) + + internal fun getDataStoreFile( + dataStoreDirectory: File, + dataStoreFileName: String + ) = File(dataStoreDirectory, dataStoreFileName) +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreHandler.kt new file mode 100644 index 0000000000..d6f8357a3e --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreHandler.kt @@ -0,0 +1,28 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore + +import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.persistence.Serializer + +internal const val CURRENT_DATASTORE_VERSION: Int = 0 + +internal interface DataStoreHandler { + fun write( + dataStoreFileName: String, + featureName: String, + serializer: Serializer, + data: T + ) + + fun read( + dataStoreFileName: String, + featureName: String, + deserializer: Deserializer, + version: Int + ): T? +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/FileDataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/FileDataStoreHandler.kt new file mode 100644 index 0000000000..8e5f91221b --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/FileDataStoreHandler.kt @@ -0,0 +1,289 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore + +import android.text.format.DateUtils +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.internal.persistence.datastore.ext.toByteArray +import com.datadog.android.core.internal.persistence.datastore.ext.toInt +import com.datadog.android.core.internal.persistence.datastore.ext.toLong +import com.datadog.android.core.internal.persistence.file.FileReaderWriter +import com.datadog.android.core.internal.persistence.file.createNewFileSafe +import com.datadog.android.core.internal.persistence.file.deleteSafe +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.file.mkdirsSafe +import com.datadog.android.core.internal.persistence.tlvformat.FileTLVBlockReader +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockType +import com.datadog.android.core.persistence.Serializer +import java.io.File +import java.util.Locale + +@Suppress("TooManyFunctions") +internal class FileDataStoreHandler( + private val sdkInstanceId: String, + private val storageDir: File, + private val internalLogger: InternalLogger, + private val fileReaderWriter: FileReaderWriter, + private val fileTLVBlockReader: FileTLVBlockReader, + private val dataStoreFileHelper: DataStoreFileHelper +) : DataStoreHandler { + @WorkerThread + override fun write( + dataStoreFileName: String, + featureName: String, + serializer: Serializer, + data: T + ) { + val dataStoreDirectory = createDataStoreDirectoryIfNecessary(featureName) + val dataStoreFile = createDataStoreFileIfNecessary(dataStoreDirectory, dataStoreFileName) + + val lastUpdateBlock = getLastUpdateDateBlock() + val versionCodeBlock = getVersionCodeBlock() + val dataBlock = getDataBlock(data, serializer) + + if (lastUpdateBlock == null || versionCodeBlock == null || dataBlock == null) return + + writeToFile( + dataStoreFile, + lastUpdateBlock + versionCodeBlock + dataBlock + ) + } + + @WorkerThread + override fun read( + dataStoreFileName: String, + featureName: String, + deserializer: Deserializer, + version: Int + ): T? { + val dataStoreDirectory = dataStoreFileHelper.getDataStoreDirectory( + sdkInstanceId = sdkInstanceId, + featureName = featureName, + folderName = DATASTORE_FOLDER_NAME.format(Locale.US, version), + storageDir = storageDir + ) + + val datastoreFile = dataStoreFileHelper.getDataStoreFile( + dataStoreDirectory = dataStoreDirectory, + dataStoreFileName = dataStoreFileName + ) + + if (!datastoreFile.existsSafe(internalLogger)) { + return null // no datastore file found + } + + return readFromDataStoreFile(datastoreFile, deserializer, fileTLVBlockReader, version) + } + + @WorkerThread + private fun writeToFile(dataStoreFile: File, data: ByteArray) { + fileReaderWriter.writeData( + file = dataStoreFile, + data = data, + append = false + ) + } + + @WorkerThread + private fun getDataBlock( + data: T, + serializer: Serializer + ): ByteArray? { + val serializedData = serializer.serialize(data)?.toByteArray() + + if (serializedData == null) { + logFailedToSerializeDataError() + return null + } + + val dataBlock = TLVBlock( + type = TLVBlockType.DATA, + data = serializedData + ) + + return dataBlock.serialize() + } + + @WorkerThread + private fun getLastUpdateDateBlock(): ByteArray? { + val now = System.currentTimeMillis() + val lastUpdateDateByteArray = now.toByteArray() + val lastUpdateDateBlock = TLVBlock( + type = TLVBlockType.LAST_UPDATE_DATE, + data = lastUpdateDateByteArray + ) + + return lastUpdateDateBlock.serialize() + } + + @WorkerThread + private fun getVersionCodeBlock(): ByteArray? { + val versionCodeByteArray = CURRENT_DATASTORE_VERSION.toByteArray() + val versionBlock = TLVBlock( + type = TLVBlockType.VERSION_CODE, + data = versionCodeByteArray + ) + + return versionBlock.serialize() + } + + @WorkerThread + @Suppress("ReturnCount") + private fun readFromDataStoreFile( + datastoreFile: File, + deserializer: Deserializer, + fileTLVBlockReader: FileTLVBlockReader, + requestedVersion: Int + ): T? { + val tlvBlocks = fileTLVBlockReader.all(datastoreFile) + + // there should be as many blocks read as there are block types + if (tlvBlocks.size != TLVBlockType.values().size) { + logInvalidNumberOfBlocksError(tlvBlocks.size) + return null + } + + val dataStoreContents = tryToMapToDataStoreContents(deserializer, tlvBlocks) + ?: return null + + val fileVersionIsWrong = dataStoreContents.versionCode != requestedVersion + val fileIsTooOld = isDataStoreTooOld(dataStoreContents.lastUpdateDate) + + // the version check is a double redundancy. Since we store each file in a folder + // whose name contains the version it should be impossible to read a file + // that contains the wrong version. + if (fileVersionIsWrong) { + logInvalidVersionError() + } + + return if (fileIsTooOld || fileVersionIsWrong) { + datastoreFile.deleteSafe(internalLogger) + null + } else { + dataStoreContents.data + } + } + + private fun tryToMapToDataStoreContents( + deserializer: Deserializer, + tlvBlocks: List + ): DataStoreContents? { + // map the blocks to the actual types + val typesToBlocks = mutableMapOf() + for (block in tlvBlocks) { + val type = block.type + val blockTypeAlreadyExists = typesToBlocks[type] != null + + // verify that the same block doesn't appear more than once + if (blockTypeAlreadyExists) { + logSameBlockAppearsTwiceError(type) + return null + } + + typesToBlocks[type] = block + } + + val lastUpdateBlock = typesToBlocks[TLVBlockType.LAST_UPDATE_DATE] + val versionCodeBlock = typesToBlocks[TLVBlockType.VERSION_CODE] + val dataBlock = typesToBlocks[TLVBlockType.DATA] + return if (lastUpdateBlock == null || versionCodeBlock == null || dataBlock == null) { + null // this should never happen as we know by this stage that these cannot be null + } else { + DataStoreContents( + lastUpdateDate = lastUpdateBlock.data.toLong(), + versionCode = versionCodeBlock.data.toInt(), + data = deserializer.deserialize(String(dataBlock.data)) + ) + } + } + + private fun isDataStoreTooOld(lastUpdateDate: Long): Boolean { + val currentTime = System.currentTimeMillis() + return currentTime - lastUpdateDate > DATASTORE_EXPIRE_TIME + } + + private fun logSameBlockAppearsTwiceError(type: TLVBlockType) { + internalLogger.log( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + messageBuilder = { SAME_BLOCK_APPEARS_TWICE_ERROR.format(Locale.US, type) } + ) + } + + private fun logInvalidVersionError() { + internalLogger.log( + level = InternalLogger.Level.ERROR, + target = InternalLogger.Target.MAINTAINER, + messageBuilder = { INVALID_VERSION_ERROR } + ) + } + + private fun logInvalidNumberOfBlocksError(numberOfBlocks: Int) { + internalLogger.log( + level = InternalLogger.Level.ERROR, + target = InternalLogger.Target.MAINTAINER, + messageBuilder = { INVALID_NUMBER_OF_BLOCKS_ERROR.format(Locale.US, numberOfBlocks) } + ) + } + + private fun logFailedToSerializeDataError() { + internalLogger.log( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + messageBuilder = { FAILED_TO_SERIALIZE_DATA_ERROR } + ) + } + + private fun createDataStoreDirectoryIfNecessary(featureName: String): File { + val dataStoreDirectory = dataStoreFileHelper.getDataStoreDirectory( + sdkInstanceId = sdkInstanceId, + featureName = featureName, + folderName = DATASTORE_FOLDER_NAME.format(Locale.US, CURRENT_DATASTORE_VERSION), + storageDir = storageDir + ) + + if (!dataStoreDirectory.existsSafe(internalLogger)) { + dataStoreDirectory.mkdirsSafe(internalLogger) + } + + return dataStoreDirectory + } + + private fun createDataStoreFileIfNecessary( + dataStoreDirectory: File, + dataStoreFileName: String + ): File { + val datastoreFile = dataStoreFileHelper.getDataStoreFile( + dataStoreDirectory = dataStoreDirectory, + dataStoreFileName = dataStoreFileName + ) + + if (!datastoreFile.existsSafe(internalLogger)) { + datastoreFile.createNewFileSafe(internalLogger) + } + + return datastoreFile + } + + internal companion object { + internal const val DATASTORE_FOLDER_NAME = "datastore_v$%s" + private const val DATASTORE_EXPIRE_TIME = DateUtils.DAY_IN_MILLIS * 30 // 30 days + + internal const val FAILED_TO_SERIALIZE_DATA_ERROR = + "Write error - Failed to serialize data for the datastore" + + internal const val INVALID_VERSION_ERROR = + "Read error - datastore file contains wrong version! This should never happen" + internal const val INVALID_NUMBER_OF_BLOCKS_ERROR = + "Read error - datastore file contains an invalid number of blocks. Was: %s" + internal const val SAME_BLOCK_APPEARS_TWICE_ERROR = + "Read error - same block appears twice in the datastore. Type: %s" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt new file mode 100644 index 0000000000..ab694ad844 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt @@ -0,0 +1,31 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore + +import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.persistence.Serializer + +internal class NoOpDataStoreHandler : DataStoreHandler { + override fun write( + dataStoreFileName: String, + featureName: String, + serializer: Serializer, + data: T + ) { + // NoOp Implementation + } + + override fun read( + dataStoreFileName: String, + featureName: String, + deserializer: Deserializer, + version: Int + ): T? { + // NoOp Implementation + return null + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/ext/ByteArrayExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/ext/ByteArrayExt.kt new file mode 100644 index 0000000000..6b97dda2f4 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/ext/ByteArrayExt.kt @@ -0,0 +1,27 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore.ext + +import java.nio.ByteBuffer + +internal fun ByteArray.toLong(): Long { + // wrap provides valid backing array + @Suppress("UnsafeThirdPartyFunctionCall") + return ByteBuffer.wrap(this).getLong() +} + +internal fun ByteArray.toInt(): Int { + // wrap provides valid backing array + @Suppress("UnsafeThirdPartyFunctionCall") + return ByteBuffer.wrap(this).getInt() +} + +internal fun ByteArray.toShort(): Short { + // wrap provides valid backing array + @Suppress("UnsafeThirdPartyFunctionCall") + return ByteBuffer.wrap(this).getShort() +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/ext/IntExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/ext/IntExt.kt new file mode 100644 index 0000000000..87591488fd --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/ext/IntExt.kt @@ -0,0 +1,17 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore.ext + +import java.nio.ByteBuffer + +internal fun Int.toByteArray(): ByteArray { + // capacity is not a negative integer, buffer is not read only, + // has sufficient capacity and is backed by an array + @Suppress("UnsafeThirdPartyFunctionCall") + return ByteBuffer.allocate(Int.SIZE_BYTES) + .putInt(this).array() +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/ext/LongExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/ext/LongExt.kt new file mode 100644 index 0000000000..b9db105cf6 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/ext/LongExt.kt @@ -0,0 +1,17 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore.ext + +import java.nio.ByteBuffer + +internal fun Long.toByteArray(): ByteArray { + // capacity is not a negative integer, buffer is not read only, + // has sufficient capacity and is backed by an array + @Suppress("UnsafeThirdPartyFunctionCall") + return ByteBuffer.allocate(Long.SIZE_BYTES) + .putLong(this).array() +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileExt.kt index d3abeaa92a..3fcf6ae72a 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileExt.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileExt.kt @@ -79,6 +79,13 @@ internal fun File.deleteSafe(internalLogger: InternalLogger): Boolean { } } +internal fun File.createNewFileSafe(internalLogger: InternalLogger): Boolean { + return safeCall(default = false, internalLogger) { + @Suppress("UnsafeThirdPartyFunctionCall") + createNewFile() + } +} + /** * Non-throwing version of [File.exists]. If exception happens, false is returned. */ diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/FileTLVBlockReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/FileTLVBlockReader.kt new file mode 100644 index 0000000000..932f3c16b0 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/FileTLVBlockReader.kt @@ -0,0 +1,126 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.tlvformat + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.datastore.ext.toInt +import com.datadog.android.core.internal.persistence.datastore.ext.toShort +import com.datadog.android.core.internal.persistence.file.FileReaderWriter +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.file.lengthSafe +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.util.Locale +import java.util.concurrent.atomic.AtomicBoolean + +internal class FileTLVBlockReader( + val internalLogger: InternalLogger, + val fileReaderWriter: FileReaderWriter +) { + private var endOfStream = AtomicBoolean(false) + + @WorkerThread + internal fun all(file: File): List { + if (!file.existsSafe(internalLogger) || file.lengthSafe(internalLogger) == 0L) { + return arrayListOf() + } + + val blocks = mutableListOf() + val stream = fileReaderWriter.readData(file).inputStream() + + while (!endOfStream.get()) { + val nextBlock = readBlock(stream) ?: break + blocks.add(nextBlock) + } + + return blocks + } + + private fun readBlock(stream: InputStream): TLVBlock? { + val type = readType(stream) ?: return null + + val data = readData(stream) + + return TLVBlock(type, data) + } + + private fun readType(stream: InputStream): TLVBlockType? { + val typeBlockSize = UShort.SIZE_BYTES + val bytes = read(stream, typeBlockSize) + + if (bytes.size != typeBlockSize) { + return null + } + + val shortValue = bytes.toShort() + val tlvHeader = TLVBlockType.fromValue(shortValue.toUShort()) + + if (tlvHeader == null) { + logTypeCorruptionError(shortValue) + } + + return tlvHeader + } + + private fun readData(stream: InputStream): ByteArray { + val lengthBlockSize = Int.SIZE_BYTES + val lengthInBytes = read(stream, lengthBlockSize) + val length = lengthInBytes.toInt() + + return read(stream, length) + } + + private fun read(stream: InputStream, length: Int): ByteArray { + val buffer = ByteArray(length) + val status = safeReadFromStream(stream, buffer) + return if (status == -1) { // stream is finished (or error) + endOfStream.set(true) + ByteArray(0) + } else { + buffer + } + } + + private fun logTypeCorruptionError(shortValue: Short) { + internalLogger.log( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + messageBuilder = { + CORRUPT_TLV_HEADER_TYPE_ERROR.format( + Locale.US, + shortValue + ) + } + ) + } + + @Suppress("SwallowedException", "TooGenericExceptionCaught", "UnsafeThirdPartyFunctionCall") + private fun safeReadFromStream(stream: InputStream, buffer: ByteArray): Int { + return try { + stream.read(buffer) + } catch (e: IOException) { + internalLogger.log( + level = InternalLogger.Level.ERROR, + target = InternalLogger.Target.MAINTAINER, + messageBuilder = { FAILED_TO_READ_FROM_INPUT_STREAM_ERROR }, + e + ) + -1 + } catch (e: NullPointerException) { + // cannot happen - buffer is not null + -1 + } + } + + internal companion object { + internal const val CORRUPT_TLV_HEADER_TYPE_ERROR = "TLV header corrupt. Invalid type" + internal const val FAILED_TO_READ_FROM_INPUT_STREAM_ERROR = + "Failed to read from input stream" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt new file mode 100644 index 0000000000..04b84d8c03 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt @@ -0,0 +1,39 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.tlvformat + +import java.nio.ByteBuffer + +internal class TLVBlock( + val type: TLVBlockType, + val data: ByteArray +) { + internal fun serialize(): ByteArray? { + if (data.isEmpty()) return null + + val typeAsShort = type.rawValue.toShort() + val length = data.size + + // allocate IllegalArgumentException - cannot happen because + // capacity is always positive + // + // put BufferOverflowException - cannot happen because we calculate the capacity + // of the buffer to take into account the size of the TLV headers + // + // put/array ReadOnlyBufferException - cannot happen because ByteBuffer + // gives a mutable buffer + // + // array UnsupportedOperationException - ByteBuffer buffer is backed by array + @Suppress("UnsafeThirdPartyFunctionCall") + return ByteBuffer + .allocate(data.size + Int.SIZE_BYTES + Short.SIZE_BYTES) + .putShort(typeAsShort) + .putInt(length) + .put(data) + .array() + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockType.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockType.kt new file mode 100644 index 0000000000..55bd06c725 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockType.kt @@ -0,0 +1,21 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.tlvformat + +internal enum class TLVBlockType(val rawValue: UShort) { + LAST_UPDATE_DATE(0x00u), + VERSION_CODE(0x01u), + DATA(0x02u); + + companion object { + private val map = values().associateBy { it.rawValue } + + fun fromValue(value: UShort): TLVBlockType? { + return map[value] + } + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt index 56c9dd133b..2bc980082d 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt @@ -21,6 +21,8 @@ import com.datadog.android.core.configuration.Configuration import com.datadog.android.core.internal.net.info.BroadcastReceiverNetworkInfoProvider import com.datadog.android.core.internal.net.info.CallbackNetworkInfoProvider import com.datadog.android.core.internal.net.info.NoOpNetworkInfoProvider +import com.datadog.android.core.internal.persistence.datastore.DataStoreHandler +import com.datadog.android.core.internal.persistence.datastore.NoOpDataStoreHandler import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig import com.datadog.android.core.internal.persistence.file.batch.BatchFileReaderWriter import com.datadog.android.core.internal.privacy.ConsentProvider @@ -90,7 +92,6 @@ import java.io.File import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream -import java.lang.RuntimeException import java.net.Proxy import java.util.Locale import java.util.UUID @@ -838,6 +839,22 @@ internal class CoreFeatureTest { .isEqualTo(fakeConfig.coreConfig.batchSize.windowDurationMs) } + @Test + fun `M initialize the DataStoreHandler W initialize()`() { + // When + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // Then + assertThat(testedFeature.dataStoreHandler) + .isInstanceOf(DataStoreHandler::class.java) + .isNotInstanceOf(NoOpDataStoreHandler::class.java) + } + @Test fun `M initialize the NdkCrashHandler data W initialize() {main process}`( @TempDir tempDir: File, @@ -1248,6 +1265,23 @@ internal class CoreFeatureTest { assertThat(testedFeature.ndkCrashHandler).isInstanceOf(NoOpNdkCrashHandler::class.java) } + @Test + fun `M cleanup DataStoreHandler W stop()`() { + // Given + testedFeature.initialize( + appContext.mockInstance, + fakeSdkInstanceId, + fakeConfig, + fakeConsent + ) + + // When + testedFeature.stop() + + // Then + assertThat(testedFeature.dataStoreHandler).isInstanceOf(NoOpDataStoreHandler::class.java) + } + @Test fun `M cleanup app info W stop()`() { // Given diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt index daf85f7940..c88a15336f 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt @@ -31,12 +31,16 @@ import com.datadog.android.core.internal.metrics.BatchMetricsDispatcher import com.datadog.android.core.internal.metrics.NoOpMetricsDispatcher import com.datadog.android.core.internal.persistence.AbstractStorage import com.datadog.android.core.internal.persistence.ConsentAwareStorage +import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.internal.persistence.NoOpStorage import com.datadog.android.core.internal.persistence.Storage +import com.datadog.android.core.internal.persistence.datastore.CURRENT_DATASTORE_VERSION +import com.datadog.android.core.internal.persistence.datastore.DataStoreHandler import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig import com.datadog.android.core.internal.persistence.file.NoOpFileOrchestrator import com.datadog.android.core.internal.persistence.file.batch.BatchFileOrchestrator import com.datadog.android.core.persistence.PersistenceStrategy +import com.datadog.android.core.persistence.Serializer import com.datadog.android.privacy.TrackingConsent import com.datadog.android.privacy.TrackingConsentProviderCallback import com.datadog.android.utils.config.ApplicationContextTestConfiguration @@ -488,6 +492,72 @@ internal class SdkFeatureTest { verify(mockEventReceiver).onReceive(fakeEvent) } + @Test + fun `M return datastore version W getDataStoreCurrentVersion()`() { + // When + val result = testedFeature.getDataStoreCurrentVersion() + + // Then + assertThat(result).isEqualTo(CURRENT_DATASTORE_VERSION) + } + + @Test + fun `M call datastore read W readFromDataStore()`( + @StringForgery fakeDataStoreName: String, + @StringForgery fakeFeatureName: String, + @Mock mockDeserializer: Deserializer, + @Mock mockDataStoreHandler: DataStoreHandler + ) { + // Given + whenever(testedFeature.coreFeature.dataStoreHandler) + .thenReturn(mockDataStoreHandler) + + // When + testedFeature.readFromDataStore( + dataStoreFileName = fakeDataStoreName, + featureName = fakeFeatureName, + deserializer = mockDeserializer, + version = CURRENT_DATASTORE_VERSION + ) + + // Then + verify(testedFeature.coreFeature.dataStoreHandler).read( + dataStoreFileName = eq(fakeDataStoreName), + featureName = eq(fakeFeatureName), + deserializer = eq(mockDeserializer), + version = eq(CURRENT_DATASTORE_VERSION) + ) + } + + @Test + fun `M call datastore write W writeToDataStore()`( + @StringForgery fakeDataStoreName: String, + @StringForgery fakeFeatureName: String, + @StringForgery fakeDataString: String, + @Mock mockSerializer: Serializer, + @Mock mockDataStoreHandler: DataStoreHandler + ) { + // Given + whenever(testedFeature.coreFeature.dataStoreHandler) + .thenReturn(mockDataStoreHandler) + + // When + testedFeature.writeToDataStore( + dataStoreFileName = fakeDataStoreName, + featureName = fakeFeatureName, + serializer = mockSerializer, + data = fakeDataString + ) + + // Then + verify(testedFeature.coreFeature.dataStoreHandler).write( + dataStoreFileName = eq(fakeDataStoreName), + featureName = eq(fakeFeatureName), + serializer = eq(mockSerializer), + data = eq(fakeDataString) + ) + } + @Test fun `M notify no receiver W sendEvent(event)`() { // Given diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/FileDataStoreHandlerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/FileDataStoreHandlerTest.kt new file mode 100644 index 0000000000..b3c1ed4848 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/FileDataStoreHandlerTest.kt @@ -0,0 +1,464 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.datastore + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.internal.persistence.datastore.CURRENT_DATASTORE_VERSION +import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper +import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler +import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler.Companion.DATASTORE_FOLDER_NAME +import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler.Companion.FAILED_TO_SERIALIZE_DATA_ERROR +import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler.Companion.INVALID_NUMBER_OF_BLOCKS_ERROR +import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler.Companion.INVALID_VERSION_ERROR +import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler.Companion.SAME_BLOCK_APPEARS_TWICE_ERROR +import com.datadog.android.core.internal.persistence.file.FileReaderWriter +import com.datadog.android.core.internal.persistence.file.createNewFileSafe +import com.datadog.android.core.internal.persistence.file.deleteSafe +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.file.mkdirsSafe +import com.datadog.android.core.internal.persistence.tlvformat.FileTLVBlockReader +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockType +import com.datadog.android.core.persistence.Serializer +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import java.nio.ByteBuffer +import java.util.Locale + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class FileDataStoreHandlerTest { + private lateinit var testedDataStoreHandler: FileDataStoreHandler + + @Mock + lateinit var mockFileReaderWriter: FileReaderWriter + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockStorageDir: File + + @Mock + lateinit var mockDataStoreDirectory: File + + @Mock + lateinit var mockDataStoreFile: File + + @Mock + lateinit var mockSerializer: Serializer + + @Mock + lateinit var mockDeserializer: Deserializer + + @Mock + lateinit var mockFileTLVBlockReader: FileTLVBlockReader + + @Mock + lateinit var mockDataStoreFileHelper: DataStoreFileHelper + + @StringForgery + lateinit var fakeSdkInstanceId: String + + @StringForgery + lateinit var fakeDataStoreFileName: String + + @StringForgery + lateinit var fakeDataString: String + + @StringForgery + lateinit var fakeFeatureName: String + + private lateinit var fakeDataBytes: ByteArray + private lateinit var blocksReturned: ArrayList + + @BeforeEach + fun setup() { + fakeDataBytes = fakeDataString.toByteArray(Charsets.UTF_8) + + whenever(mockDataStoreDirectory.existsSafe(mockInternalLogger)).thenReturn(true) + whenever( + mockDataStoreFileHelper.getDataStoreDirectory( + sdkInstanceId = fakeSdkInstanceId, + featureName = fakeFeatureName, + folderName = DATASTORE_FOLDER_NAME.format(Locale.US, CURRENT_DATASTORE_VERSION), + storageDir = mockStorageDir + ) + ).thenReturn(mockDataStoreDirectory) + whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(true) + whenever( + mockDataStoreFileHelper.getDataStoreFile( + dataStoreDirectory = mockDataStoreDirectory, + dataStoreFileName = fakeDataStoreFileName + ) + ).thenReturn(mockDataStoreFile) + + whenever(mockSerializer.serialize(fakeDataString)).thenReturn(fakeDataString) + whenever(mockDeserializer.deserialize(fakeDataString)).thenReturn(fakeDataBytes) + + val versionBlock = createVersionBlock(true) + val lastUpdateDateBlock = createLastUpdateDateBlock(true) + val dataBlock = createDataBlock() + blocksReturned = arrayListOf(versionBlock, lastUpdateDateBlock, dataBlock) + whenever(mockFileTLVBlockReader.all(mockDataStoreFile)).thenReturn(blocksReturned) + + testedDataStoreHandler = FileDataStoreHandler( + sdkInstanceId = fakeSdkInstanceId, + fileReaderWriter = mockFileReaderWriter, + internalLogger = mockInternalLogger, + storageDir = mockStorageDir, + fileTLVBlockReader = mockFileTLVBlockReader, + dataStoreFileHelper = mockDataStoreFileHelper + ) + } + + // region read + + @Test + fun `M return null W read() { datastore file does not exist }`() { + // Given + whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(false) + + // When + val result = testedDataStoreHandler.read( + dataStoreFileName = fakeDataStoreFileName, + deserializer = mockDeserializer, + featureName = fakeFeatureName, + version = CURRENT_DATASTORE_VERSION + ) + + // Then + assertThat(result).isNull() + } + + @Test + fun `M return null W read() { invalid number of blocks }`() { + // Given + blocksReturned.removeLast() + + // When + val result = testedDataStoreHandler.read( + dataStoreFileName = fakeDataStoreFileName, + deserializer = mockDeserializer, + featureName = fakeFeatureName, + version = CURRENT_DATASTORE_VERSION + ) + + // Then + assertThat(result).isNull() + } + + @Test + fun `M log error W read() { invalid number of blocks }`() { + // Given + blocksReturned.removeLast() + + val expectedError = INVALID_NUMBER_OF_BLOCKS_ERROR.format(Locale.US, blocksReturned.size) + + // When + testedDataStoreHandler.read( + dataStoreFileName = fakeDataStoreFileName, + deserializer = mockDeserializer, + featureName = fakeFeatureName, + version = CURRENT_DATASTORE_VERSION + ) + + // Then + mockInternalLogger.verifyLog( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + message = expectedError + ) + } + + @Test + fun `M log error W read() { same block appears twice }`() { + // Given + blocksReturned.clear() + blocksReturned.add(createVersionBlock(true)) + blocksReturned.add(createVersionBlock(true)) + blocksReturned.add(createDataBlock()) + + val expectedError = SAME_BLOCK_APPEARS_TWICE_ERROR.format(Locale.US, TLVBlockType.VERSION_CODE) + + // When + testedDataStoreHandler.read( + dataStoreFileName = fakeDataStoreFileName, + deserializer = mockDeserializer, + featureName = fakeFeatureName, + version = CURRENT_DATASTORE_VERSION + ) + + // Then + mockInternalLogger.verifyLog( + level = InternalLogger.Level.ERROR, + target = InternalLogger.Target.MAINTAINER, + message = expectedError + ) + } + + @Test + fun `M log error W read() { version too old }`() { + // Given + blocksReturned.clear() + blocksReturned.add(createLastUpdateDateBlock(true)) + blocksReturned.add(createVersionBlock(false)) + blocksReturned.add(createDataBlock()) + + // When + testedDataStoreHandler.read( + dataStoreFileName = fakeDataStoreFileName, + deserializer = mockDeserializer, + featureName = fakeFeatureName, + version = CURRENT_DATASTORE_VERSION + ) + + // Then + mockInternalLogger.verifyLog( + level = InternalLogger.Level.ERROR, + target = InternalLogger.Target.MAINTAINER, + message = INVALID_VERSION_ERROR + ) + } + + @Test + fun `M delete datastore file W read() { version too old }`() { + // Given + blocksReturned.clear() + blocksReturned.add(createLastUpdateDateBlock(true)) + blocksReturned.add(createVersionBlock(false)) + blocksReturned.add(createDataBlock()) + + // When + testedDataStoreHandler.read( + dataStoreFileName = fakeDataStoreFileName, + deserializer = mockDeserializer, + featureName = fakeFeatureName, + version = CURRENT_DATASTORE_VERSION + ) + + // Then + verify(mockDataStoreFile).deleteSafe(mockInternalLogger) + } + + @Test + fun `M return null W read() { version too old }`() { + // Given + blocksReturned.clear() + blocksReturned.add(createLastUpdateDateBlock(true)) + blocksReturned.add(createVersionBlock(false)) + blocksReturned.add(createDataBlock()) + + // When + val result = testedDataStoreHandler.read( + dataStoreFileName = fakeDataStoreFileName, + deserializer = mockDeserializer, + featureName = fakeFeatureName, + version = CURRENT_DATASTORE_VERSION + ) + + // Then + assertThat(result).isNull() + } + + @Test + fun `M return null W read() { last update over threshold }`() { + // Given + blocksReturned.clear() + blocksReturned.add(createLastUpdateDateBlock(false)) + blocksReturned.add(createVersionBlock(true)) + blocksReturned.add(createDataBlock()) + + // When + val result = testedDataStoreHandler.read( + dataStoreFileName = fakeDataStoreFileName, + deserializer = mockDeserializer, + featureName = fakeFeatureName, + version = CURRENT_DATASTORE_VERSION + ) + + // Then + assertThat(result).isNull() + } + + @Test + fun `M delete datastore file W read() { last update over threshold }`() { + // Given + blocksReturned.clear() + blocksReturned.add(createLastUpdateDateBlock(false)) + blocksReturned.add(createVersionBlock(true)) + blocksReturned.add(createDataBlock()) + + // When + testedDataStoreHandler.read( + dataStoreFileName = fakeDataStoreFileName, + deserializer = mockDeserializer, + featureName = fakeFeatureName, + version = CURRENT_DATASTORE_VERSION + ) + + // Then + verify(mockDataStoreFile).deleteSafe(mockInternalLogger) + } + + @Test + fun `M return deserialized data W read()`() { + // Given + blocksReturned.clear() + blocksReturned.add(createLastUpdateDateBlock(true)) + blocksReturned.add(createVersionBlock(true)) + blocksReturned.add(createDataBlock()) + + // When + val result = testedDataStoreHandler.read( + dataStoreFileName = fakeDataStoreFileName, + deserializer = mockDeserializer, + featureName = fakeFeatureName, + version = CURRENT_DATASTORE_VERSION + ) + + // Then + assertThat(result).isEqualTo(fakeDataBytes) + } + + // endregion + + // region write + + @Test + fun `M not write to file W write() { unable to serialize data }`() { + // Given + whenever(mockSerializer.serialize(fakeDataString)).thenReturn(null) + + // When + testedDataStoreHandler.write( + dataStoreFileName = fakeDataStoreFileName, + featureName = fakeFeatureName, + serializer = mockSerializer, + data = fakeDataString + ) + + // Then + verifyNoInteractions(mockFileReaderWriter) + } + + @Test + fun `M log error W write() { unable to serialize data }`() { + // Given + whenever(mockSerializer.serialize(fakeDataString)).thenReturn(null) + + // When + testedDataStoreHandler.write( + dataStoreFileName = fakeDataStoreFileName, + featureName = fakeFeatureName, + serializer = mockSerializer, + data = fakeDataString + ) + + // Then + mockInternalLogger.verifyLog( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + message = FAILED_TO_SERIALIZE_DATA_ERROR + ) + } + + @Test + fun `M create directory paths W write() { directory does not already exist }`() { + // Given + whenever(mockDataStoreDirectory.existsSafe(mockInternalLogger)).thenReturn(false) + + // When + testedDataStoreHandler.write( + dataStoreFileName = fakeDataStoreFileName, + featureName = fakeFeatureName, + serializer = mockSerializer, + data = fakeDataString + ) + + // Then + verify(mockDataStoreDirectory).mkdirsSafe(mockInternalLogger) + } + + @Test + fun `M create new datastore file W write() { file does not already exist }`() { + // Given + whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(false) + + // When + testedDataStoreHandler.write( + dataStoreFileName = fakeDataStoreFileName, + featureName = fakeFeatureName, + serializer = mockSerializer, + data = fakeDataString + ) + + // Then + verify(mockDataStoreFile).createNewFileSafe(mockInternalLogger) + } + + // endregion + + private fun createVersionBlock(valid: Boolean): TLVBlock { + return if (valid) { + TLVBlock( + type = TLVBlockType.VERSION_CODE, + data = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(CURRENT_DATASTORE_VERSION).array() + ) + } else { + TLVBlock( + type = TLVBlockType.VERSION_CODE, + data = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(CURRENT_DATASTORE_VERSION - 1).array() + ) + } + } + + private fun createLastUpdateDateBlock(valid: Boolean): TLVBlock { + return if (valid) { + TLVBlock( + type = TLVBlockType.LAST_UPDATE_DATE, + data = ByteBuffer.allocate(Long.SIZE_BYTES) + .putLong(System.currentTimeMillis()) + .array() + ) + } else { + TLVBlock( + type = TLVBlockType.LAST_UPDATE_DATE, + data = ByteBuffer.allocate(Long.SIZE_BYTES) + .putLong(0) + .array() + ) + } + } + + private fun createDataBlock(): TLVBlock = + TLVBlock( + type = TLVBlockType.DATA, + data = fakeDataBytes + ) +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/FileTLVBlockReaderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/FileTLVBlockReaderTest.kt new file mode 100644 index 0000000000..aa8ef8c7b4 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/FileTLVBlockReaderTest.kt @@ -0,0 +1,145 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.tlvformat + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.file.FileReaderWriter +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.file.lengthSafe +import com.datadog.android.core.internal.persistence.tlvformat.FileTLVBlockReader.Companion.CORRUPT_TLV_HEADER_TYPE_ERROR +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import java.nio.ByteBuffer + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class FileTLVBlockReaderTest { + private lateinit var testedReader: FileTLVBlockReader + + @Mock + private lateinit var mockFile: File + + @Mock + private lateinit var mockFileReaderWriter: FileReaderWriter + + @Mock + private lateinit var mockInternalLogger: InternalLogger + + @StringForgery + private lateinit var fakeDataString: String + + private lateinit var fakeDataBytes: ByteArray + private lateinit var fakeBufferBytes: ByteArray + + @BeforeEach + fun setup() { + fakeDataBytes = fakeDataString.toByteArray(Charsets.UTF_8) + val length = fakeDataBytes.size + val type = TLVBlockType.DATA + + val buffer = ByteBuffer.allocate(fakeDataBytes.size + 6) + buffer.putShort(type.rawValue.toShort()) + buffer.putInt(length) + buffer.put(fakeDataBytes) + + fakeBufferBytes = buffer.array() + + whenever(mockFile.existsSafe(mockInternalLogger)).thenReturn(true) + whenever(mockFile.lengthSafe(mockInternalLogger)).thenReturn(fakeBufferBytes.size.toLong()) + whenever(mockFileReaderWriter.readData(mockFile)).thenReturn(fakeBufferBytes) + + testedReader = FileTLVBlockReader( + fileReaderWriter = mockFileReaderWriter, + internalLogger = mockInternalLogger + ) + } + + @Test + fun `M return empty collection W all() { file does not exist }`() { + // Given + whenever(mockFile.existsSafe(mockInternalLogger)).thenReturn(false) + + // When + val readBytes = testedReader.all(file = mockFile) + + // Then + assertThat(readBytes).isEmpty() + } + + @Test + fun `M return empty collection W all() { empty file }`() { + // Given + whenever(mockFile.lengthSafe(mockInternalLogger)).thenReturn(0L) + + // When + val readBytes = testedReader.all(file = mockFile) + + // Then + assertThat(readBytes).isEmpty() + } + + @Test + fun `M return empty collection W all() { invalid TLV type }`() { + // Given + fakeBufferBytes = fakeDataString.toByteArray(Charsets.UTF_8) + whenever(mockFileReaderWriter.readData(mockFile)) + .thenReturn(fakeBufferBytes) + + // When + val readBytes = testedReader.all(file = mockFile) + + // Then + assertThat(readBytes).isEmpty() + } + + @Test + fun `M log error W all() { invalid TLV type }`() { + // Given + fakeBufferBytes = fakeDataString.toByteArray(Charsets.UTF_8) + whenever(mockFileReaderWriter.readData(mockFile)) + .thenReturn(fakeBufferBytes) + + // When + testedReader.all(file = mockFile) + + // Then + mockInternalLogger.verifyLog( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + message = CORRUPT_TLV_HEADER_TYPE_ERROR + ) + } + + @Test + fun `M return valid object W all() { valid TLV format }`() { + // When + val tlvArray = testedReader.all(file = mockFile) + val dataObject = tlvArray[0] + + // Then + assertThat(dataObject.type).isEqualTo(TLVBlockType.DATA) + assertThat(dataObject.data).isEqualTo(fakeDataBytes) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt new file mode 100644 index 0000000000..6f907fdd97 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt @@ -0,0 +1,81 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.tlvformat + +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.annotation.IntForgery +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.nio.ByteBuffer + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class TLVBlockTest { + private lateinit var testedTLVBlock: TLVBlock + + @Mock + lateinit var mockTLVBlockType: TLVBlockType + + @Test + fun `M return null W serialize() { empty data }`() { + // Given + testedTLVBlock = TLVBlock( + type = mockTLVBlockType, + data = ByteArray(0) + ) + + // When + val block = testedTLVBlock.serialize() + + // Then + assertThat(block).isNull() + } + + @Test + fun `M use appropriate TLV datatypes W serialize() { has data }`( + @StringForgery fakeString: String, + @IntForgery(min = 0, max = 10) fakeTypeAsInt: Int + ) { + // Given + val fakeTLVType = fakeTypeAsInt.toShort() + val fakeByteArray = fakeString.toByteArray(Charsets.UTF_8) + whenever(mockTLVBlockType.rawValue).thenReturn(fakeTLVType.toUShort()) + + testedTLVBlock = TLVBlock( + type = mockTLVBlockType, + data = fakeByteArray + ) + + // When + val block = testedTLVBlock.serialize() + + // Then + assertThat(block?.size).isEqualTo(fakeString.length + 6) + val type = block?.copyOfRange(0, 2) + val length = block?.copyOfRange(2, 6) + val data = block?.copyOfRange(6, block.size) + val typeAsShort = type?.let { ByteBuffer.wrap(it).getShort() } + val lengthAsInt = length?.let { ByteBuffer.wrap(it).getInt() } + + assertThat(typeAsShort).isEqualTo(fakeTLVType) + assertThat(lengthAsInt).isEqualTo(data?.size) + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTypeTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTypeTest.kt new file mode 100644 index 0000000000..e475bc30be --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTypeTest.kt @@ -0,0 +1,44 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.tlvformat + +import com.datadog.android.utils.forge.Configurator +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class TLVBlockTypeTest { + @Test + fun `M return type value W fromValue() { existing value }`() { + // When + val shortValue = TLVBlockType.fromValue(TLVBlockType.LAST_UPDATE_DATE.rawValue) + + // Then + assertThat(shortValue).isEqualTo(TLVBlockType.LAST_UPDATE_DATE) + } + + @Test + fun `M return null W fromValue() { nonexistent value }`() { + // When + val shortValue = TLVBlockType.fromValue(999u) + + // Then + assertThat(shortValue).isNull() + } +} diff --git a/detekt_custom.yml b/detekt_custom.yml index 0c931af1ff..604ce5c950 100644 --- a/detekt_custom.yml +++ b/detekt_custom.yml @@ -714,6 +714,7 @@ datadog: - "java.security.SecureRandom.nextFloat()" - "java.security.SecureRandom.nextInt()" - "java.security.SecureRandom.nextLong()" + - "java.util.HashSet.addAll(kotlin.collections.Collection)" - "java.util.HashSet.find(kotlin.Function1)" - "java.util.LinkedList.addFirst(com.datadog.android.webview.internal.rum.domain.WebViewNativeRumViewsCache.ViewEntry?)" - "java.util.LinkedList.peekLast()" @@ -747,6 +748,7 @@ datadog: # endregion # region Kotlin Collections - "kotlin.Array.all(kotlin.Function1)" + - "kotlin.Array.associateBy(kotlin.Function1)" - "kotlin.Array.constructor(kotlin.Int, kotlin.Function1)" - "kotlin.Array.contentEquals(kotlin.Array?)" - "kotlin.Array.contentHashCode()" @@ -770,6 +772,7 @@ datadog: - "kotlin.ByteArray.isNotEmpty()" - "kotlin.ByteArray.joinToString(kotlin.CharSequence, kotlin.CharSequence, kotlin.CharSequence, kotlin.Int, kotlin.CharSequence, kotlin.Function1?)" - "kotlin.arrayOf(kotlin.Array)" + - "kotlin.collections.arrayListOf()" - "kotlin.collections.ArrayList(kotlin.collections.MutableCollection?)" - "kotlin.collections.Collection.flatten()" - "kotlin.collections.Collection.isNotEmpty()" @@ -861,6 +864,7 @@ datadog: - "kotlin.collections.MutableIterator.hasNext()" - "kotlin.collections.MutableList.add(com.datadog.android.api.InternalLogger.Target)" - "kotlin.collections.MutableList.add(com.datadog.android.core.internal.persistence.Batch)" + - "kotlin.collections.MutableList.add(com.datadog.android.core.internal.persistence.tlvformat.TLVBlock)" - "kotlin.collections.MutableList.add(com.datadog.android.plugin.DatadogPlugin)" - "kotlin.collections.MutableList.add(com.datadog.android.rum.internal.domain.scope.RumScope)" - "kotlin.collections.MutableList.add(com.datadog.android.rum.model.ActionEvent.Type)" @@ -1006,6 +1010,7 @@ datadog: - "kotlin.Boolean.hashCode()" - "kotlin.Byte.toInt()" - "kotlin.ByteArray.constructor(kotlin.Int)" + - "kotlin.ByteArray.inputStream()" - "kotlin.Char.isLowerCase()" - "kotlin.Char.titlecase(java.util.Locale)" - "kotlin.CharArray.constructor(kotlin.Int, kotlin.Function1)" @@ -1037,6 +1042,8 @@ datadog: - "kotlin.LongArray.constructor(kotlin.Int)" - "kotlin.Number.toFloat()" - "kotlin.Number.toLong()" + - "kotlin.Short.toUShort()" + - "kotlin.UShort.toShort()" - "kotlin.math.abs(kotlin.Float)" - "kotlin.math.max(kotlin.Double, kotlin.Double)" - "kotlin.math.max(kotlin.Int, kotlin.Int)" From 1e40058c39bcbd3b43bf761a354ee54225a5e843 Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Tue, 21 May 2024 10:46:20 +0300 Subject: [PATCH 02/14] RUM-4098: Fix OutdatedDocumentation error --- .../main/kotlin/com/datadog/android/api/feature/FeatureScope.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt index 2eac046ffe..c8bc92941d 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt @@ -35,6 +35,7 @@ interface FeatureScope { /** * Write data to the datastore. * + * @param T datatype of the data to write to the datastore. * @param dataStoreFileName name of the datastore file as there could be multiple such files per feature. * @param featureName of the calling feature, to determine the path to the datastore file. * @param serializer to use to serialize the data. @@ -50,6 +51,7 @@ interface FeatureScope { /** * Read data from the datastore. * + * @param T datatype of the data to read from the datastore. * @param dataStoreFileName name of the datastore file as there could be multiple such files per feature. * @param featureName of the calling feature, to determine the path to the datastore file. * @param deserializer to use to deserialize the data. From 6645987eca269d2bbbca0825dfd26f30a10ff678 Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Sun, 26 May 2024 10:13:48 +0300 Subject: [PATCH 03/14] RUM-4098: Expose datastore as a property in corefeature --- dd-sdk-android-core/api/apiSurface | 9 +- .../api/dd-sdk-android-core.api | 16 ++- .../android/api/feature/FeatureScope.kt | 45 ++------ .../android/core/internal/CoreFeature.kt | 35 +----- .../android/core/internal/SdkFeature.kt | 66 ++++++------ .../persistence/datastore/DataStoreHandler.kt | 28 ++++- .../android/core/internal/CoreFeatureTest.kt | 17 --- .../android/core/internal/SdkFeatureTest.kt | 101 ++++++------------ 8 files changed, 118 insertions(+), 199 deletions(-) diff --git a/dd-sdk-android-core/api/apiSurface b/dd-sdk-android-core/api/apiSurface index 1e9920c61e..ce18e1f827 100644 --- a/dd-sdk-android-core/api/apiSurface +++ b/dd-sdk-android-core/api/apiSurface @@ -102,10 +102,8 @@ interface com.datadog.android.api.feature.FeatureContextUpdateReceiver interface com.datadog.android.api.feature.FeatureEventReceiver fun onReceive(Any) interface com.datadog.android.api.feature.FeatureScope + var dataStore: com.datadog.android.core.internal.persistence.datastore.DataStoreHandler fun withWriteContext(Boolean = false, (com.datadog.android.api.context.DatadogContext, com.datadog.android.api.storage.EventBatchWriter) -> Unit) - fun writeToDataStore(String, String, com.datadog.android.core.persistence.Serializer, T) - fun readFromDataStore(String, String, com.datadog.android.core.internal.persistence.Deserializer, Int): T? - fun getDataStoreCurrentVersion(): Int fun sendEvent(Any) fun unwrap(): T fun com.datadog.android.api.InternalLogger.measureMethodCallPerf(Class<*>, String, Float = 100f, () -> R): R @@ -249,6 +247,11 @@ interface com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver fun isEmpty(): Boolean interface com.datadog.android.core.internal.persistence.Deserializer fun deserialize(P): R? +interface com.datadog.android.core.internal.persistence.datastore.DataStoreHandler + fun write(String, String, com.datadog.android.core.persistence.Serializer, T) + fun read(String, String, com.datadog.android.core.internal.persistence.Deserializer, Int): T? + companion object + const val CURRENT_DATASTORE_VERSION: Int fun java.io.File.canReadSafe(com.datadog.android.api.InternalLogger): Boolean fun java.io.File.existsSafe(com.datadog.android.api.InternalLogger): Boolean fun java.io.File.readTextSafe(java.nio.charset.Charset = Charsets.UTF_8, com.datadog.android.api.InternalLogger): String? diff --git a/dd-sdk-android-core/api/dd-sdk-android-core.api b/dd-sdk-android-core/api/dd-sdk-android-core.api index 3d198e74df..4ae043b7e0 100644 --- a/dd-sdk-android-core/api/dd-sdk-android-core.api +++ b/dd-sdk-android-core/api/dd-sdk-android-core.api @@ -325,12 +325,11 @@ public abstract interface class com/datadog/android/api/feature/FeatureEventRece } public abstract interface class com/datadog/android/api/feature/FeatureScope { - public abstract fun getDataStoreCurrentVersion ()I - public abstract fun readFromDataStore (Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Deserializer;I)Ljava/lang/Object; + public abstract fun getDataStore ()Lcom/datadog/android/core/internal/persistence/datastore/DataStoreHandler; public abstract fun sendEvent (Ljava/lang/Object;)V + public abstract fun setDataStore (Lcom/datadog/android/core/internal/persistence/datastore/DataStoreHandler;)V public abstract fun unwrap ()Lcom/datadog/android/api/feature/Feature; public abstract fun withWriteContext (ZLkotlin/jvm/functions/Function2;)V - public abstract fun writeToDataStore (Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/core/persistence/Serializer;Ljava/lang/Object;)V } public final class com/datadog/android/api/feature/FeatureScope$DefaultImpls { @@ -680,6 +679,17 @@ public abstract interface class com/datadog/android/core/internal/persistence/De public abstract fun deserialize (Ljava/lang/Object;)Ljava/lang/Object; } +public abstract interface class com/datadog/android/core/internal/persistence/datastore/DataStoreHandler { + public static final field CURRENT_DATASTORE_VERSION I + public static final field Companion Lcom/datadog/android/core/internal/persistence/datastore/DataStoreHandler$Companion; + public abstract fun read (Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Deserializer;I)Ljava/lang/Object; + public abstract fun write (Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/core/persistence/Serializer;Ljava/lang/Object;)V +} + +public final class com/datadog/android/core/internal/persistence/datastore/DataStoreHandler$Companion { + public static final field CURRENT_DATASTORE_VERSION I +} + public final class com/datadog/android/core/internal/persistence/file/FileExtKt { public static final fun canReadSafe (Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Z public static final fun existsSafe (Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Z diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt index c8bc92941d..40d32d4166 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt @@ -9,14 +9,18 @@ package com.datadog.android.api.feature import androidx.annotation.AnyThread import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.storage.EventBatchWriter -import com.datadog.android.core.internal.persistence.Deserializer -import com.datadog.android.core.persistence.Serializer +import com.datadog.android.core.internal.persistence.datastore.DataStoreHandler /** * Represents a Datadog feature. */ interface FeatureScope { + /** + * Property to enable interaction with the DataStore. + */ + var dataStore: DataStoreHandler + /** * Utility to write an event, asynchronously. * @param forceNewBatch if `true` forces the [EventBatchWriter] to write in a new file and @@ -32,43 +36,6 @@ interface FeatureScope { callback: (DatadogContext, EventBatchWriter) -> Unit ) - /** - * Write data to the datastore. - * - * @param T datatype of the data to write to the datastore. - * @param dataStoreFileName name of the datastore file as there could be multiple such files per feature. - * @param featureName of the calling feature, to determine the path to the datastore file. - * @param serializer to use to serialize the data. - * @param data to write. - */ - fun writeToDataStore( - dataStoreFileName: String, - featureName: String, - serializer: Serializer, - data: T - ) - - /** - * Read data from the datastore. - * - * @param T datatype of the data to read from the datastore. - * @param dataStoreFileName name of the datastore file as there could be multiple such files per feature. - * @param featureName of the calling feature, to determine the path to the datastore file. - * @param deserializer to use to deserialize the data. - * @param version to use when reading from the datastore (to support migrations). - */ - fun readFromDataStore( - dataStoreFileName: String, - featureName: String, - deserializer: Deserializer, - version: Int - ): T? - - /** - * Return the current version of the datastore. - */ - fun getDataStoreCurrentVersion(): Int - /** * Send event to a given feature. It will be sent in a synchronous way. * diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt index 10d9198cba..a8b89055bd 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt @@ -35,9 +35,7 @@ import com.datadog.android.core.internal.net.info.NetworkInfoDeserializer import com.datadog.android.core.internal.net.info.NetworkInfoProvider import com.datadog.android.core.internal.net.info.NoOpNetworkInfoProvider import com.datadog.android.core.internal.persistence.JsonObjectDeserializer -import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper import com.datadog.android.core.internal.persistence.datastore.DataStoreHandler -import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler import com.datadog.android.core.internal.persistence.datastore.NoOpDataStoreHandler import com.datadog.android.core.internal.persistence.file.FileMover import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig @@ -49,7 +47,6 @@ import com.datadog.android.core.internal.persistence.file.deleteSafe import com.datadog.android.core.internal.persistence.file.existsSafe import com.datadog.android.core.internal.persistence.file.readTextSafe import com.datadog.android.core.internal.persistence.file.writeTextSafe -import com.datadog.android.core.internal.persistence.tlvformat.FileTLVBlockReader import com.datadog.android.core.internal.privacy.ConsentProvider import com.datadog.android.core.internal.privacy.NoOpConsentProvider import com.datadog.android.core.internal.privacy.TrackingConsentProvider @@ -201,6 +198,7 @@ internal class CoreFeature( if (initialized.get()) { return } + readConfigurationSettings(configuration.coreConfig) readApplicationInformation(appContext, configuration) resolveProcessInfo(appContext) @@ -231,10 +229,7 @@ internal class CoreFeature( val nativeSourceOverride = configuration.additionalConfig[Datadog.DD_NATIVE_SOURCE_TYPE] as? String prepareNdkCrashData(nativeSourceOverride) setupInfoProviders(appContext, consent) - prepareDataStoreHandler( - sdkInstanceId = sdkInstanceId, - configuration = configuration.coreConfig - ) + initialized.set(true) contextProvider = DatadogContextProvider(this) } @@ -380,28 +375,6 @@ internal class CoreFeature( } } - private fun prepareDataStoreHandler( - sdkInstanceId: String, - configuration: Configuration.Core - ) { - val fileReaderWriter = FileReaderWriter.create( - internalLogger, - configuration.encryption - ) - - dataStoreHandler = FileDataStoreHandler( - sdkInstanceId = sdkInstanceId, - storageDir = storageDir, - internalLogger = internalLogger, - fileReaderWriter = fileReaderWriter, - fileTLVBlockReader = FileTLVBlockReader( - internalLogger = internalLogger, - fileReaderWriter = fileReaderWriter - ), - dataStoreFileHelper = DataStoreFileHelper() - ) - } - private fun prepareNdkCrashData(nativeSourceType: String?) { if (isMainProcess) { ndkCrashHandler = DatadogNdkCrashHandler( @@ -737,8 +710,8 @@ internal class CoreFeature( internal const val BUILD_ID_FILE_NAME = "datadog.buildId" internal const val BUILD_ID_IS_MISSING_INFO_MESSAGE = "Build ID is not found in the application" + - " assets. If you are using obfuscation, please use Datadog Gradle Plugin 1.13.0" + - " or above to be able to de-obfuscate stacktraces." + " assets. If you are using obfuscation, please use Datadog Gradle Plugin 1.13.0" + + " or above to be able to de-obfuscate stacktraces." internal const val BUILD_ID_READ_ERROR = "Failed to read Build ID information, de-obfuscation may not work properly." diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt index bbf40c6be5..47236101f3 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt @@ -34,10 +34,12 @@ import com.datadog.android.core.internal.metrics.MetricsDispatcher import com.datadog.android.core.internal.metrics.NoOpMetricsDispatcher import com.datadog.android.core.internal.persistence.AbstractStorage import com.datadog.android.core.internal.persistence.ConsentAwareStorage -import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.internal.persistence.NoOpStorage import com.datadog.android.core.internal.persistence.Storage -import com.datadog.android.core.internal.persistence.datastore.CURRENT_DATASTORE_VERSION +import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper +import com.datadog.android.core.internal.persistence.datastore.DataStoreHandler +import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler +import com.datadog.android.core.internal.persistence.datastore.NoOpDataStoreHandler import com.datadog.android.core.internal.persistence.file.FileMover import com.datadog.android.core.internal.persistence.file.FileOrchestrator import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig @@ -45,9 +47,10 @@ import com.datadog.android.core.internal.persistence.file.FileReaderWriter import com.datadog.android.core.internal.persistence.file.NoOpFileOrchestrator import com.datadog.android.core.internal.persistence.file.advanced.FeatureFileOrchestrator import com.datadog.android.core.internal.persistence.file.batch.BatchFileReaderWriter +import com.datadog.android.core.internal.persistence.tlvformat.FileTLVBlockReader import com.datadog.android.core.persistence.PersistenceStrategy -import com.datadog.android.core.persistence.Serializer import com.datadog.android.privacy.TrackingConsentProviderCallback +import com.datadog.android.security.Encryption import java.util.Collections import java.util.Locale import java.util.concurrent.ConcurrentHashMap @@ -61,6 +64,8 @@ internal class SdkFeature( internal val internalLogger: InternalLogger ) : FeatureScope { + override var dataStore: DataStoreHandler = NoOpDataStoreHandler() + internal val initialized = AtomicBoolean(false) @Suppress("UnsafeThirdPartyFunctionCall") // the argument is always empty @@ -108,6 +113,10 @@ internal class SdkFeature( coreFeature.trackingConsentProvider.registerCallback(wrappedFeature) } + prepareDataStoreHandler( + encryption = coreFeature.localDataEncryption + ) + initialized.set(true) uploadScheduler.startScheduling() @@ -132,6 +141,7 @@ internal class SdkFeature( uploadScheduler.stopScheduling() uploadScheduler = NoOpUploadScheduler() storage = NoOpStorage() + dataStore = NoOpDataStoreHandler() uploader = NoOpDataUploader() fileOrchestrator = NoOpFileOrchestrator() metricsDispatcher = NoOpMetricsDispatcher() @@ -146,36 +156,6 @@ internal class SdkFeature( // region FeatureScope - override fun writeToDataStore( - dataStoreFileName: String, - featureName: String, - serializer: Serializer, - data: T - ) { - coreFeature.dataStoreHandler.write( - dataStoreFileName = dataStoreFileName, - featureName = featureName, - serializer = serializer, - data = data - ) - } - - override fun readFromDataStore( - dataStoreFileName: String, - featureName: String, - deserializer: Deserializer, - version: Int - ): T? = - coreFeature.dataStoreHandler.read( - dataStoreFileName = dataStoreFileName, - featureName = featureName, - deserializer = deserializer, - version = version - ) - - override fun getDataStoreCurrentVersion(): Int = - CURRENT_DATASTORE_VERSION - override fun withWriteContext( forceNewBatch: Boolean, callback: (DatadogContext, EventBatchWriter) -> Unit @@ -376,6 +356,26 @@ internal class SdkFeature( ) } + private fun prepareDataStoreHandler( + encryption: Encryption? + ) { + val fileReaderWriter = FileReaderWriter.create( + internalLogger, + encryption + ) + + dataStore = FileDataStoreHandler( + storageDir = coreFeature.storageDir, + internalLogger = internalLogger, + fileReaderWriter = fileReaderWriter, + fileTLVBlockReader = FileTLVBlockReader( + internalLogger = internalLogger, + fileReaderWriter = fileReaderWriter + ), + dataStoreFileHelper = DataStoreFileHelper() + ) + } + // endregion // Used for nightly tests only diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreHandler.kt index d6f8357a3e..cc817cff80 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreHandler.kt @@ -9,9 +9,17 @@ package com.datadog.android.core.internal.persistence.datastore import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.persistence.Serializer -internal const val CURRENT_DATASTORE_VERSION: Int = 0 +interface DataStoreHandler { -internal interface DataStoreHandler { + /** + * Write data to the datastore. + * + * @param T datatype of the data to write to the datastore. + * @param dataStoreFileName name of the datastore file as there could be multiple such files per feature. + * @param featureName of the calling feature, to determine the path to the datastore file. + * @param serializer to use to serialize the data. + * @param data to write. + */ fun write( dataStoreFileName: String, featureName: String, @@ -19,10 +27,26 @@ internal interface DataStoreHandler { data: T ) + /** + * Read data from the datastore. + * + * @param T datatype of the data to read from the datastore. + * @param dataStoreFileName name of the datastore file as there could be multiple such files per feature. + * @param featureName of the calling feature, to determine the path to the datastore file. + * @param deserializer to use to deserialize the data. + * @param version to use when reading from the datastore (to support migrations). + */ fun read( dataStoreFileName: String, featureName: String, deserializer: Deserializer, version: Int ): T? + + companion object { + /** + * The current version of the datastore. + */ + const val CURRENT_DATASTORE_VERSION: Int = 0 + } } diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt index 2bc980082d..99ee0a9dec 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt @@ -21,7 +21,6 @@ import com.datadog.android.core.configuration.Configuration import com.datadog.android.core.internal.net.info.BroadcastReceiverNetworkInfoProvider import com.datadog.android.core.internal.net.info.CallbackNetworkInfoProvider import com.datadog.android.core.internal.net.info.NoOpNetworkInfoProvider -import com.datadog.android.core.internal.persistence.datastore.DataStoreHandler import com.datadog.android.core.internal.persistence.datastore.NoOpDataStoreHandler import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig import com.datadog.android.core.internal.persistence.file.batch.BatchFileReaderWriter @@ -839,22 +838,6 @@ internal class CoreFeatureTest { .isEqualTo(fakeConfig.coreConfig.batchSize.windowDurationMs) } - @Test - fun `M initialize the DataStoreHandler W initialize()`() { - // When - testedFeature.initialize( - appContext.mockInstance, - fakeSdkInstanceId, - fakeConfig, - fakeConsent - ) - - // Then - assertThat(testedFeature.dataStoreHandler) - .isInstanceOf(DataStoreHandler::class.java) - .isNotInstanceOf(NoOpDataStoreHandler::class.java) - } - @Test fun `M initialize the NdkCrashHandler data W initialize() {main process}`( @TempDir tempDir: File, diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt index c88a15336f..bfe447e40a 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt @@ -31,16 +31,14 @@ import com.datadog.android.core.internal.metrics.BatchMetricsDispatcher import com.datadog.android.core.internal.metrics.NoOpMetricsDispatcher import com.datadog.android.core.internal.persistence.AbstractStorage import com.datadog.android.core.internal.persistence.ConsentAwareStorage -import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.internal.persistence.NoOpStorage import com.datadog.android.core.internal.persistence.Storage -import com.datadog.android.core.internal.persistence.datastore.CURRENT_DATASTORE_VERSION import com.datadog.android.core.internal.persistence.datastore.DataStoreHandler +import com.datadog.android.core.internal.persistence.datastore.NoOpDataStoreHandler import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig import com.datadog.android.core.internal.persistence.file.NoOpFileOrchestrator import com.datadog.android.core.internal.persistence.file.batch.BatchFileOrchestrator import com.datadog.android.core.persistence.PersistenceStrategy -import com.datadog.android.core.persistence.Serializer import com.datadog.android.privacy.TrackingConsent import com.datadog.android.privacy.TrackingConsentProviderCallback import com.datadog.android.utils.config.ApplicationContextTestConfiguration @@ -88,7 +86,7 @@ import java.util.Locale @ForgeConfiguration(Configurator::class) internal class SdkFeatureTest { - lateinit var testedFeature: SdkFeature + private lateinit var testedFeature: SdkFeature @Mock lateinit var mockStorage: Storage @@ -152,7 +150,7 @@ internal class SdkFeatureTest { testedFeature.initialize(appContext.mockInstance, fakeInstanceId) // Then - argumentCaptor() { + argumentCaptor { verify((appContext.mockInstance)).registerActivityLifecycleCallbacks(capture()) assertThat(firstValue).isInstanceOf(ProcessLifecycleMonitor::class.java) assertThat((firstValue as ProcessLifecycleMonitor).callback) @@ -428,6 +426,33 @@ internal class SdkFeatureTest { // region FeatureScope + @Test + fun `M unregister datastore W stop()`() { + // Given + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + assertThat(testedFeature.dataStore) + .isInstanceOf(DataStoreHandler::class.java) + .isNotInstanceOf(NoOpDataStoreHandler::class.java) + + // When + testedFeature.stop() + + // Then + assertThat(testedFeature.dataStore).isInstanceOf(NoOpDataStoreHandler::class.java) + } + + @Test + fun `M register datastore W initialize()`() { + // When + testedFeature.initialize(appContext.mockInstance, fakeInstanceId) + + // Then + assertThat(testedFeature.dataStore) + .isInstanceOf(DataStoreHandler::class.java) + .isNotInstanceOf(NoOpDataStoreHandler::class.java) + } + @Test fun `M provide write context W withWriteContext(callback)`( @BoolForgery forceNewBatch: Boolean, @@ -492,72 +517,6 @@ internal class SdkFeatureTest { verify(mockEventReceiver).onReceive(fakeEvent) } - @Test - fun `M return datastore version W getDataStoreCurrentVersion()`() { - // When - val result = testedFeature.getDataStoreCurrentVersion() - - // Then - assertThat(result).isEqualTo(CURRENT_DATASTORE_VERSION) - } - - @Test - fun `M call datastore read W readFromDataStore()`( - @StringForgery fakeDataStoreName: String, - @StringForgery fakeFeatureName: String, - @Mock mockDeserializer: Deserializer, - @Mock mockDataStoreHandler: DataStoreHandler - ) { - // Given - whenever(testedFeature.coreFeature.dataStoreHandler) - .thenReturn(mockDataStoreHandler) - - // When - testedFeature.readFromDataStore( - dataStoreFileName = fakeDataStoreName, - featureName = fakeFeatureName, - deserializer = mockDeserializer, - version = CURRENT_DATASTORE_VERSION - ) - - // Then - verify(testedFeature.coreFeature.dataStoreHandler).read( - dataStoreFileName = eq(fakeDataStoreName), - featureName = eq(fakeFeatureName), - deserializer = eq(mockDeserializer), - version = eq(CURRENT_DATASTORE_VERSION) - ) - } - - @Test - fun `M call datastore write W writeToDataStore()`( - @StringForgery fakeDataStoreName: String, - @StringForgery fakeFeatureName: String, - @StringForgery fakeDataString: String, - @Mock mockSerializer: Serializer, - @Mock mockDataStoreHandler: DataStoreHandler - ) { - // Given - whenever(testedFeature.coreFeature.dataStoreHandler) - .thenReturn(mockDataStoreHandler) - - // When - testedFeature.writeToDataStore( - dataStoreFileName = fakeDataStoreName, - featureName = fakeFeatureName, - serializer = mockSerializer, - data = fakeDataString - ) - - // Then - verify(testedFeature.coreFeature.dataStoreHandler).write( - dataStoreFileName = eq(fakeDataStoreName), - featureName = eq(fakeFeatureName), - serializer = eq(mockSerializer), - data = eq(fakeDataString) - ) - } - @Test fun `M notify no receiver W sendEvent(event)`() { // Given From 4b2c2e425c9fc2ab497c4660f871aadabc8c9052 Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Sun, 26 May 2024 10:14:39 +0300 Subject: [PATCH 04/14] RUM-4098: Remove redundant sdkInstanceId from datastore path --- .../internal/persistence/datastore/DataStoreFileHelper.kt | 3 +-- .../internal/persistence/datastore/FileDataStoreHandler.kt | 6 ++---- .../persistence/file/datastore/FileDataStoreHandlerTest.kt | 7 +------ 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt index dd11add533..d6b04be320 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt @@ -10,13 +10,12 @@ import java.io.File internal class DataStoreFileHelper { internal fun getDataStoreDirectory( - sdkInstanceId: String, storageDir: File, featureName: String, folderName: String ): File = File( storageDir, - "$sdkInstanceId/$featureName/$folderName" + "$featureName/$folderName" ) internal fun getDataStoreFile( diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/FileDataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/FileDataStoreHandler.kt index 8e5f91221b..5a4e2dfabb 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/FileDataStoreHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/FileDataStoreHandler.kt @@ -10,6 +10,7 @@ import android.text.format.DateUtils import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.internal.persistence.datastore.DataStoreHandler.Companion.CURRENT_DATASTORE_VERSION import com.datadog.android.core.internal.persistence.datastore.ext.toByteArray import com.datadog.android.core.internal.persistence.datastore.ext.toInt import com.datadog.android.core.internal.persistence.datastore.ext.toLong @@ -27,7 +28,6 @@ import java.util.Locale @Suppress("TooManyFunctions") internal class FileDataStoreHandler( - private val sdkInstanceId: String, private val storageDir: File, private val internalLogger: InternalLogger, private val fileReaderWriter: FileReaderWriter, @@ -64,7 +64,6 @@ internal class FileDataStoreHandler( version: Int ): T? { val dataStoreDirectory = dataStoreFileHelper.getDataStoreDirectory( - sdkInstanceId = sdkInstanceId, featureName = featureName, folderName = DATASTORE_FOLDER_NAME.format(Locale.US, version), storageDir = storageDir @@ -243,7 +242,6 @@ internal class FileDataStoreHandler( private fun createDataStoreDirectoryIfNecessary(featureName: String): File { val dataStoreDirectory = dataStoreFileHelper.getDataStoreDirectory( - sdkInstanceId = sdkInstanceId, featureName = featureName, folderName = DATASTORE_FOLDER_NAME.format(Locale.US, CURRENT_DATASTORE_VERSION), storageDir = storageDir @@ -273,7 +271,7 @@ internal class FileDataStoreHandler( } internal companion object { - internal const val DATASTORE_FOLDER_NAME = "datastore_v$%s" + internal const val DATASTORE_FOLDER_NAME = "datastore_v%s" private const val DATASTORE_EXPIRE_TIME = DateUtils.DAY_IN_MILLIS * 30 // 30 days internal const val FAILED_TO_SERIALIZE_DATA_ERROR = diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/FileDataStoreHandlerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/FileDataStoreHandlerTest.kt index b3c1ed4848..27594a732c 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/FileDataStoreHandlerTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/FileDataStoreHandlerTest.kt @@ -8,8 +8,8 @@ package com.datadog.android.core.internal.persistence.file.datastore import com.datadog.android.api.InternalLogger import com.datadog.android.core.internal.persistence.Deserializer -import com.datadog.android.core.internal.persistence.datastore.CURRENT_DATASTORE_VERSION import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper +import com.datadog.android.core.internal.persistence.datastore.DataStoreHandler.Companion.CURRENT_DATASTORE_VERSION import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler.Companion.DATASTORE_FOLDER_NAME import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler.Companion.FAILED_TO_SERIALIZE_DATA_ERROR @@ -82,9 +82,6 @@ internal class FileDataStoreHandlerTest { @Mock lateinit var mockDataStoreFileHelper: DataStoreFileHelper - @StringForgery - lateinit var fakeSdkInstanceId: String - @StringForgery lateinit var fakeDataStoreFileName: String @@ -104,7 +101,6 @@ internal class FileDataStoreHandlerTest { whenever(mockDataStoreDirectory.existsSafe(mockInternalLogger)).thenReturn(true) whenever( mockDataStoreFileHelper.getDataStoreDirectory( - sdkInstanceId = fakeSdkInstanceId, featureName = fakeFeatureName, folderName = DATASTORE_FOLDER_NAME.format(Locale.US, CURRENT_DATASTORE_VERSION), storageDir = mockStorageDir @@ -128,7 +124,6 @@ internal class FileDataStoreHandlerTest { whenever(mockFileTLVBlockReader.all(mockDataStoreFile)).thenReturn(blocksReturned) testedDataStoreHandler = FileDataStoreHandler( - sdkInstanceId = fakeSdkInstanceId, fileReaderWriter = mockFileReaderWriter, internalLogger = mockInternalLogger, storageDir = mockStorageDir, From bf8e7f5324651f1d69815febfb0119b302d42184 Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Sun, 26 May 2024 23:26:01 +0300 Subject: [PATCH 05/14] RUM-4098: Align datastore api with iOS --- dd-sdk-android-core/api/apiSurface | 19 +- .../api/dd-sdk-android-core.api | 53 ++- .../android/api/feature/FeatureScope.kt | 2 +- .../android/core/internal/CoreFeature.kt | 8 +- .../android/core/internal/SdkFeature.kt | 12 +- .../datastore/DataStoreContents.kt | 13 - ...toreHandler.kt => DataStoreFileHandler.kt} | 156 ++++--- .../datastore/NoOpDataStoreHandler.kt | 25 +- ...LVBlockReader.kt => TLVBlockFileReader.kt} | 17 +- .../datastore/DataStoreCallback.kt | 31 ++ .../persistence/datastore/DataStoreContent.kt | 21 + .../persistence/datastore/DataStoreHandler.kt | 46 ++- .../android/core/internal/SdkFeatureTest.kt | 3 +- ...lerTest.kt => DataStoreFileHandlerTest.kt} | 388 +++++++++--------- ...eaderTest.kt => TLVBlockFileReaderTest.kt} | 18 +- detekt_custom.yml | 1 + 16 files changed, 485 insertions(+), 328 deletions(-) delete mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreContents.kt rename dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/{FileDataStoreHandler.kt => DataStoreFileHandler.kt} (70%) rename dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/{FileTLVBlockReader.kt => TLVBlockFileReader.kt} (91%) create mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreCallback.kt create mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreContent.kt rename dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/{internal => }/persistence/datastore/DataStoreHandler.kt (52%) rename dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/{FileDataStoreHandlerTest.kt => DataStoreFileHandlerTest.kt} (51%) rename dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/{FileTLVBlockReaderTest.kt => TLVBlockFileReaderTest.kt} (88%) diff --git a/dd-sdk-android-core/api/apiSurface b/dd-sdk-android-core/api/apiSurface index ce18e1f827..8f45de8089 100644 --- a/dd-sdk-android-core/api/apiSurface +++ b/dd-sdk-android-core/api/apiSurface @@ -102,7 +102,7 @@ interface com.datadog.android.api.feature.FeatureContextUpdateReceiver interface com.datadog.android.api.feature.FeatureEventReceiver fun onReceive(Any) interface com.datadog.android.api.feature.FeatureScope - var dataStore: com.datadog.android.core.internal.persistence.datastore.DataStoreHandler + var dataStore: com.datadog.android.core.persistence.datastore.DataStoreHandler fun withWriteContext(Boolean = false, (com.datadog.android.api.context.DatadogContext, com.datadog.android.api.storage.EventBatchWriter) -> Unit) fun sendEvent(Any) fun unwrap(): T @@ -247,11 +247,6 @@ interface com.datadog.android.core.internal.net.FirstPartyHostHeaderTypeResolver fun isEmpty(): Boolean interface com.datadog.android.core.internal.persistence.Deserializer fun deserialize(P): R? -interface com.datadog.android.core.internal.persistence.datastore.DataStoreHandler - fun write(String, String, com.datadog.android.core.persistence.Serializer, T) - fun read(String, String, com.datadog.android.core.internal.persistence.Deserializer, Int): T? - companion object - const val CURRENT_DATASTORE_VERSION: Int fun java.io.File.canReadSafe(com.datadog.android.api.InternalLogger): Boolean fun java.io.File.existsSafe(com.datadog.android.api.InternalLogger): Boolean fun java.io.File.readTextSafe(java.nio.charset.Charset = Charsets.UTF_8, com.datadog.android.api.InternalLogger): String? @@ -296,6 +291,18 @@ interface com.datadog.android.core.persistence.Serializer fun serialize(T): String? companion object fun Serializer.serializeToByteArray(T, com.datadog.android.api.InternalLogger): ByteArray? +interface com.datadog.android.core.persistence.datastore.DataStoreCallback + fun onSuccess(DataStoreContent) + fun onFailure() + fun onNoData() +data class com.datadog.android.core.persistence.datastore.DataStoreContent + constructor(Long, Int, T?) +interface com.datadog.android.core.persistence.datastore.DataStoreHandler + fun setValue(String, T, com.datadog.android.core.persistence.Serializer, Int = 0) + fun value(String, com.datadog.android.core.internal.persistence.Deserializer, Int = 0, DataStoreCallback) + fun removeValue(String) + companion object + const val CURRENT_DATASTORE_VERSION: Int class com.datadog.android.core.sampling.RateBasedSampler : Sampler constructor(() -> Float) constructor(Float) diff --git a/dd-sdk-android-core/api/dd-sdk-android-core.api b/dd-sdk-android-core/api/dd-sdk-android-core.api index 4ae043b7e0..a6d2edc7ec 100644 --- a/dd-sdk-android-core/api/dd-sdk-android-core.api +++ b/dd-sdk-android-core/api/dd-sdk-android-core.api @@ -325,9 +325,9 @@ public abstract interface class com/datadog/android/api/feature/FeatureEventRece } public abstract interface class com/datadog/android/api/feature/FeatureScope { - public abstract fun getDataStore ()Lcom/datadog/android/core/internal/persistence/datastore/DataStoreHandler; + public abstract fun getDataStore ()Lcom/datadog/android/core/persistence/datastore/DataStoreHandler; public abstract fun sendEvent (Ljava/lang/Object;)V - public abstract fun setDataStore (Lcom/datadog/android/core/internal/persistence/datastore/DataStoreHandler;)V + public abstract fun setDataStore (Lcom/datadog/android/core/persistence/datastore/DataStoreHandler;)V public abstract fun unwrap ()Lcom/datadog/android/api/feature/Feature; public abstract fun withWriteContext (ZLkotlin/jvm/functions/Function2;)V } @@ -679,17 +679,6 @@ public abstract interface class com/datadog/android/core/internal/persistence/De public abstract fun deserialize (Ljava/lang/Object;)Ljava/lang/Object; } -public abstract interface class com/datadog/android/core/internal/persistence/datastore/DataStoreHandler { - public static final field CURRENT_DATASTORE_VERSION I - public static final field Companion Lcom/datadog/android/core/internal/persistence/datastore/DataStoreHandler$Companion; - public abstract fun read (Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Deserializer;I)Ljava/lang/Object; - public abstract fun write (Ljava/lang/String;Ljava/lang/String;Lcom/datadog/android/core/persistence/Serializer;Ljava/lang/Object;)V -} - -public final class com/datadog/android/core/internal/persistence/datastore/DataStoreHandler$Companion { - public static final field CURRENT_DATASTORE_VERSION I -} - public final class com/datadog/android/core/internal/persistence/file/FileExtKt { public static final fun canReadSafe (Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Z public static final fun existsSafe (Ljava/io/File;Lcom/datadog/android/api/InternalLogger;)Z @@ -802,6 +791,44 @@ public final class com/datadog/android/core/persistence/SerializerKt { public static final fun serializeToByteArray (Lcom/datadog/android/core/persistence/Serializer;Ljava/lang/Object;Lcom/datadog/android/api/InternalLogger;)[B } +public abstract interface class com/datadog/android/core/persistence/datastore/DataStoreCallback { + public abstract fun onFailure ()V + public abstract fun onNoData ()V + public abstract fun onSuccess (Lcom/datadog/android/core/persistence/datastore/DataStoreContent;)V +} + +public final class com/datadog/android/core/persistence/datastore/DataStoreContent { + public fun (JILjava/lang/Object;)V + public final fun component1 ()J + public final fun component2 ()I + public final fun component3 ()Ljava/lang/Object; + public final fun copy (JILjava/lang/Object;)Lcom/datadog/android/core/persistence/datastore/DataStoreContent; + public static synthetic fun copy$default (Lcom/datadog/android/core/persistence/datastore/DataStoreContent;JILjava/lang/Object;ILjava/lang/Object;)Lcom/datadog/android/core/persistence/datastore/DataStoreContent; + public fun equals (Ljava/lang/Object;)Z + public final fun getData ()Ljava/lang/Object; + public final fun getLastUpdateDate ()J + public final fun getVersionCode ()I + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class com/datadog/android/core/persistence/datastore/DataStoreHandler { + public static final field CURRENT_DATASTORE_VERSION I + public static final field Companion Lcom/datadog/android/core/persistence/datastore/DataStoreHandler$Companion; + public abstract fun removeValue (Ljava/lang/String;)V + public abstract fun setValue (Ljava/lang/String;Ljava/lang/Object;Lcom/datadog/android/core/persistence/Serializer;I)V + public abstract fun value (Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Deserializer;ILcom/datadog/android/core/persistence/datastore/DataStoreCallback;)V +} + +public final class com/datadog/android/core/persistence/datastore/DataStoreHandler$Companion { + public static final field CURRENT_DATASTORE_VERSION I +} + +public final class com/datadog/android/core/persistence/datastore/DataStoreHandler$DefaultImpls { + public static synthetic fun setValue$default (Lcom/datadog/android/core/persistence/datastore/DataStoreHandler;Ljava/lang/String;Ljava/lang/Object;Lcom/datadog/android/core/persistence/Serializer;IILjava/lang/Object;)V + public static synthetic fun value$default (Lcom/datadog/android/core/persistence/datastore/DataStoreHandler;Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Deserializer;ILcom/datadog/android/core/persistence/datastore/DataStoreCallback;ILjava/lang/Object;)V +} + public final class com/datadog/android/core/sampling/RateBasedSampler : com/datadog/android/core/sampling/Sampler { public static final field SAMPLE_ALL_RATE F public fun (D)V diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt index 40d32d4166..295ffd1d4b 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt @@ -9,7 +9,7 @@ package com.datadog.android.api.feature import androidx.annotation.AnyThread import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.storage.EventBatchWriter -import com.datadog.android.core.internal.persistence.datastore.DataStoreHandler +import com.datadog.android.core.persistence.datastore.DataStoreHandler /** * Represents a Datadog feature. diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt index a8b89055bd..b38ecf5bee 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt @@ -35,7 +35,6 @@ import com.datadog.android.core.internal.net.info.NetworkInfoDeserializer import com.datadog.android.core.internal.net.info.NetworkInfoProvider import com.datadog.android.core.internal.net.info.NoOpNetworkInfoProvider import com.datadog.android.core.internal.persistence.JsonObjectDeserializer -import com.datadog.android.core.internal.persistence.datastore.DataStoreHandler import com.datadog.android.core.internal.persistence.datastore.NoOpDataStoreHandler import com.datadog.android.core.internal.persistence.file.FileMover import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig @@ -75,6 +74,7 @@ import com.datadog.android.core.internal.user.UserInfoDeserializer import com.datadog.android.core.internal.utils.submitSafe import com.datadog.android.core.internal.utils.unboundInternalLogger import com.datadog.android.core.persistence.PersistenceStrategy +import com.datadog.android.core.persistence.datastore.DataStoreHandler import com.datadog.android.core.thread.FlushableExecutorService import com.datadog.android.ndk.internal.DatadogNdkCrashHandler import com.datadog.android.ndk.internal.NdkCrashHandler @@ -198,7 +198,7 @@ internal class CoreFeature( if (initialized.get()) { return } - + readConfigurationSettings(configuration.coreConfig) readApplicationInformation(appContext, configuration) resolveProcessInfo(appContext) @@ -710,8 +710,8 @@ internal class CoreFeature( internal const val BUILD_ID_FILE_NAME = "datadog.buildId" internal const val BUILD_ID_IS_MISSING_INFO_MESSAGE = "Build ID is not found in the application" + - " assets. If you are using obfuscation, please use Datadog Gradle Plugin 1.13.0" + - " or above to be able to de-obfuscate stacktraces." + " assets. If you are using obfuscation, please use Datadog Gradle Plugin 1.13.0" + + " or above to be able to de-obfuscate stacktraces." internal const val BUILD_ID_READ_ERROR = "Failed to read Build ID information, de-obfuscation may not work properly." diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt index 47236101f3..96f97a035a 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt @@ -36,9 +36,8 @@ import com.datadog.android.core.internal.persistence.AbstractStorage import com.datadog.android.core.internal.persistence.ConsentAwareStorage import com.datadog.android.core.internal.persistence.NoOpStorage import com.datadog.android.core.internal.persistence.Storage +import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper -import com.datadog.android.core.internal.persistence.datastore.DataStoreHandler -import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler import com.datadog.android.core.internal.persistence.datastore.NoOpDataStoreHandler import com.datadog.android.core.internal.persistence.file.FileMover import com.datadog.android.core.internal.persistence.file.FileOrchestrator @@ -47,8 +46,9 @@ import com.datadog.android.core.internal.persistence.file.FileReaderWriter import com.datadog.android.core.internal.persistence.file.NoOpFileOrchestrator import com.datadog.android.core.internal.persistence.file.advanced.FeatureFileOrchestrator import com.datadog.android.core.internal.persistence.file.batch.BatchFileReaderWriter -import com.datadog.android.core.internal.persistence.tlvformat.FileTLVBlockReader +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader import com.datadog.android.core.persistence.PersistenceStrategy +import com.datadog.android.core.persistence.datastore.DataStoreHandler import com.datadog.android.privacy.TrackingConsentProviderCallback import com.datadog.android.security.Encryption import java.util.Collections @@ -364,11 +364,13 @@ internal class SdkFeature( encryption ) - dataStore = FileDataStoreHandler( + dataStore = DataStoreFileHandler( + executorService = coreFeature.persistenceExecutorService, storageDir = coreFeature.storageDir, + featureName = wrappedFeature.name, internalLogger = internalLogger, fileReaderWriter = fileReaderWriter, - fileTLVBlockReader = FileTLVBlockReader( + tlvBlockFileReader = TLVBlockFileReader( internalLogger = internalLogger, fileReaderWriter = fileReaderWriter ), diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreContents.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreContents.kt deleted file mode 100644 index 20c042ba6a..0000000000 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreContents.kt +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.persistence.datastore - -internal data class DataStoreContents( - val lastUpdateDate: Long, - val versionCode: Int, - val data: T? -) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/FileDataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt similarity index 70% rename from dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/FileDataStoreHandler.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt index 5a4e2dfabb..2089bb16f8 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/FileDataStoreHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt @@ -10,7 +10,6 @@ import android.text.format.DateUtils import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger import com.datadog.android.core.internal.persistence.Deserializer -import com.datadog.android.core.internal.persistence.datastore.DataStoreHandler.Companion.CURRENT_DATASTORE_VERSION import com.datadog.android.core.internal.persistence.datastore.ext.toByteArray import com.datadog.android.core.internal.persistence.datastore.ext.toInt import com.datadog.android.core.internal.persistence.datastore.ext.toLong @@ -19,69 +18,125 @@ import com.datadog.android.core.internal.persistence.file.createNewFileSafe import com.datadog.android.core.internal.persistence.file.deleteSafe import com.datadog.android.core.internal.persistence.file.existsSafe import com.datadog.android.core.internal.persistence.file.mkdirsSafe -import com.datadog.android.core.internal.persistence.tlvformat.FileTLVBlockReader import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockType +import com.datadog.android.core.internal.utils.join +import com.datadog.android.core.internal.utils.submitSafe import com.datadog.android.core.persistence.Serializer +import com.datadog.android.core.persistence.datastore.DataStoreCallback +import com.datadog.android.core.persistence.datastore.DataStoreContent +import com.datadog.android.core.persistence.datastore.DataStoreHandler +import com.datadog.android.core.persistence.datastore.DataStoreHandler.Companion.CURRENT_DATASTORE_VERSION import java.io.File import java.util.Locale +import java.util.concurrent.ExecutorService @Suppress("TooManyFunctions") -internal class FileDataStoreHandler( +internal class DataStoreFileHandler( + private val executorService: ExecutorService, private val storageDir: File, + private val featureName: String, private val internalLogger: InternalLogger, private val fileReaderWriter: FileReaderWriter, - private val fileTLVBlockReader: FileTLVBlockReader, + private val tlvBlockFileReader: TLVBlockFileReader, private val dataStoreFileHelper: DataStoreFileHelper ) : DataStoreHandler { @WorkerThread - override fun write( - dataStoreFileName: String, - featureName: String, + override fun setValue( + key: String, + data: T, serializer: Serializer, - data: T + version: Int ) { - val dataStoreDirectory = createDataStoreDirectoryIfNecessary(featureName) - val dataStoreFile = createDataStoreFileIfNecessary(dataStoreDirectory, dataStoreFileName) + executorService.submitSafe("datastoreRead", internalLogger) { + writeEntry(key, data, serializer, version) + } + } - val lastUpdateBlock = getLastUpdateDateBlock() - val versionCodeBlock = getVersionCodeBlock() - val dataBlock = getDataBlock(data, serializer) + @WorkerThread + override fun value( + key: String, + deserializer: Deserializer, + version: Int, + callback: DataStoreCallback + ) { + executorService.submitSafe("readEntry", internalLogger) { + readEntry(key, deserializer, version, callback) + } + } - if (lastUpdateBlock == null || versionCodeBlock == null || dataBlock == null) return + @WorkerThread + override fun removeValue(key: String) { + executorService.submitSafe("readEntry", internalLogger) { + deleteFromDataStore(key) + } + } - writeToFile( - dataStoreFile, - lastUpdateBlock + versionCodeBlock + dataBlock + private fun deleteFromDataStore(key: String) { + val dataStoreDirectory = dataStoreFileHelper.getDataStoreDirectory( + featureName = featureName, + folderName = DATASTORE_FOLDER_NAME.format(Locale.US, CURRENT_DATASTORE_VERSION), + storageDir = storageDir ) + + val datastoreFile = dataStoreFileHelper.getDataStoreFile( + dataStoreDirectory = dataStoreDirectory, + dataStoreFileName = key + ) + + datastoreFile.deleteSafe(internalLogger) } - @WorkerThread - override fun read( - dataStoreFileName: String, - featureName: String, + private fun readEntry( + key: String, deserializer: Deserializer, - version: Int - ): T? { + version: Int, + callback: DataStoreCallback + ) { val dataStoreDirectory = dataStoreFileHelper.getDataStoreDirectory( featureName = featureName, - folderName = DATASTORE_FOLDER_NAME.format(Locale.US, version), + folderName = DATASTORE_FOLDER_NAME.format(Locale.US, CURRENT_DATASTORE_VERSION), storageDir = storageDir ) val datastoreFile = dataStoreFileHelper.getDataStoreFile( dataStoreDirectory = dataStoreDirectory, - dataStoreFileName = dataStoreFileName + dataStoreFileName = key ) if (!datastoreFile.existsSafe(internalLogger)) { - return null // no datastore file found + callback.onNoData() + return } - return readFromDataStoreFile(datastoreFile, deserializer, fileTLVBlockReader, version) + readFromDataStoreFile(datastoreFile, deserializer, tlvBlockFileReader, version, callback) + } + + private fun writeEntry( + key: String, + data: T, + serializer: Serializer, + version: Int + ) { + val dataStoreDirectory = createDataStoreDirectoryIfNecessary(featureName) + val dataStoreFile = createDataStoreFileIfNecessary(dataStoreDirectory, key) + + val lastUpdateBlock = getLastUpdateDateBlock() + val versionCodeBlock = getVersionCodeBlock(version) + val dataBlock = getDataBlock(data, serializer) + + if (lastUpdateBlock == null || versionCodeBlock == null || dataBlock == null) return + + writeToFile( + dataStoreFile, + listOf(lastUpdateBlock, versionCodeBlock, dataBlock).join( + separator = byteArrayOf(), + internalLogger = internalLogger + ) + ) } - @WorkerThread private fun writeToFile(dataStoreFile: File, data: ByteArray) { fileReaderWriter.writeData( file = dataStoreFile, @@ -90,7 +145,6 @@ internal class FileDataStoreHandler( ) } - @WorkerThread private fun getDataBlock( data: T, serializer: Serializer @@ -110,7 +164,6 @@ internal class FileDataStoreHandler( return dataBlock.serialize() } - @WorkerThread private fun getLastUpdateDateBlock(): ByteArray? { val now = System.currentTimeMillis() val lastUpdateDateByteArray = now.toByteArray() @@ -122,9 +175,8 @@ internal class FileDataStoreHandler( return lastUpdateDateBlock.serialize() } - @WorkerThread - private fun getVersionCodeBlock(): ByteArray? { - val versionCodeByteArray = CURRENT_DATASTORE_VERSION.toByteArray() + private fun getVersionCodeBlock(version: Int): ByteArray? { + val versionCodeByteArray = version.toByteArray() val versionBlock = TLVBlock( type = TLVBlockType.VERSION_CODE, data = versionCodeByteArray @@ -133,47 +185,49 @@ internal class FileDataStoreHandler( return versionBlock.serialize() } - @WorkerThread @Suppress("ReturnCount") private fun readFromDataStoreFile( datastoreFile: File, deserializer: Deserializer, - fileTLVBlockReader: FileTLVBlockReader, - requestedVersion: Int - ): T? { - val tlvBlocks = fileTLVBlockReader.all(datastoreFile) + tlvBlockFileReader: TLVBlockFileReader, + requestedVersion: Int? = 0, + callback: DataStoreCallback + ) { + val tlvBlocks = tlvBlockFileReader.read(datastoreFile) // there should be as many blocks read as there are block types if (tlvBlocks.size != TLVBlockType.values().size) { logInvalidNumberOfBlocksError(tlvBlocks.size) - return null + callback.onFailure() + return } - val dataStoreContents = tryToMapToDataStoreContents(deserializer, tlvBlocks) - ?: return null + val dataStoreContent = tryToMapToDataStoreContents(deserializer, tlvBlocks) + + if (dataStoreContent == null) { + callback.onFailure() + return + } - val fileVersionIsWrong = dataStoreContents.versionCode != requestedVersion - val fileIsTooOld = isDataStoreTooOld(dataStoreContents.lastUpdateDate) + val fileVersionIsWrong = dataStoreContent.versionCode != requestedVersion + val fileIsTooOld = isDataStoreTooOld(dataStoreContent.lastUpdateDate) - // the version check is a double redundancy. Since we store each file in a folder - // whose name contains the version it should be impossible to read a file - // that contains the wrong version. if (fileVersionIsWrong) { logInvalidVersionError() } - return if (fileIsTooOld || fileVersionIsWrong) { + if (fileIsTooOld || fileVersionIsWrong) { datastoreFile.deleteSafe(internalLogger) - null + callback.onFailure() } else { - dataStoreContents.data + callback.onSuccess(dataStoreContent) } } private fun tryToMapToDataStoreContents( deserializer: Deserializer, tlvBlocks: List - ): DataStoreContents? { + ): DataStoreContent? { // map the blocks to the actual types val typesToBlocks = mutableMapOf() for (block in tlvBlocks) { @@ -195,7 +249,7 @@ internal class FileDataStoreHandler( return if (lastUpdateBlock == null || versionCodeBlock == null || dataBlock == null) { null // this should never happen as we know by this stage that these cannot be null } else { - DataStoreContents( + DataStoreContent( lastUpdateDate = lastUpdateBlock.data.toLong(), versionCode = versionCodeBlock.data.toInt(), data = deserializer.deserialize(String(dataBlock.data)) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt index ab694ad844..f23ff676c7 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt @@ -8,24 +8,29 @@ package com.datadog.android.core.internal.persistence.datastore import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.persistence.Serializer +import com.datadog.android.core.persistence.datastore.DataStoreCallback +import com.datadog.android.core.persistence.datastore.DataStoreHandler internal class NoOpDataStoreHandler : DataStoreHandler { - override fun write( - dataStoreFileName: String, - featureName: String, + override fun setValue( + key: String, + data: T, serializer: Serializer, - data: T + version: Int ) { // NoOp Implementation } - override fun read( - dataStoreFileName: String, - featureName: String, + override fun value( + key: String, deserializer: Deserializer, - version: Int - ): T? { + version: Int, + callback: DataStoreCallback + ) { + // NoOp Implementation + } + + override fun removeValue(key: String) { // NoOp Implementation - return null } } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/FileTLVBlockReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReader.kt similarity index 91% rename from dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/FileTLVBlockReader.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReader.kt index 932f3c16b0..bf28326901 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/FileTLVBlockReader.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReader.kt @@ -17,16 +17,13 @@ import java.io.File import java.io.IOException import java.io.InputStream import java.util.Locale -import java.util.concurrent.atomic.AtomicBoolean -internal class FileTLVBlockReader( +internal class TLVBlockFileReader( val internalLogger: InternalLogger, val fileReaderWriter: FileReaderWriter ) { - private var endOfStream = AtomicBoolean(false) - @WorkerThread - internal fun all(file: File): List { + internal fun read(file: File): List { if (!file.existsSafe(internalLogger) || file.lengthSafe(internalLogger) == 0L) { return arrayListOf() } @@ -34,10 +31,11 @@ internal class FileTLVBlockReader( val blocks = mutableListOf() val stream = fileReaderWriter.readData(file).inputStream() - while (!endOfStream.get()) { - val nextBlock = readBlock(stream) ?: break - blocks.add(nextBlock) - } + var nextBlock: TLVBlock? + do { + nextBlock = readBlock(stream) + if (nextBlock != null) blocks.add(nextBlock) + } while (nextBlock != null) return blocks } @@ -80,7 +78,6 @@ internal class FileTLVBlockReader( val buffer = ByteArray(length) val status = safeReadFromStream(stream, buffer) return if (status == -1) { // stream is finished (or error) - endOfStream.set(true) ByteArray(0) } else { buffer diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreCallback.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreCallback.kt new file mode 100644 index 0000000000..39fcc6bbfb --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreCallback.kt @@ -0,0 +1,31 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.persistence.datastore + +/** + * Callback for asynchronous operations on the datastore. + */ +interface DataStoreCallback { + + /** + * Called on successfully fetching data from the datastore. + * + * @param T datatype returned by the datastore. + * @param dataStoreContent contains the datastore data, version and lastUpdateDate. + */ + fun onSuccess(dataStoreContent: DataStoreContent) + + /** + * Called when an exception occurred getting data from the datastore. + */ + fun onFailure() + + /** + * Called when no data is found for the given key. + */ + fun onNoData() +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreContent.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreContent.kt new file mode 100644 index 0000000000..25945bc0cf --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreContent.kt @@ -0,0 +1,21 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.persistence.datastore + +/** + * Datastore entry contents and metadata. + * + * @param T type of data used by this entry in the datastore. + * @property lastUpdateDate date when the entry was written. + * @property versionCode version used by the entry. + * @property data content of the entry. + */ +data class DataStoreContent( + val lastUpdateDate: Long, + val versionCode: Int, + val data: T? +) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreHandler.kt similarity index 52% rename from dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreHandler.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreHandler.kt index cc817cff80..2821bb7c31 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreHandler.kt @@ -4,44 +4,58 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.core.internal.persistence.datastore +package com.datadog.android.core.persistence.datastore import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.persistence.Serializer +/** + * Interface for the datastore. + */ interface DataStoreHandler { /** * Write data to the datastore. * * @param T datatype of the data to write to the datastore. - * @param dataStoreFileName name of the datastore file as there could be multiple such files per feature. - * @param featureName of the calling feature, to determine the path to the datastore file. - * @param serializer to use to serialize the data. + * @param key name of the datastore entry. * @param data to write. + * @param serializer to use to serialize the data. + * @param version optional version for the entry. + * If not specified will give the entry version 0 - even if that would be a downgrade from the previous version. */ - fun write( - dataStoreFileName: String, - featureName: String, + fun setValue( + key: String, + data: T, serializer: Serializer, - data: T + version: Int = 0 ) /** * Read data from the datastore. * * @param T datatype of the data to read from the datastore. - * @param dataStoreFileName name of the datastore file as there could be multiple such files per feature. - * @param featureName of the calling feature, to determine the path to the datastore file. + * @param key name of the datastore entry. * @param deserializer to use to deserialize the data. - * @param version to use when reading from the datastore (to support migrations). + * @param version optional version to use when reading from the datastore. + * If specified, will only return data if it exactly matches this version number. + * @param callback to return result asynchronously. */ - fun read( - dataStoreFileName: String, - featureName: String, + fun value( + key: String, deserializer: Deserializer, - version: Int - ): T? + version: Int = 0, + callback: DataStoreCallback + ) + + /** + * Remove an entry from the datastore. + * + * @param key name of the datastore entry + */ + fun removeValue( + key: String + ) companion object { /** diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt index bfe447e40a..f114cf5d3b 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt @@ -33,12 +33,12 @@ import com.datadog.android.core.internal.persistence.AbstractStorage import com.datadog.android.core.internal.persistence.ConsentAwareStorage import com.datadog.android.core.internal.persistence.NoOpStorage import com.datadog.android.core.internal.persistence.Storage -import com.datadog.android.core.internal.persistence.datastore.DataStoreHandler import com.datadog.android.core.internal.persistence.datastore.NoOpDataStoreHandler import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig import com.datadog.android.core.internal.persistence.file.NoOpFileOrchestrator import com.datadog.android.core.internal.persistence.file.batch.BatchFileOrchestrator import com.datadog.android.core.persistence.PersistenceStrategy +import com.datadog.android.core.persistence.datastore.DataStoreHandler import com.datadog.android.privacy.TrackingConsent import com.datadog.android.privacy.TrackingConsentProviderCallback import com.datadog.android.utils.config.ApplicationContextTestConfiguration @@ -246,6 +246,7 @@ internal class SdkFeatureTest { fun `M register tracking consent callback W initialize(){feature+TrackingConsentProviderCallback}`() { // Given val mockFeature = mock() + whenever(mockFeature.name).thenReturn(fakeFeatureName) testedFeature = SdkFeature( coreFeature.mockInstance, mockFeature, diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/FileDataStoreHandlerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt similarity index 51% rename from dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/FileDataStoreHandlerTest.kt rename to dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt index 27594a732c..b113820039 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/FileDataStoreHandlerTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt @@ -8,23 +8,24 @@ package com.datadog.android.core.internal.persistence.file.datastore import com.datadog.android.api.InternalLogger import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler +import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler.Companion.DATASTORE_FOLDER_NAME +import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler.Companion.FAILED_TO_SERIALIZE_DATA_ERROR +import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler.Companion.INVALID_NUMBER_OF_BLOCKS_ERROR +import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler.Companion.INVALID_VERSION_ERROR +import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler.Companion.SAME_BLOCK_APPEARS_TWICE_ERROR import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper -import com.datadog.android.core.internal.persistence.datastore.DataStoreHandler.Companion.CURRENT_DATASTORE_VERSION -import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler -import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler.Companion.DATASTORE_FOLDER_NAME -import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler.Companion.FAILED_TO_SERIALIZE_DATA_ERROR -import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler.Companion.INVALID_NUMBER_OF_BLOCKS_ERROR -import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler.Companion.INVALID_VERSION_ERROR -import com.datadog.android.core.internal.persistence.datastore.FileDataStoreHandler.Companion.SAME_BLOCK_APPEARS_TWICE_ERROR import com.datadog.android.core.internal.persistence.file.FileReaderWriter import com.datadog.android.core.internal.persistence.file.createNewFileSafe -import com.datadog.android.core.internal.persistence.file.deleteSafe import com.datadog.android.core.internal.persistence.file.existsSafe import com.datadog.android.core.internal.persistence.file.mkdirsSafe -import com.datadog.android.core.internal.persistence.tlvformat.FileTLVBlockReader import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockType import com.datadog.android.core.persistence.Serializer +import com.datadog.android.core.persistence.datastore.DataStoreCallback +import com.datadog.android.core.persistence.datastore.DataStoreContent +import com.datadog.android.core.persistence.datastore.DataStoreHandler.Companion.CURRENT_DATASTORE_VERSION import com.datadog.android.utils.forge.Configurator import com.datadog.android.utils.verifyLog import fr.xgouchet.elmyr.annotation.StringForgery @@ -35,9 +36,12 @@ import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.doAnswer import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever @@ -45,6 +49,9 @@ import org.mockito.quality.Strictness import java.io.File import java.nio.ByteBuffer import java.util.Locale +import java.util.concurrent.ExecutorService +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit @Extensions( ExtendWith(MockitoExtension::class), @@ -52,8 +59,9 @@ import java.util.Locale ) @MockitoSettings(strictness = Strictness.LENIENT) @ForgeConfiguration(Configurator::class) -internal class FileDataStoreHandlerTest { - private lateinit var testedDataStoreHandler: FileDataStoreHandler +internal class DataStoreFileHandlerTest { + + private lateinit var testedDataStoreHandler: DataStoreFileHandler @Mock lateinit var mockFileReaderWriter: FileReaderWriter @@ -61,9 +69,6 @@ internal class FileDataStoreHandlerTest { @Mock lateinit var mockInternalLogger: InternalLogger - @Mock - lateinit var mockStorageDir: File - @Mock lateinit var mockDataStoreDirectory: File @@ -77,13 +82,19 @@ internal class FileDataStoreHandlerTest { lateinit var mockDeserializer: Deserializer @Mock - lateinit var mockFileTLVBlockReader: FileTLVBlockReader + lateinit var mockTLVBlockFileReader: TLVBlockFileReader @Mock lateinit var mockDataStoreFileHelper: DataStoreFileHelper + @Mock + lateinit var mockExecutorService: ExecutorService + + @TempDir + lateinit var mockStorageDir: File + @StringForgery - lateinit var fakeDataStoreFileName: String + lateinit var fakeKey: String @StringForgery lateinit var fakeDataString: String @@ -98,7 +109,11 @@ internal class FileDataStoreHandlerTest { fun setup() { fakeDataBytes = fakeDataString.toByteArray(Charsets.UTF_8) - whenever(mockDataStoreDirectory.existsSafe(mockInternalLogger)).thenReturn(true) + whenever(mockExecutorService.submit(any())) doAnswer { + it.getArgument(0).run() + StubFuture() + } + whenever( mockDataStoreFileHelper.getDataStoreDirectory( featureName = fakeFeatureName, @@ -106,28 +121,32 @@ internal class FileDataStoreHandlerTest { storageDir = mockStorageDir ) ).thenReturn(mockDataStoreDirectory) - whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(true) + whenever( mockDataStoreFileHelper.getDataStoreFile( dataStoreDirectory = mockDataStoreDirectory, - dataStoreFileName = fakeDataStoreFileName + dataStoreFileName = fakeKey ) ).thenReturn(mockDataStoreFile) + whenever(mockDataStoreDirectory.existsSafe(mockInternalLogger)).thenReturn(true) + whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(true) whenever(mockSerializer.serialize(fakeDataString)).thenReturn(fakeDataString) whenever(mockDeserializer.deserialize(fakeDataString)).thenReturn(fakeDataBytes) val versionBlock = createVersionBlock(true) - val lastUpdateDateBlock = createLastUpdateDateBlock(true) + val lastUpdateDateBlock = createLastUpdateDateBlock() val dataBlock = createDataBlock() blocksReturned = arrayListOf(versionBlock, lastUpdateDateBlock, dataBlock) - whenever(mockFileTLVBlockReader.all(mockDataStoreFile)).thenReturn(blocksReturned) + whenever(mockTLVBlockFileReader.read(mockDataStoreFile)).thenReturn(blocksReturned) - testedDataStoreHandler = FileDataStoreHandler( + testedDataStoreHandler = DataStoreFileHandler( + executorService = mockExecutorService, fileReaderWriter = mockFileReaderWriter, + featureName = fakeFeatureName, internalLogger = mockInternalLogger, storageDir = mockStorageDir, - fileTLVBlockReader = mockFileTLVBlockReader, + tlvBlockFileReader = mockTLVBlockFileReader, dataStoreFileHelper = mockDataStoreFileHelper ) } @@ -138,34 +157,64 @@ internal class FileDataStoreHandlerTest { fun `M return null W read() { datastore file does not exist }`() { // Given whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(false) + var gotNoData = false // When - val result = testedDataStoreHandler.read( - dataStoreFileName = fakeDataStoreFileName, + testedDataStoreHandler.value( + key = fakeKey, deserializer = mockDeserializer, - featureName = fakeFeatureName, - version = CURRENT_DATASTORE_VERSION + version = CURRENT_DATASTORE_VERSION, + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) { + // should not get here + assertThat(1).isEqualTo(2) + } + + override fun onFailure() { + // should not get here + assertThat(1).isEqualTo(2) + } + + override fun onNoData() { + gotNoData = true + } + } ) // Then - assertThat(result).isNull() + assertThat(gotNoData).isTrue() } @Test fun `M return null W read() { invalid number of blocks }`() { // Given blocksReturned.removeLast() + var gotFailure = false // When - val result = testedDataStoreHandler.read( - dataStoreFileName = fakeDataStoreFileName, + testedDataStoreHandler.value( + key = fakeKey, deserializer = mockDeserializer, - featureName = fakeFeatureName, - version = CURRENT_DATASTORE_VERSION + version = CURRENT_DATASTORE_VERSION, + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) { + // should not get here + assertThat(1).isEqualTo(2) + } + + override fun onNoData() { + // should not get here + assertThat(1).isEqualTo(2) + } + + override fun onFailure() { + gotFailure = true + } + } ) // Then - assertThat(result).isNull() + assertThat(gotFailure).isTrue() } @Test @@ -176,18 +225,29 @@ internal class FileDataStoreHandlerTest { val expectedError = INVALID_NUMBER_OF_BLOCKS_ERROR.format(Locale.US, blocksReturned.size) // When - testedDataStoreHandler.read( - dataStoreFileName = fakeDataStoreFileName, + testedDataStoreHandler.value( + key = fakeKey, deserializer = mockDeserializer, - featureName = fakeFeatureName, - version = CURRENT_DATASTORE_VERSION - ) - - // Then - mockInternalLogger.verifyLog( - target = InternalLogger.Target.MAINTAINER, - level = InternalLogger.Level.ERROR, - message = expectedError + version = CURRENT_DATASTORE_VERSION, + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) { + // should not get here + assertThat(1).isEqualTo(2) + } + + override fun onFailure() { + mockInternalLogger.verifyLog( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + message = expectedError + ) + } + + override fun onNoData() { + // should not get here + assertThat(1).isEqualTo(2) + } + } ) } @@ -202,18 +262,29 @@ internal class FileDataStoreHandlerTest { val expectedError = SAME_BLOCK_APPEARS_TWICE_ERROR.format(Locale.US, TLVBlockType.VERSION_CODE) // When - testedDataStoreHandler.read( - dataStoreFileName = fakeDataStoreFileName, + testedDataStoreHandler.value( + key = fakeKey, deserializer = mockDeserializer, - featureName = fakeFeatureName, - version = CURRENT_DATASTORE_VERSION - ) - - // Then - mockInternalLogger.verifyLog( - level = InternalLogger.Level.ERROR, - target = InternalLogger.Target.MAINTAINER, - message = expectedError + version = CURRENT_DATASTORE_VERSION, + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) { + // should not get here + assertThat(1).isEqualTo(2) + } + + override fun onFailure() { + mockInternalLogger.verifyLog( + level = InternalLogger.Level.ERROR, + target = InternalLogger.Target.MAINTAINER, + message = expectedError + ) + } + + override fun onNoData() { + // should not get here + assertThat(1).isEqualTo(2) + } + } ) } @@ -221,124 +292,66 @@ internal class FileDataStoreHandlerTest { fun `M log error W read() { version too old }`() { // Given blocksReturned.clear() - blocksReturned.add(createLastUpdateDateBlock(true)) + blocksReturned.add(createLastUpdateDateBlock()) blocksReturned.add(createVersionBlock(false)) blocksReturned.add(createDataBlock()) // When - testedDataStoreHandler.read( - dataStoreFileName = fakeDataStoreFileName, + testedDataStoreHandler.value( + key = fakeKey, deserializer = mockDeserializer, - featureName = fakeFeatureName, - version = CURRENT_DATASTORE_VERSION - ) - - // Then - mockInternalLogger.verifyLog( - level = InternalLogger.Level.ERROR, - target = InternalLogger.Target.MAINTAINER, - message = INVALID_VERSION_ERROR + version = CURRENT_DATASTORE_VERSION, + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) { + // should not get here + assertThat(1).isEqualTo(2) + } + + override fun onFailure() { + mockInternalLogger.verifyLog( + level = InternalLogger.Level.ERROR, + target = InternalLogger.Target.MAINTAINER, + message = INVALID_VERSION_ERROR + ) + } + + override fun onNoData() { + // should not get here + assertThat(1).isEqualTo(2) + } + } ) } - @Test - fun `M delete datastore file W read() { version too old }`() { - // Given - blocksReturned.clear() - blocksReturned.add(createLastUpdateDateBlock(true)) - blocksReturned.add(createVersionBlock(false)) - blocksReturned.add(createDataBlock()) - - // When - testedDataStoreHandler.read( - dataStoreFileName = fakeDataStoreFileName, - deserializer = mockDeserializer, - featureName = fakeFeatureName, - version = CURRENT_DATASTORE_VERSION - ) - - // Then - verify(mockDataStoreFile).deleteSafe(mockInternalLogger) - } - - @Test - fun `M return null W read() { version too old }`() { - // Given - blocksReturned.clear() - blocksReturned.add(createLastUpdateDateBlock(true)) - blocksReturned.add(createVersionBlock(false)) - blocksReturned.add(createDataBlock()) - - // When - val result = testedDataStoreHandler.read( - dataStoreFileName = fakeDataStoreFileName, - deserializer = mockDeserializer, - featureName = fakeFeatureName, - version = CURRENT_DATASTORE_VERSION - ) - - // Then - assertThat(result).isNull() - } - - @Test - fun `M return null W read() { last update over threshold }`() { - // Given - blocksReturned.clear() - blocksReturned.add(createLastUpdateDateBlock(false)) - blocksReturned.add(createVersionBlock(true)) - blocksReturned.add(createDataBlock()) - - // When - val result = testedDataStoreHandler.read( - dataStoreFileName = fakeDataStoreFileName, - deserializer = mockDeserializer, - featureName = fakeFeatureName, - version = CURRENT_DATASTORE_VERSION - ) - - // Then - assertThat(result).isNull() - } - - @Test - fun `M delete datastore file W read() { last update over threshold }`() { - // Given - blocksReturned.clear() - blocksReturned.add(createLastUpdateDateBlock(false)) - blocksReturned.add(createVersionBlock(true)) - blocksReturned.add(createDataBlock()) - - // When - testedDataStoreHandler.read( - dataStoreFileName = fakeDataStoreFileName, - deserializer = mockDeserializer, - featureName = fakeFeatureName, - version = CURRENT_DATASTORE_VERSION - ) - - // Then - verify(mockDataStoreFile).deleteSafe(mockInternalLogger) - } - @Test fun `M return deserialized data W read()`() { // Given blocksReturned.clear() - blocksReturned.add(createLastUpdateDateBlock(true)) + blocksReturned.add(createLastUpdateDateBlock()) blocksReturned.add(createVersionBlock(true)) blocksReturned.add(createDataBlock()) // When - val result = testedDataStoreHandler.read( - dataStoreFileName = fakeDataStoreFileName, + testedDataStoreHandler.value( + key = fakeKey, deserializer = mockDeserializer, - featureName = fakeFeatureName, - version = CURRENT_DATASTORE_VERSION + version = CURRENT_DATASTORE_VERSION, + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) { + assertThat(dataStoreContent.data).isEqualTo(fakeDataBytes) + } + + override fun onFailure() { + // should not get here + assertThat(1).isEqualTo(2) + } + + override fun onNoData() { + // should not get here + assertThat(1).isEqualTo(2) + } + } ) - - // Then - assertThat(result).isEqualTo(fakeDataBytes) } // endregion @@ -351,9 +364,8 @@ internal class FileDataStoreHandlerTest { whenever(mockSerializer.serialize(fakeDataString)).thenReturn(null) // When - testedDataStoreHandler.write( - dataStoreFileName = fakeDataStoreFileName, - featureName = fakeFeatureName, + testedDataStoreHandler.setValue( + key = fakeKey, serializer = mockSerializer, data = fakeDataString ) @@ -368,9 +380,8 @@ internal class FileDataStoreHandlerTest { whenever(mockSerializer.serialize(fakeDataString)).thenReturn(null) // When - testedDataStoreHandler.write( - dataStoreFileName = fakeDataStoreFileName, - featureName = fakeFeatureName, + testedDataStoreHandler.setValue( + key = fakeKey, serializer = mockSerializer, data = fakeDataString ) @@ -389,9 +400,8 @@ internal class FileDataStoreHandlerTest { whenever(mockDataStoreDirectory.existsSafe(mockInternalLogger)).thenReturn(false) // When - testedDataStoreHandler.write( - dataStoreFileName = fakeDataStoreFileName, - featureName = fakeFeatureName, + testedDataStoreHandler.setValue( + key = fakeKey, serializer = mockSerializer, data = fakeDataString ) @@ -406,9 +416,8 @@ internal class FileDataStoreHandlerTest { whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(false) // When - testedDataStoreHandler.write( - dataStoreFileName = fakeDataStoreFileName, - featureName = fakeFeatureName, + testedDataStoreHandler.setValue( + key = fakeKey, serializer = mockSerializer, data = fakeDataString ) @@ -419,41 +428,42 @@ internal class FileDataStoreHandlerTest { // endregion - private fun createVersionBlock(valid: Boolean): TLVBlock { + private fun createVersionBlock(valid: Boolean, newVersion: Int = 0): TLVBlock { return if (valid) { TLVBlock( type = TLVBlockType.VERSION_CODE, - data = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(CURRENT_DATASTORE_VERSION).array() + data = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(newVersion).array() ) } else { TLVBlock( type = TLVBlockType.VERSION_CODE, - data = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(CURRENT_DATASTORE_VERSION - 1).array() + data = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(newVersion - 1).array() ) } } - private fun createLastUpdateDateBlock(valid: Boolean): TLVBlock { - return if (valid) { - TLVBlock( - type = TLVBlockType.LAST_UPDATE_DATE, - data = ByteBuffer.allocate(Long.SIZE_BYTES) - .putLong(System.currentTimeMillis()) - .array() - ) - } else { - TLVBlock( - type = TLVBlockType.LAST_UPDATE_DATE, - data = ByteBuffer.allocate(Long.SIZE_BYTES) - .putLong(0) - .array() - ) - } - } + private fun createLastUpdateDateBlock(): TLVBlock = + TLVBlock( + type = TLVBlockType.LAST_UPDATE_DATE, + data = ByteBuffer.allocate(Long.SIZE_BYTES) + .putLong(System.currentTimeMillis()) + .array() + ) - private fun createDataBlock(): TLVBlock = + private fun createDataBlock(dataBytes: ByteArray = fakeDataBytes): TLVBlock = TLVBlock( type = TLVBlockType.DATA, - data = fakeDataBytes + data = dataBytes ) + + private class StubFuture : Future { + override fun cancel(mayInterruptIfRunning: Boolean) = + error("Not supposed to be called") + + override fun isCancelled(): Boolean = error("Not supposed to be called") + override fun isDone(): Boolean = error("Not supposed to be called") + override fun get(): Any = error("Not supposed to be called") + override fun get(timeout: Long, unit: TimeUnit?): Any = + error("Not supposed to be called") + } } diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/FileTLVBlockReaderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt similarity index 88% rename from dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/FileTLVBlockReaderTest.kt rename to dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt index aa8ef8c7b4..9a96fccebe 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/FileTLVBlockReaderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt @@ -10,7 +10,7 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.core.internal.persistence.file.FileReaderWriter import com.datadog.android.core.internal.persistence.file.existsSafe import com.datadog.android.core.internal.persistence.file.lengthSafe -import com.datadog.android.core.internal.persistence.tlvformat.FileTLVBlockReader.Companion.CORRUPT_TLV_HEADER_TYPE_ERROR +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader.Companion.CORRUPT_TLV_HEADER_TYPE_ERROR import com.datadog.android.utils.forge.Configurator import com.datadog.android.utils.verifyLog import fr.xgouchet.elmyr.annotation.StringForgery @@ -35,8 +35,8 @@ import java.nio.ByteBuffer ) @MockitoSettings(strictness = Strictness.LENIENT) @ForgeConfiguration(Configurator::class) -internal class FileTLVBlockReaderTest { - private lateinit var testedReader: FileTLVBlockReader +internal class TLVBlockFileReaderTest { + private lateinit var testedReader: TLVBlockFileReader @Mock private lateinit var mockFile: File @@ -70,7 +70,7 @@ internal class FileTLVBlockReaderTest { whenever(mockFile.lengthSafe(mockInternalLogger)).thenReturn(fakeBufferBytes.size.toLong()) whenever(mockFileReaderWriter.readData(mockFile)).thenReturn(fakeBufferBytes) - testedReader = FileTLVBlockReader( + testedReader = TLVBlockFileReader( fileReaderWriter = mockFileReaderWriter, internalLogger = mockInternalLogger ) @@ -82,7 +82,7 @@ internal class FileTLVBlockReaderTest { whenever(mockFile.existsSafe(mockInternalLogger)).thenReturn(false) // When - val readBytes = testedReader.all(file = mockFile) + val readBytes = testedReader.read(file = mockFile) // Then assertThat(readBytes).isEmpty() @@ -94,7 +94,7 @@ internal class FileTLVBlockReaderTest { whenever(mockFile.lengthSafe(mockInternalLogger)).thenReturn(0L) // When - val readBytes = testedReader.all(file = mockFile) + val readBytes = testedReader.read(file = mockFile) // Then assertThat(readBytes).isEmpty() @@ -108,7 +108,7 @@ internal class FileTLVBlockReaderTest { .thenReturn(fakeBufferBytes) // When - val readBytes = testedReader.all(file = mockFile) + val readBytes = testedReader.read(file = mockFile) // Then assertThat(readBytes).isEmpty() @@ -122,7 +122,7 @@ internal class FileTLVBlockReaderTest { .thenReturn(fakeBufferBytes) // When - testedReader.all(file = mockFile) + testedReader.read(file = mockFile) // Then mockInternalLogger.verifyLog( @@ -135,7 +135,7 @@ internal class FileTLVBlockReaderTest { @Test fun `M return valid object W all() { valid TLV format }`() { // When - val tlvArray = testedReader.all(file = mockFile) + val tlvArray = testedReader.read(file = mockFile) val dataObject = tlvArray[0] // Then diff --git a/detekt_custom.yml b/detekt_custom.yml index 604ce5c950..6c893ce8b1 100644 --- a/detekt_custom.yml +++ b/detekt_custom.yml @@ -764,6 +764,7 @@ datadog: - "kotlin.Array.orEmpty()" - "kotlin.Array.sorted()" - "kotlin.Array.toList()" + - "kotlin.byteArrayOf(kotlin.ByteArray)" - "kotlin.ByteArray.any(kotlin.Function1)" - "kotlin.ByteArray.contentEquals(kotlin.ByteArray?)" - "kotlin.ByteArray.contentHashCode()" From f8cd4b3948cf49cd3add940aee7e6aa53cf291d1 Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Tue, 28 May 2024 12:06:14 +0300 Subject: [PATCH 06/14] RUM-4098: Limit max entry size and add tests --- dd-sdk-android-core/api/apiSurface | 4 +- .../api/dd-sdk-android-core.api | 8 +- .../datastore/DataStoreFileHandler.kt | 168 ++++++------------ .../datastore/DataStoreFileHelper.kt | 52 ++++-- .../datastore/NoOpDataStoreHandler.kt | 8 +- .../persistence/datastore/ext/ByteArrayExt.kt | 11 ++ .../persistence/tlvformat/TLVBlock.kt | 29 ++- .../tlvformat/TLVBlockFileReader.kt | 124 +++++++------ .../persistence/datastore/DataStoreHandler.kt | 14 +- .../datastore/DataStoreFileHandlerTest.kt | 116 ++++++------ .../tlvformat/TLVBlockFileReaderTest.kt | 150 +++++++++++----- .../persistence/tlvformat/TLVBlockTest.kt | 46 ++++- 12 files changed, 430 insertions(+), 300 deletions(-) diff --git a/dd-sdk-android-core/api/apiSurface b/dd-sdk-android-core/api/apiSurface index 8f45de8089..5df5bacb9a 100644 --- a/dd-sdk-android-core/api/apiSurface +++ b/dd-sdk-android-core/api/apiSurface @@ -298,8 +298,8 @@ interface com.datadog.android.core.persistence.datastore.DataStoreCallback data class com.datadog.android.core.persistence.datastore.DataStoreContent constructor(Long, Int, T?) interface com.datadog.android.core.persistence.datastore.DataStoreHandler - fun setValue(String, T, com.datadog.android.core.persistence.Serializer, Int = 0) - fun value(String, com.datadog.android.core.internal.persistence.Deserializer, Int = 0, DataStoreCallback) + fun setValue(String, T, Int = 0, com.datadog.android.core.persistence.Serializer) + fun value(String, Int = 0, DataStoreCallback, com.datadog.android.core.internal.persistence.Deserializer) fun removeValue(String) companion object const val CURRENT_DATASTORE_VERSION: Int diff --git a/dd-sdk-android-core/api/dd-sdk-android-core.api b/dd-sdk-android-core/api/dd-sdk-android-core.api index a6d2edc7ec..c2439c6d1b 100644 --- a/dd-sdk-android-core/api/dd-sdk-android-core.api +++ b/dd-sdk-android-core/api/dd-sdk-android-core.api @@ -816,8 +816,8 @@ public abstract interface class com/datadog/android/core/persistence/datastore/D public static final field CURRENT_DATASTORE_VERSION I public static final field Companion Lcom/datadog/android/core/persistence/datastore/DataStoreHandler$Companion; public abstract fun removeValue (Ljava/lang/String;)V - public abstract fun setValue (Ljava/lang/String;Ljava/lang/Object;Lcom/datadog/android/core/persistence/Serializer;I)V - public abstract fun value (Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Deserializer;ILcom/datadog/android/core/persistence/datastore/DataStoreCallback;)V + public abstract fun setValue (Ljava/lang/String;Ljava/lang/Object;ILcom/datadog/android/core/persistence/Serializer;)V + public abstract fun value (Ljava/lang/String;ILcom/datadog/android/core/persistence/datastore/DataStoreCallback;Lcom/datadog/android/core/internal/persistence/Deserializer;)V } public final class com/datadog/android/core/persistence/datastore/DataStoreHandler$Companion { @@ -825,8 +825,8 @@ public final class com/datadog/android/core/persistence/datastore/DataStoreHandl } public final class com/datadog/android/core/persistence/datastore/DataStoreHandler$DefaultImpls { - public static synthetic fun setValue$default (Lcom/datadog/android/core/persistence/datastore/DataStoreHandler;Ljava/lang/String;Ljava/lang/Object;Lcom/datadog/android/core/persistence/Serializer;IILjava/lang/Object;)V - public static synthetic fun value$default (Lcom/datadog/android/core/persistence/datastore/DataStoreHandler;Ljava/lang/String;Lcom/datadog/android/core/internal/persistence/Deserializer;ILcom/datadog/android/core/persistence/datastore/DataStoreCallback;ILjava/lang/Object;)V + public static synthetic fun setValue$default (Lcom/datadog/android/core/persistence/datastore/DataStoreHandler;Ljava/lang/String;Ljava/lang/Object;ILcom/datadog/android/core/persistence/Serializer;ILjava/lang/Object;)V + public static synthetic fun value$default (Lcom/datadog/android/core/persistence/datastore/DataStoreHandler;Ljava/lang/String;ILcom/datadog/android/core/persistence/datastore/DataStoreCallback;Lcom/datadog/android/core/internal/persistence/Deserializer;ILjava/lang/Object;)V } public final class com/datadog/android/core/sampling/RateBasedSampler : com/datadog/android/core/sampling/Sampler { diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt index 2089bb16f8..0390dbbf88 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt @@ -6,7 +6,6 @@ package com.datadog.android.core.internal.persistence.datastore -import android.text.format.DateUtils import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger import com.datadog.android.core.internal.persistence.Deserializer @@ -14,10 +13,8 @@ import com.datadog.android.core.internal.persistence.datastore.ext.toByteArray import com.datadog.android.core.internal.persistence.datastore.ext.toInt import com.datadog.android.core.internal.persistence.datastore.ext.toLong import com.datadog.android.core.internal.persistence.file.FileReaderWriter -import com.datadog.android.core.internal.persistence.file.createNewFileSafe import com.datadog.android.core.internal.persistence.file.deleteSafe import com.datadog.android.core.internal.persistence.file.existsSafe -import com.datadog.android.core.internal.persistence.file.mkdirsSafe import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockType @@ -27,7 +24,6 @@ import com.datadog.android.core.persistence.Serializer import com.datadog.android.core.persistence.datastore.DataStoreCallback import com.datadog.android.core.persistence.datastore.DataStoreContent import com.datadog.android.core.persistence.datastore.DataStoreHandler -import com.datadog.android.core.persistence.datastore.DataStoreHandler.Companion.CURRENT_DATASTORE_VERSION import java.io.File import java.util.Locale import java.util.concurrent.ExecutorService @@ -46,10 +42,10 @@ internal class DataStoreFileHandler( override fun setValue( key: String, data: T, - serializer: Serializer, - version: Int + version: Int, + serializer: Serializer ) { - executorService.submitSafe("datastoreRead", internalLogger) { + executorService.submitSafe("dataStoreWrite", internalLogger) { writeEntry(key, data, serializer, version) } } @@ -57,35 +53,33 @@ internal class DataStoreFileHandler( @WorkerThread override fun value( key: String, - deserializer: Deserializer, version: Int, - callback: DataStoreCallback + callback: DataStoreCallback, + deserializer: Deserializer ) { - executorService.submitSafe("readEntry", internalLogger) { + executorService.submitSafe("dataStoreRead", internalLogger) { readEntry(key, deserializer, version, callback) } } @WorkerThread override fun removeValue(key: String) { - executorService.submitSafe("readEntry", internalLogger) { + executorService.submitSafe("dataStoreRemove", internalLogger) { deleteFromDataStore(key) } } private fun deleteFromDataStore(key: String) { - val dataStoreDirectory = dataStoreFileHelper.getDataStoreDirectory( - featureName = featureName, - folderName = DATASTORE_FOLDER_NAME.format(Locale.US, CURRENT_DATASTORE_VERSION), - storageDir = storageDir - ) - val datastoreFile = dataStoreFileHelper.getDataStoreFile( - dataStoreDirectory = dataStoreDirectory, - dataStoreFileName = key + featureName = featureName, + storageDir = storageDir, + internalLogger = internalLogger, + key = key ) - datastoreFile.deleteSafe(internalLogger) + if (datastoreFile.existsSafe(internalLogger)) { + datastoreFile.deleteSafe(internalLogger) + } } private fun readEntry( @@ -94,15 +88,11 @@ internal class DataStoreFileHandler( version: Int, callback: DataStoreCallback ) { - val dataStoreDirectory = dataStoreFileHelper.getDataStoreDirectory( - featureName = featureName, - folderName = DATASTORE_FOLDER_NAME.format(Locale.US, CURRENT_DATASTORE_VERSION), - storageDir = storageDir - ) - val datastoreFile = dataStoreFileHelper.getDataStoreFile( - dataStoreDirectory = dataStoreDirectory, - dataStoreFileName = key + featureName = featureName, + storageDir = storageDir, + internalLogger = internalLogger, + key = key ) if (!datastoreFile.existsSafe(internalLogger)) { @@ -119,24 +109,33 @@ internal class DataStoreFileHandler( serializer: Serializer, version: Int ) { - val dataStoreDirectory = createDataStoreDirectoryIfNecessary(featureName) - val dataStoreFile = createDataStoreFileIfNecessary(dataStoreDirectory, key) + val datastoreFile = dataStoreFileHelper.getDataStoreFile( + featureName = featureName, + storageDir = storageDir, + internalLogger = internalLogger, + key = key + ) val lastUpdateBlock = getLastUpdateDateBlock() val versionCodeBlock = getVersionCodeBlock(version) val dataBlock = getDataBlock(data, serializer) - if (lastUpdateBlock == null || versionCodeBlock == null || dataBlock == null) return + if (lastUpdateBlock == null || versionCodeBlock == null || dataBlock == null) { + return + } + + val dataToWrite = listOf(lastUpdateBlock, versionCodeBlock, dataBlock).join( + separator = byteArrayOf(), + internalLogger = internalLogger + ) writeToFile( - dataStoreFile, - listOf(lastUpdateBlock, versionCodeBlock, dataBlock).join( - separator = byteArrayOf(), - internalLogger = internalLogger - ) + datastoreFile, + dataToWrite ) } + @Suppress("ThreadSafety") private fun writeToFile(dataStoreFile: File, data: ByteArray) { fileReaderWriter.writeData( file = dataStoreFile, @@ -158,7 +157,8 @@ internal class DataStoreFileHandler( val dataBlock = TLVBlock( type = TLVBlockType.DATA, - data = serializedData + data = serializedData, + internalLogger = internalLogger ) return dataBlock.serialize() @@ -169,7 +169,8 @@ internal class DataStoreFileHandler( val lastUpdateDateByteArray = now.toByteArray() val lastUpdateDateBlock = TLVBlock( type = TLVBlockType.LAST_UPDATE_DATE, - data = lastUpdateDateByteArray + data = lastUpdateDateByteArray, + internalLogger = internalLogger ) return lastUpdateDateBlock.serialize() @@ -179,18 +180,19 @@ internal class DataStoreFileHandler( val versionCodeByteArray = version.toByteArray() val versionBlock = TLVBlock( type = TLVBlockType.VERSION_CODE, - data = versionCodeByteArray + data = versionCodeByteArray, + internalLogger = internalLogger ) return versionBlock.serialize() } - @Suppress("ReturnCount") + @Suppress("ReturnCount", "ThreadSafety") private fun readFromDataStoreFile( datastoreFile: File, deserializer: Deserializer, tlvBlockFileReader: TLVBlockFileReader, - requestedVersion: Int? = 0, + requestedVersion: Int, callback: DataStoreCallback ) { val tlvBlocks = tlvBlockFileReader.read(datastoreFile) @@ -209,21 +211,15 @@ internal class DataStoreFileHandler( return } - val fileVersionIsWrong = dataStoreContent.versionCode != requestedVersion - val fileIsTooOld = isDataStoreTooOld(dataStoreContent.lastUpdateDate) - - if (fileVersionIsWrong) { - logInvalidVersionError() + if (requestedVersion != 0 && dataStoreContent.versionCode != requestedVersion) { + callback.onNoData() + return } - if (fileIsTooOld || fileVersionIsWrong) { - datastoreFile.deleteSafe(internalLogger) - callback.onFailure() - } else { - callback.onSuccess(dataStoreContent) - } + callback.onSuccess(dataStoreContent) } + @Suppress("ReturnCount") private fun tryToMapToDataStoreContents( deserializer: Deserializer, tlvBlocks: List @@ -243,23 +239,15 @@ internal class DataStoreFileHandler( typesToBlocks[type] = block } - val lastUpdateBlock = typesToBlocks[TLVBlockType.LAST_UPDATE_DATE] - val versionCodeBlock = typesToBlocks[TLVBlockType.VERSION_CODE] - val dataBlock = typesToBlocks[TLVBlockType.DATA] - return if (lastUpdateBlock == null || versionCodeBlock == null || dataBlock == null) { - null // this should never happen as we know by this stage that these cannot be null - } else { - DataStoreContent( - lastUpdateDate = lastUpdateBlock.data.toLong(), - versionCode = versionCodeBlock.data.toInt(), - data = deserializer.deserialize(String(dataBlock.data)) - ) - } - } + val lastUpdateBlock = typesToBlocks[TLVBlockType.LAST_UPDATE_DATE] ?: return null + val versionCodeBlock = typesToBlocks[TLVBlockType.VERSION_CODE] ?: return null + val dataBlock = typesToBlocks[TLVBlockType.DATA] ?: return null - private fun isDataStoreTooOld(lastUpdateDate: Long): Boolean { - val currentTime = System.currentTimeMillis() - return currentTime - lastUpdateDate > DATASTORE_EXPIRE_TIME + return DataStoreContent( + lastUpdateDate = lastUpdateBlock.data.toLong(), + versionCode = versionCodeBlock.data.toInt(), + data = deserializer.deserialize(String(dataBlock.data)) + ) } private fun logSameBlockAppearsTwiceError(type: TLVBlockType) { @@ -270,14 +258,6 @@ internal class DataStoreFileHandler( ) } - private fun logInvalidVersionError() { - internalLogger.log( - level = InternalLogger.Level.ERROR, - target = InternalLogger.Target.MAINTAINER, - messageBuilder = { INVALID_VERSION_ERROR } - ) - } - private fun logInvalidNumberOfBlocksError(numberOfBlocks: Int) { internalLogger.log( level = InternalLogger.Level.ERROR, @@ -294,45 +274,9 @@ internal class DataStoreFileHandler( ) } - private fun createDataStoreDirectoryIfNecessary(featureName: String): File { - val dataStoreDirectory = dataStoreFileHelper.getDataStoreDirectory( - featureName = featureName, - folderName = DATASTORE_FOLDER_NAME.format(Locale.US, CURRENT_DATASTORE_VERSION), - storageDir = storageDir - ) - - if (!dataStoreDirectory.existsSafe(internalLogger)) { - dataStoreDirectory.mkdirsSafe(internalLogger) - } - - return dataStoreDirectory - } - - private fun createDataStoreFileIfNecessary( - dataStoreDirectory: File, - dataStoreFileName: String - ): File { - val datastoreFile = dataStoreFileHelper.getDataStoreFile( - dataStoreDirectory = dataStoreDirectory, - dataStoreFileName = dataStoreFileName - ) - - if (!datastoreFile.existsSafe(internalLogger)) { - datastoreFile.createNewFileSafe(internalLogger) - } - - return datastoreFile - } - internal companion object { - internal const val DATASTORE_FOLDER_NAME = "datastore_v%s" - private const val DATASTORE_EXPIRE_TIME = DateUtils.DAY_IN_MILLIS * 30 // 30 days - internal const val FAILED_TO_SERIALIZE_DATA_ERROR = "Write error - Failed to serialize data for the datastore" - - internal const val INVALID_VERSION_ERROR = - "Read error - datastore file contains wrong version! This should never happen" internal const val INVALID_NUMBER_OF_BLOCKS_ERROR = "Read error - datastore file contains an invalid number of blocks. Was: %s" internal const val SAME_BLOCK_APPEARS_TWICE_ERROR = diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt index d6b04be320..320f3663e9 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt @@ -6,20 +6,52 @@ package com.datadog.android.core.internal.persistence.datastore +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.file.mkdirsSafe +import com.datadog.android.core.persistence.datastore.DataStoreHandler import java.io.File +import java.util.Locale internal class DataStoreFileHelper { - internal fun getDataStoreDirectory( + internal fun getDataStoreFile( + featureName: String, storageDir: File, + internalLogger: InternalLogger, + key: String + ): File { + val dataStoreDirectory = createDataStoreDirectoryIfNecessary( + featureName = featureName, + storageDir = storageDir, + internalLogger = internalLogger + ) + + return File(dataStoreDirectory, key) + } + + private fun createDataStoreDirectoryIfNecessary( featureName: String, - folderName: String - ): File = File( - storageDir, - "$featureName/$folderName" - ) + storageDir: File, + internalLogger: InternalLogger + ): File { + val folderName = DATASTORE_FOLDER_NAME.format( + Locale.US, + DataStoreHandler.CURRENT_DATASTORE_VERSION + ) - internal fun getDataStoreFile( - dataStoreDirectory: File, - dataStoreFileName: String - ) = File(dataStoreDirectory, dataStoreFileName) + val dataStoreDirectory = File( + storageDir, + "$featureName/$folderName" + ) + + if (!dataStoreDirectory.existsSafe(internalLogger)) { + dataStoreDirectory.mkdirsSafe(internalLogger) + } + + return dataStoreDirectory + } + + internal companion object { + internal const val DATASTORE_FOLDER_NAME = "datastore_v%s" + } } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt index f23ff676c7..ee80c30c19 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt @@ -15,17 +15,17 @@ internal class NoOpDataStoreHandler : DataStoreHandler { override fun setValue( key: String, data: T, - serializer: Serializer, - version: Int + version: Int, + serializer: Serializer ) { // NoOp Implementation } override fun value( key: String, - deserializer: Deserializer, version: Int, - callback: DataStoreCallback + callback: DataStoreCallback, + deserializer: Deserializer ) { // NoOp Implementation } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/ext/ByteArrayExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/ext/ByteArrayExt.kt index 6b97dda2f4..c029ffc3a7 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/ext/ByteArrayExt.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/ext/ByteArrayExt.kt @@ -25,3 +25,14 @@ internal fun ByteArray.toShort(): Short { @Suppress("UnsafeThirdPartyFunctionCall") return ByteBuffer.wrap(this).getShort() } + +@Suppress("TooGenericExceptionCaught", "SwallowedException") +internal fun ByteArray.copyOfRangeSafe(start: Int, end: Int): ByteArray { + return try { + this.copyOfRange(start, end) + } catch (e: IndexOutOfBoundsException) { + byteArrayOf() + } catch (e: IllegalArgumentException) { + byteArrayOf() + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt index 04b84d8c03..fa0f0673a9 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt @@ -6,13 +6,16 @@ package com.datadog.android.core.internal.persistence.tlvformat +import com.datadog.android.api.InternalLogger import java.nio.ByteBuffer +import java.util.Locale internal class TLVBlock( val type: TLVBlockType, - val data: ByteArray + val data: ByteArray, + val internalLogger: InternalLogger ) { - internal fun serialize(): ByteArray? { + internal fun serialize(maxLength: Int = MAXIMUM_DATA_SIZE_MB): ByteArray? { if (data.isEmpty()) return null val typeAsShort = type.rawValue.toShort() @@ -29,11 +32,31 @@ internal class TLVBlock( // // array UnsupportedOperationException - ByteBuffer buffer is backed by array @Suppress("UnsafeThirdPartyFunctionCall") - return ByteBuffer + val byteBuffer = ByteBuffer .allocate(data.size + Int.SIZE_BYTES + Short.SIZE_BYTES) .putShort(typeAsShort) .putInt(length) .put(data) .array() + + val bufferLength = byteBuffer.size + + return if (bufferLength > maxLength) { + internalLogger.log( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.WARN, + messageBuilder = { BYTE_LENGTH_EXCEEDED_ERROR.format(Locale.US, maxLength) } + ) + null + } else { + byteBuffer + } + } + + internal companion object { + // The maximum length of data (Value) in TLV block defining key data. + private const val MAXIMUM_DATA_SIZE_MB = 10 * 1024 * 1024 // 10 mb + internal const val BYTE_LENGTH_EXCEEDED_ERROR = + "DataBlock length exceeds limit of %s bytes" } } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReader.kt index bf28326901..afb0c284a2 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReader.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReader.kt @@ -8,14 +8,11 @@ package com.datadog.android.core.internal.persistence.tlvformat import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.datastore.ext.copyOfRangeSafe import com.datadog.android.core.internal.persistence.datastore.ext.toInt import com.datadog.android.core.internal.persistence.datastore.ext.toShort import com.datadog.android.core.internal.persistence.file.FileReaderWriter -import com.datadog.android.core.internal.persistence.file.existsSafe -import com.datadog.android.core.internal.persistence.file.lengthSafe import java.io.File -import java.io.IOException -import java.io.InputStream import java.util.Locale internal class TLVBlockFileReader( @@ -23,71 +20,90 @@ internal class TLVBlockFileReader( val fileReaderWriter: FileReaderWriter ) { @WorkerThread - internal fun read(file: File): List { - if (!file.existsSafe(internalLogger) || file.lengthSafe(internalLogger) == 0L) { - return arrayListOf() - } - + internal fun read( + file: File + ): List { + val byteArray = fileReaderWriter.readData(file) val blocks = mutableListOf() - val stream = fileReaderWriter.readData(file).inputStream() + var currentIndex = 0 - var nextBlock: TLVBlock? - do { - nextBlock = readBlock(stream) - if (nextBlock != null) blocks.add(nextBlock) - } while (nextBlock != null) + while (currentIndex < byteArray.size) { + val result = readBlock(byteArray, currentIndex) ?: break + blocks.add(result.data) + currentIndex = result.newIndex + } return blocks } - private fun readBlock(stream: InputStream): TLVBlock? { - val type = readType(stream) ?: return null + @Suppress("ReturnCount") + private fun readBlock(inputArray: ByteArray, currentIndex: Int): TLVResult? { + val typeResult = readType(inputArray, currentIndex) ?: return null + val data = readData(inputArray, typeResult.newIndex) ?: return null - val data = readData(stream) - - return TLVBlock(type, data) + val block = TLVBlock(typeResult.data, data.data, internalLogger) + return TLVResult( + data = block, + newIndex = data.newIndex + ) } - private fun readType(stream: InputStream): TLVBlockType? { + @Suppress("ReturnCount") + private fun readType(inputArray: ByteArray, currentIndex: Int): TLVResult? { val typeBlockSize = UShort.SIZE_BYTES - val bytes = read(stream, typeBlockSize) + var newIndex = currentIndex + newIndex += typeBlockSize - if (bytes.size != typeBlockSize) { + if (newIndex > inputArray.size) { + logFailedToDeserializeError() return null } + val bytes = inputArray.copyOfRangeSafe(currentIndex, newIndex) + val shortValue = bytes.toShort() + val tlvHeader = TLVBlockType.fromValue(shortValue.toUShort()) if (tlvHeader == null) { logTypeCorruptionError(shortValue) + return null } - return tlvHeader + return TLVResult( + data = tlvHeader, + newIndex = currentIndex + typeBlockSize + ) } - private fun readData(stream: InputStream): ByteArray { + private fun readData(inputArray: ByteArray, currentIndex: Int): TLVResult? { val lengthBlockSize = Int.SIZE_BYTES - val lengthInBytes = read(stream, lengthBlockSize) - val length = lengthInBytes.toInt() + var newIndex = currentIndex + lengthBlockSize - return read(stream, length) - } - - private fun read(stream: InputStream, length: Int): ByteArray { - val buffer = ByteArray(length) - val status = safeReadFromStream(stream, buffer) - return if (status == -1) { // stream is finished (or error) - ByteArray(0) - } else { - buffer + if (newIndex > inputArray.size) { + logFailedToDeserializeError() + return null } + + val lengthInBytes = inputArray.copyOfRangeSafe(currentIndex, newIndex) + + val lengthData = lengthInBytes.toInt() + + val dataBytes = + inputArray.copyOfRangeSafe(newIndex, newIndex + lengthData) + + newIndex += lengthData + + return TLVResult( + data = dataBytes, + newIndex = newIndex + ) } private fun logTypeCorruptionError(shortValue: Short) { internalLogger.log( target = InternalLogger.Target.MAINTAINER, - level = InternalLogger.Level.ERROR, + level = InternalLogger.Level.WARN, messageBuilder = { CORRUPT_TLV_HEADER_TYPE_ERROR.format( Locale.US, @@ -97,27 +113,21 @@ internal class TLVBlockFileReader( ) } - @Suppress("SwallowedException", "TooGenericExceptionCaught", "UnsafeThirdPartyFunctionCall") - private fun safeReadFromStream(stream: InputStream, buffer: ByteArray): Int { - return try { - stream.read(buffer) - } catch (e: IOException) { - internalLogger.log( - level = InternalLogger.Level.ERROR, - target = InternalLogger.Target.MAINTAINER, - messageBuilder = { FAILED_TO_READ_FROM_INPUT_STREAM_ERROR }, - e - ) - -1 - } catch (e: NullPointerException) { - // cannot happen - buffer is not null - -1 - } + private fun logFailedToDeserializeError() { + internalLogger.log( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.WARN, + messageBuilder = { FAILED_TO_DESERIALIZE_ERROR } + ) } + private data class TLVResult( + val data: T, + val newIndex: Int + ) + internal companion object { - internal const val CORRUPT_TLV_HEADER_TYPE_ERROR = "TLV header corrupt. Invalid type" - internal const val FAILED_TO_READ_FROM_INPUT_STREAM_ERROR = - "Failed to read from input stream" + internal const val CORRUPT_TLV_HEADER_TYPE_ERROR = "TLV header corrupt. Invalid type %s" + internal const val FAILED_TO_DESERIALIZE_ERROR = "Failed to deserialize TLV data length" } } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreHandler.kt index 2821bb7c31..7117b81cdb 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreHandler.kt @@ -20,15 +20,15 @@ interface DataStoreHandler { * @param T datatype of the data to write to the datastore. * @param key name of the datastore entry. * @param data to write. - * @param serializer to use to serialize the data. * @param version optional version for the entry. * If not specified will give the entry version 0 - even if that would be a downgrade from the previous version. + * @param serializer to use to serialize the data. */ fun setValue( key: String, data: T, - serializer: Serializer, - version: Int = 0 + version: Int = 0, + serializer: Serializer ) /** @@ -36,16 +36,16 @@ interface DataStoreHandler { * * @param T datatype of the data to read from the datastore. * @param key name of the datastore entry. - * @param deserializer to use to deserialize the data. * @param version optional version to use when reading from the datastore. - * If specified, will only return data if it exactly matches this version number. + * If specified, will only return data if the persistent entry exactly matches this version number. * @param callback to return result asynchronously. + * @param deserializer to use to deserialize the data. */ fun value( key: String, - deserializer: Deserializer, version: Int = 0, - callback: DataStoreCallback + callback: DataStoreCallback, + deserializer: Deserializer ) /** diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt index b113820039..96864e8c1d 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt @@ -9,16 +9,13 @@ package com.datadog.android.core.internal.persistence.file.datastore import com.datadog.android.api.InternalLogger import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler -import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler.Companion.DATASTORE_FOLDER_NAME import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler.Companion.FAILED_TO_SERIALIZE_DATA_ERROR import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler.Companion.INVALID_NUMBER_OF_BLOCKS_ERROR -import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler.Companion.INVALID_VERSION_ERROR import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler.Companion.SAME_BLOCK_APPEARS_TWICE_ERROR import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper import com.datadog.android.core.internal.persistence.file.FileReaderWriter -import com.datadog.android.core.internal.persistence.file.createNewFileSafe +import com.datadog.android.core.internal.persistence.file.deleteSafe import com.datadog.android.core.internal.persistence.file.existsSafe -import com.datadog.android.core.internal.persistence.file.mkdirsSafe import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockType @@ -42,6 +39,8 @@ import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.any import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.eq +import org.mockito.kotlin.never import org.mockito.kotlin.verify import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever @@ -114,18 +113,12 @@ internal class DataStoreFileHandlerTest { StubFuture() } - whenever( - mockDataStoreFileHelper.getDataStoreDirectory( - featureName = fakeFeatureName, - folderName = DATASTORE_FOLDER_NAME.format(Locale.US, CURRENT_DATASTORE_VERSION), - storageDir = mockStorageDir - ) - ).thenReturn(mockDataStoreDirectory) - whenever( mockDataStoreFileHelper.getDataStoreFile( - dataStoreDirectory = mockDataStoreDirectory, - dataStoreFileName = fakeKey + featureName = eq(fakeFeatureName), + storageDir = eq(mockStorageDir), + internalLogger = eq(mockInternalLogger), + key = any() ) ).thenReturn(mockDataStoreFile) @@ -252,20 +245,14 @@ internal class DataStoreFileHandlerTest { } @Test - fun `M log error W read() { same block appears twice }`() { + fun `M return no data W value() { explicit version and versions don't match }`() { // Given - blocksReturned.clear() - blocksReturned.add(createVersionBlock(true)) - blocksReturned.add(createVersionBlock(true)) - blocksReturned.add(createDataBlock()) - - val expectedError = SAME_BLOCK_APPEARS_TWICE_ERROR.format(Locale.US, TLVBlockType.VERSION_CODE) + var noData = false // When testedDataStoreHandler.value( key = fakeKey, - deserializer = mockDeserializer, - version = CURRENT_DATASTORE_VERSION, + version = 99, callback = object : DataStoreCallback { override fun onSuccess(dataStoreContent: DataStoreContent) { // should not get here @@ -273,29 +260,31 @@ internal class DataStoreFileHandlerTest { } override fun onFailure() { - mockInternalLogger.verifyLog( - level = InternalLogger.Level.ERROR, - target = InternalLogger.Target.MAINTAINER, - message = expectedError - ) + // should not get here + assertThat(1).isEqualTo(2) } override fun onNoData() { - // should not get here - assertThat(1).isEqualTo(2) + noData = true } - } + }, + deserializer = mockDeserializer ) + + // Then + assertThat(noData).isTrue() } @Test - fun `M log error W read() { version too old }`() { + fun `M log error W read() { same block appears twice }`() { // Given blocksReturned.clear() - blocksReturned.add(createLastUpdateDateBlock()) - blocksReturned.add(createVersionBlock(false)) + blocksReturned.add(createVersionBlock(true)) + blocksReturned.add(createVersionBlock(true)) blocksReturned.add(createDataBlock()) + val expectedError = SAME_BLOCK_APPEARS_TWICE_ERROR.format(Locale.US, TLVBlockType.VERSION_CODE) + // When testedDataStoreHandler.value( key = fakeKey, @@ -311,7 +300,7 @@ internal class DataStoreFileHandlerTest { mockInternalLogger.verifyLog( level = InternalLogger.Level.ERROR, target = InternalLogger.Target.MAINTAINER, - message = INVALID_VERSION_ERROR + message = expectedError ) } @@ -382,8 +371,8 @@ internal class DataStoreFileHandlerTest { // When testedDataStoreHandler.setValue( key = fakeKey, - serializer = mockSerializer, - data = fakeDataString + data = fakeDataString, + serializer = mockSerializer ) // Then @@ -395,35 +384,48 @@ internal class DataStoreFileHandlerTest { } @Test - fun `M create directory paths W write() { directory does not already exist }`() { - // Given - whenever(mockDataStoreDirectory.existsSafe(mockInternalLogger)).thenReturn(false) - + fun `M write to file W setValue()`() { // When testedDataStoreHandler.setValue( key = fakeKey, - serializer = mockSerializer, - data = fakeDataString + data = fakeDataString, + serializer = mockSerializer ) // Then - verify(mockDataStoreDirectory).mkdirsSafe(mockInternalLogger) + verify(mockFileReaderWriter).writeData( + eq(mockDataStoreFile), + any(), + eq(false) + ) } + // endregion + + // region removeValue + @Test - fun `M create new datastore file W write() { file does not already exist }`() { + fun `M call deleteSafe W removeValue() { file exists }`() { + // Given + whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(true) + + // When + testedDataStoreHandler.removeValue(fakeKey) + + // Then + verify(mockDataStoreFile).deleteSafe(mockInternalLogger) + } + + @Test + fun `M not call deleteSafe W removeValue() { file does not exist }`() { // Given whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(false) // When - testedDataStoreHandler.setValue( - key = fakeKey, - serializer = mockSerializer, - data = fakeDataString - ) + testedDataStoreHandler.removeValue(fakeKey) // Then - verify(mockDataStoreFile).createNewFileSafe(mockInternalLogger) + verify(mockDataStoreFile, never()).deleteSafe(mockInternalLogger) } // endregion @@ -432,12 +434,14 @@ internal class DataStoreFileHandlerTest { return if (valid) { TLVBlock( type = TLVBlockType.VERSION_CODE, - data = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(newVersion).array() + data = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(newVersion).array(), + internalLogger = mockInternalLogger ) } else { TLVBlock( type = TLVBlockType.VERSION_CODE, - data = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(newVersion - 1).array() + data = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(newVersion - 1).array(), + internalLogger = mockInternalLogger ) } } @@ -447,13 +451,15 @@ internal class DataStoreFileHandlerTest { type = TLVBlockType.LAST_UPDATE_DATE, data = ByteBuffer.allocate(Long.SIZE_BYTES) .putLong(System.currentTimeMillis()) - .array() + .array(), + internalLogger = mockInternalLogger ) private fun createDataBlock(dataBytes: ByteArray = fakeDataBytes): TLVBlock = TLVBlock( type = TLVBlockType.DATA, - data = dataBytes + data = dataBytes, + internalLogger = mockInternalLogger ) private class StubFuture : Future { diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt index 9a96fccebe..e5747cd411 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt @@ -7,12 +7,12 @@ package com.datadog.android.core.internal.persistence.tlvformat import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.datastore.ext.toByteArray import com.datadog.android.core.internal.persistence.file.FileReaderWriter -import com.datadog.android.core.internal.persistence.file.existsSafe -import com.datadog.android.core.internal.persistence.file.lengthSafe -import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader.Companion.CORRUPT_TLV_HEADER_TYPE_ERROR +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader.Companion.FAILED_TO_DESERIALIZE_ERROR import com.datadog.android.utils.forge.Configurator import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.IntForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension @@ -24,6 +24,10 @@ import org.junit.jupiter.api.extension.Extensions import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import java.io.File @@ -47,28 +51,22 @@ internal class TLVBlockFileReaderTest { @Mock private lateinit var mockInternalLogger: InternalLogger - @StringForgery + @StringForgery(regex = "^(\\w{3})\$") // a minimal number of chars to avoid flakiness private lateinit var fakeDataString: String + private lateinit var fakeUpdateBytes: ByteArray + private lateinit var fakeVersionBytes: ByteArray private lateinit var fakeDataBytes: ByteArray private lateinit var fakeBufferBytes: ByteArray @BeforeEach - fun setup() { - fakeDataBytes = fakeDataString.toByteArray(Charsets.UTF_8) - val length = fakeDataBytes.size - val type = TLVBlockType.DATA - - val buffer = ByteBuffer.allocate(fakeDataBytes.size + 6) - buffer.putShort(type.rawValue.toShort()) - buffer.putInt(length) - buffer.put(fakeDataBytes) - - fakeBufferBytes = buffer.array() + fun setup(@IntForgery(min = 0, max = 10) fakeVersion: Int) { + val lastUpdateBytes = createLastUpdateBytes() + val versionBytes = createVersionBytes(fakeVersion) + val dataBytes = createDataBytes() + val dataToWrite = lastUpdateBytes + versionBytes + dataBytes - whenever(mockFile.existsSafe(mockInternalLogger)).thenReturn(true) - whenever(mockFile.lengthSafe(mockInternalLogger)).thenReturn(fakeBufferBytes.size.toLong()) - whenever(mockFileReaderWriter.readData(mockFile)).thenReturn(fakeBufferBytes) + whenever(mockFileReaderWriter.readData(mockFile)).thenReturn(dataToWrite) testedReader = TLVBlockFileReader( fileReaderWriter = mockFileReaderWriter, @@ -77,9 +75,11 @@ internal class TLVBlockFileReaderTest { } @Test - fun `M return empty collection W all() { file does not exist }`() { + fun `M return empty collection W read() { invalid TLV type }`() { // Given - whenever(mockFile.existsSafe(mockInternalLogger)).thenReturn(false) + fakeBufferBytes = fakeDataString.toByteArray(Charsets.UTF_8) + whenever(mockFileReaderWriter.readData(mockFile)) + .thenReturn(fakeBufferBytes) // When val readBytes = testedReader.read(file = mockFile) @@ -89,57 +89,119 @@ internal class TLVBlockFileReaderTest { } @Test - fun `M return empty collection W all() { empty file }`() { + fun `M log error W read() { invalid TLV type }`() { // Given - whenever(mockFile.lengthSafe(mockInternalLogger)).thenReturn(0L) + fakeBufferBytes = fakeDataString.toByteArray(Charsets.UTF_8) + whenever(mockFileReaderWriter.readData(mockFile)) + .thenReturn(fakeBufferBytes) // When - val readBytes = testedReader.read(file = mockFile) + testedReader.read(file = mockFile) // Then - assertThat(readBytes).isEmpty() + val captor = argumentCaptor<() -> String>() + verify(mockInternalLogger).log( + level = eq(InternalLogger.Level.WARN), + target = eq(InternalLogger.Target.MAINTAINER), + captor.capture(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + assertThat(captor.firstValue.invoke()) + .startsWith("TLV header corrupt. Invalid type") } @Test - fun `M return empty collection W all() { invalid TLV type }`() { - // Given - fakeBufferBytes = fakeDataString.toByteArray(Charsets.UTF_8) - whenever(mockFileReaderWriter.readData(mockFile)) - .thenReturn(fakeBufferBytes) - + fun `M return valid object W read() { valid TLV format }`() { // When - val readBytes = testedReader.read(file = mockFile) + val tlvArray = testedReader.read(file = mockFile) // Then - assertThat(readBytes).isEmpty() + assertThat(tlvArray.size).isEqualTo(3) + val lastUpdateObject = tlvArray[0] + val versionObject = tlvArray[1] + val dataObject = tlvArray[2] + + assertThat(lastUpdateObject.type).isEqualTo(TLVBlockType.LAST_UPDATE_DATE) + assertThat(lastUpdateObject.data).isEqualTo(fakeUpdateBytes) + assertThat(versionObject.type).isEqualTo(TLVBlockType.VERSION_CODE) + assertThat(versionObject.data).isEqualTo(fakeVersionBytes) + assertThat(dataObject.type).isEqualTo(TLVBlockType.DATA) + assertThat(dataObject.data).isEqualTo(fakeDataBytes) } @Test - fun `M log error W all() { invalid TLV type }`() { + fun `M return empty array W read() { invalid type length }`() { // Given - fakeBufferBytes = fakeDataString.toByteArray(Charsets.UTF_8) - whenever(mockFileReaderWriter.readData(mockFile)) - .thenReturn(fakeBufferBytes) + val fakeByteArray = ByteBuffer.allocate(1).array() + whenever(mockFileReaderWriter.readData(mockFile)).thenReturn(fakeByteArray) // When - testedReader.read(file = mockFile) + val result = testedReader.read(mockFile) // Then + assertThat(result).isEmpty() mockInternalLogger.verifyLog( + level = InternalLogger.Level.WARN, target = InternalLogger.Target.MAINTAINER, - level = InternalLogger.Level.ERROR, - message = CORRUPT_TLV_HEADER_TYPE_ERROR + message = FAILED_TO_DESERIALIZE_ERROR ) } @Test - fun `M return valid object W all() { valid TLV format }`() { + fun `M return empty array W read() { invalid data length }`() { + // Given + val fakeBuffer = ByteBuffer.allocate(3) + val fakeArray = fakeBuffer.putShort(TLVBlockType.DATA.rawValue.toShort()).array() + whenever(mockFileReaderWriter.readData(mockFile)).thenReturn(fakeArray) + // When - val tlvArray = testedReader.read(file = mockFile) - val dataObject = tlvArray[0] + val result = testedReader.read(mockFile) // Then - assertThat(dataObject.type).isEqualTo(TLVBlockType.DATA) - assertThat(dataObject.data).isEqualTo(fakeDataBytes) + assertThat(result).isEmpty() + mockInternalLogger.verifyLog( + level = InternalLogger.Level.WARN, + target = InternalLogger.Target.MAINTAINER, + message = FAILED_TO_DESERIALIZE_ERROR + ) + } + + private fun createLastUpdateBytes(): ByteArray { + val now = System.currentTimeMillis() + fakeUpdateBytes = now.toByteArray() + val lastUpdateType = TLVBlockType.LAST_UPDATE_DATE.rawValue.toShort() + + return ByteBuffer + .allocate(fakeUpdateBytes.size + Int.SIZE_BYTES + Short.SIZE_BYTES) + .putShort(lastUpdateType) + .putInt(fakeUpdateBytes.size) + .put(fakeUpdateBytes) + .array() + } + + private fun createVersionBytes(fakeVersion: Int): ByteArray { + val versionType = TLVBlockType.VERSION_CODE.rawValue.toShort() + fakeVersionBytes = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(fakeVersion).array() + + return ByteBuffer + .allocate(fakeVersionBytes.size + Int.SIZE_BYTES + Short.SIZE_BYTES) + .putShort(versionType) + .putInt(fakeVersionBytes.size) + .put(fakeVersionBytes) + .array() + } + + private fun createDataBytes(): ByteArray { + fakeDataBytes = fakeDataString.toByteArray(Charsets.UTF_8) + val dataType = TLVBlockType.DATA.rawValue.toShort() + + return ByteBuffer + .allocate(fakeDataBytes.size + Int.SIZE_BYTES + Short.SIZE_BYTES) + .putShort(dataType) + .putInt(fakeDataBytes.size) + .put(fakeDataBytes) + .array() } } diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt index 6f907fdd97..72baa7c0d7 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt @@ -6,6 +6,7 @@ package com.datadog.android.core.internal.persistence.tlvformat +import com.datadog.android.api.InternalLogger import com.datadog.android.utils.forge.Configurator import fr.xgouchet.elmyr.annotation.IntForgery import fr.xgouchet.elmyr.annotation.StringForgery @@ -18,6 +19,10 @@ import org.junit.jupiter.api.extension.Extensions import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import java.nio.ByteBuffer @@ -34,12 +39,16 @@ internal class TLVBlockTest { @Mock lateinit var mockTLVBlockType: TLVBlockType + @Mock + lateinit var mockInternalLogger: InternalLogger + @Test fun `M return null W serialize() { empty data }`() { // Given testedTLVBlock = TLVBlock( type = mockTLVBlockType, - data = ByteArray(0) + data = ByteArray(0), + internalLogger = mockInternalLogger ) // When @@ -61,7 +70,8 @@ internal class TLVBlockTest { testedTLVBlock = TLVBlock( type = mockTLVBlockType, - data = fakeByteArray + data = fakeByteArray, + internalLogger = mockInternalLogger ) // When @@ -78,4 +88,36 @@ internal class TLVBlockTest { assertThat(typeAsShort).isEqualTo(fakeTLVType) assertThat(lengthAsInt).isEqualTo(data?.size) } + + @Test + fun `M log error W serialize() { exceeds max data length }`( + @StringForgery fakeString: String, + @IntForgery(min = 0, max = 10) fakeTypeAsInt: Int + ) { + // Given + val fakeTLVType = fakeTypeAsInt.toShort() + val fakeByteArray = fakeString.toByteArray(Charsets.UTF_8) + whenever(mockTLVBlockType.rawValue).thenReturn(fakeTLVType.toUShort()) + + testedTLVBlock = TLVBlock( + type = mockTLVBlockType, + data = fakeByteArray, + internalLogger = mockInternalLogger + ) + + // When + testedTLVBlock.serialize(1) + + // Then + val stringCaptor = argumentCaptor<() -> String>() + verify(mockInternalLogger).log( + level = eq(InternalLogger.Level.WARN), + target = eq(InternalLogger.Target.MAINTAINER), + stringCaptor.capture(), + anyOrNull(), + anyOrNull(), + anyOrNull() + ) + assertThat(stringCaptor.firstValue.invoke()).startsWith("DataBlock length exceeds limit") + } } From 6cba367fc331035a8e2204344131139bdebe0ee1 Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Wed, 29 May 2024 12:48:07 +0300 Subject: [PATCH 07/14] RUM-4098: improve DataStoreCallback --- dd-sdk-android-core/api/apiSurface | 6 ++--- .../datastore/DataStoreFileHandler.kt | 6 ++--- .../datastore/NoOpDataStoreHandler.kt | 2 +- .../datastore/DataStoreCallback.kt | 5 ++-- .../persistence/datastore/DataStoreHandler.kt | 2 +- .../datastore/DataStoreFileHandlerTest.kt | 25 ++++++++++--------- 6 files changed, 23 insertions(+), 23 deletions(-) diff --git a/dd-sdk-android-core/api/apiSurface b/dd-sdk-android-core/api/apiSurface index 5df5bacb9a..e31a3994f0 100644 --- a/dd-sdk-android-core/api/apiSurface +++ b/dd-sdk-android-core/api/apiSurface @@ -291,15 +291,15 @@ interface com.datadog.android.core.persistence.Serializer fun serialize(T): String? companion object fun Serializer.serializeToByteArray(T, com.datadog.android.api.InternalLogger): ByteArray? -interface com.datadog.android.core.persistence.datastore.DataStoreCallback - fun onSuccess(DataStoreContent) +interface com.datadog.android.core.persistence.datastore.DataStoreCallback + fun onSuccess(DataStoreContent) fun onFailure() fun onNoData() data class com.datadog.android.core.persistence.datastore.DataStoreContent constructor(Long, Int, T?) interface com.datadog.android.core.persistence.datastore.DataStoreHandler fun setValue(String, T, Int = 0, com.datadog.android.core.persistence.Serializer) - fun value(String, Int = 0, DataStoreCallback, com.datadog.android.core.internal.persistence.Deserializer) + fun value(String, Int = 0, DataStoreCallback, com.datadog.android.core.internal.persistence.Deserializer) fun removeValue(String) companion object const val CURRENT_DATASTORE_VERSION: Int diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt index 0390dbbf88..16328f7fe6 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt @@ -54,7 +54,7 @@ internal class DataStoreFileHandler( override fun value( key: String, version: Int, - callback: DataStoreCallback, + callback: DataStoreCallback, deserializer: Deserializer ) { executorService.submitSafe("dataStoreRead", internalLogger) { @@ -86,7 +86,7 @@ internal class DataStoreFileHandler( key: String, deserializer: Deserializer, version: Int, - callback: DataStoreCallback + callback: DataStoreCallback ) { val datastoreFile = dataStoreFileHelper.getDataStoreFile( featureName = featureName, @@ -193,7 +193,7 @@ internal class DataStoreFileHandler( deserializer: Deserializer, tlvBlockFileReader: TLVBlockFileReader, requestedVersion: Int, - callback: DataStoreCallback + callback: DataStoreCallback ) { val tlvBlocks = tlvBlockFileReader.read(datastoreFile) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt index ee80c30c19..e42673f36f 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt @@ -24,7 +24,7 @@ internal class NoOpDataStoreHandler : DataStoreHandler { override fun value( key: String, version: Int, - callback: DataStoreCallback, + callback: DataStoreCallback, deserializer: Deserializer ) { // NoOp Implementation diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreCallback.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreCallback.kt index 39fcc6bbfb..02c1feb1bf 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreCallback.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreCallback.kt @@ -9,15 +9,14 @@ package com.datadog.android.core.persistence.datastore /** * Callback for asynchronous operations on the datastore. */ -interface DataStoreCallback { +interface DataStoreCallback { /** * Called on successfully fetching data from the datastore. * - * @param T datatype returned by the datastore. * @param dataStoreContent contains the datastore data, version and lastUpdateDate. */ - fun onSuccess(dataStoreContent: DataStoreContent) + fun onSuccess(dataStoreContent: DataStoreContent) /** * Called when an exception occurred getting data from the datastore. diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreHandler.kt index 7117b81cdb..eba4f70e1e 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreHandler.kt @@ -44,7 +44,7 @@ interface DataStoreHandler { fun value( key: String, version: Int = 0, - callback: DataStoreCallback, + callback: DataStoreCallback, deserializer: Deserializer ) diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt index 96864e8c1d..cc3502144e 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt @@ -157,8 +157,8 @@ internal class DataStoreFileHandlerTest { key = fakeKey, deserializer = mockDeserializer, version = CURRENT_DATASTORE_VERSION, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) { // should not get here assertThat(1).isEqualTo(2) } @@ -189,8 +189,8 @@ internal class DataStoreFileHandlerTest { key = fakeKey, deserializer = mockDeserializer, version = CURRENT_DATASTORE_VERSION, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) { // should not get here assertThat(1).isEqualTo(2) } @@ -222,8 +222,8 @@ internal class DataStoreFileHandlerTest { key = fakeKey, deserializer = mockDeserializer, version = CURRENT_DATASTORE_VERSION, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) { // should not get here assertThat(1).isEqualTo(2) } @@ -253,8 +253,8 @@ internal class DataStoreFileHandlerTest { testedDataStoreHandler.value( key = fakeKey, version = 99, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) { // should not get here assertThat(1).isEqualTo(2) } @@ -290,8 +290,8 @@ internal class DataStoreFileHandlerTest { key = fakeKey, deserializer = mockDeserializer, version = CURRENT_DATASTORE_VERSION, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) { // should not get here assertThat(1).isEqualTo(2) } @@ -325,8 +325,9 @@ internal class DataStoreFileHandlerTest { key = fakeKey, deserializer = mockDeserializer, version = CURRENT_DATASTORE_VERSION, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { + callback = object : DataStoreCallback { + + override fun onSuccess(dataStoreContent: DataStoreContent) { assertThat(dataStoreContent.data).isEqualTo(fakeDataBytes) } From 311961ad424c2a3ff35834e66fe13fcac6f384fe Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Thu, 30 May 2024 15:06:33 +0300 Subject: [PATCH 08/14] RUM-4098: Extract Reader and Writer from DataStoreFileHandler --- .../android/core/internal/CoreFeature.kt | 6 - .../android/core/internal/SdkFeature.kt | 37 +- .../datastore/DataStoreFileHandler.kt | 257 +----------- .../datastore/DataStoreFileHelper.kt | 19 +- .../datastore/DatastoreFileReader.kt | 132 ++++++ .../datastore/DatastoreFileWriter.kt | 140 +++++++ .../core/internal/persistence/file/FileExt.kt | 7 - .../persistence/tlvformat/TLVBlock.kt | 58 ++- .../android/core/internal/CoreFeatureTest.kt | 18 - .../datastore/DataStoreFileHandlerTest.kt | 383 ++---------------- .../file/datastore/DataStoreFileHelperTest.kt | 80 ++++ .../file/datastore/DataStoreFileReaderTest.kt | 365 +++++++++++++++++ .../file/datastore/DataStoreFileWriterTest.kt | 201 +++++++++ .../persistence/tlvformat/TLVBlockTest.kt | 2 +- 14 files changed, 1050 insertions(+), 655 deletions(-) create mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt create mode 100644 dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt create mode 100644 dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHelperTest.kt create mode 100644 dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt create mode 100644 dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileWriterTest.kt diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt index b38ecf5bee..7eccf027a2 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/CoreFeature.kt @@ -35,7 +35,6 @@ import com.datadog.android.core.internal.net.info.NetworkInfoDeserializer import com.datadog.android.core.internal.net.info.NetworkInfoProvider import com.datadog.android.core.internal.net.info.NoOpNetworkInfoProvider import com.datadog.android.core.internal.persistence.JsonObjectDeserializer -import com.datadog.android.core.internal.persistence.datastore.NoOpDataStoreHandler import com.datadog.android.core.internal.persistence.file.FileMover import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig import com.datadog.android.core.internal.persistence.file.FileReaderWriter @@ -74,7 +73,6 @@ import com.datadog.android.core.internal.user.UserInfoDeserializer import com.datadog.android.core.internal.utils.submitSafe import com.datadog.android.core.internal.utils.unboundInternalLogger import com.datadog.android.core.persistence.PersistenceStrategy -import com.datadog.android.core.persistence.datastore.DataStoreHandler import com.datadog.android.core.thread.FlushableExecutorService import com.datadog.android.ndk.internal.DatadogNdkCrashHandler import com.datadog.android.ndk.internal.NdkCrashHandler @@ -139,7 +137,6 @@ internal class CoreFeature( internal var uploadFrequency: UploadFrequency = UploadFrequency.AVERAGE internal var batchProcessingLevel: BatchProcessingLevel = BatchProcessingLevel.MEDIUM internal var ndkCrashHandler: NdkCrashHandler = NoOpNdkCrashHandler() - internal var dataStoreHandler: DataStoreHandler = NoOpDataStoreHandler() internal var site: DatadogSite = DatadogSite.US1 internal var appBuildId: String? = null @@ -198,7 +195,6 @@ internal class CoreFeature( if (initialized.get()) { return } - readConfigurationSettings(configuration.coreConfig) readApplicationInformation(appContext, configuration) resolveProcessInfo(appContext) @@ -229,7 +225,6 @@ internal class CoreFeature( val nativeSourceOverride = configuration.additionalConfig[Datadog.DD_NATIVE_SOURCE_TYPE] as? String prepareNdkCrashData(nativeSourceOverride) setupInfoProviders(appContext, consent) - initialized.set(true) contextProvider = DatadogContextProvider(this) } @@ -265,7 +260,6 @@ internal class CoreFeature( initialized.set(false) ndkCrashHandler = NoOpNdkCrashHandler() - dataStoreHandler = NoOpDataStoreHandler() trackingConsentProvider = NoOpConsentProvider() contextProvider = NoOpContextProvider() } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt index 96f97a035a..21b8db0bde 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt @@ -38,6 +38,8 @@ import com.datadog.android.core.internal.persistence.NoOpStorage import com.datadog.android.core.internal.persistence.Storage import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper +import com.datadog.android.core.internal.persistence.datastore.DatastoreFileReader +import com.datadog.android.core.internal.persistence.datastore.DatastoreFileWriter import com.datadog.android.core.internal.persistence.datastore.NoOpDataStoreHandler import com.datadog.android.core.internal.persistence.file.FileMover import com.datadog.android.core.internal.persistence.file.FileOrchestrator @@ -364,17 +366,36 @@ internal class SdkFeature( encryption ) + val dataStoreFileHelper = DataStoreFileHelper(internalLogger) + val featureName = wrappedFeature.name + val storageDir = coreFeature.storageDir + + val tlvBlockFileReader = TLVBlockFileReader( + internalLogger = internalLogger, + fileReaderWriter = fileReaderWriter + ) + + val dataStoreFileReader = DatastoreFileReader( + dataStoreFileHelper = dataStoreFileHelper, + featureName = featureName, + internalLogger = internalLogger, + storageDir = storageDir, + tlvBlockFileReader = tlvBlockFileReader + ) + + val dataStoreFileWriter = DatastoreFileWriter( + dataStoreFileHelper = dataStoreFileHelper, + featureName = featureName, + fileReaderWriter = fileReaderWriter, + internalLogger = internalLogger, + storageDir = storageDir + ) + dataStore = DataStoreFileHandler( executorService = coreFeature.persistenceExecutorService, - storageDir = coreFeature.storageDir, - featureName = wrappedFeature.name, internalLogger = internalLogger, - fileReaderWriter = fileReaderWriter, - tlvBlockFileReader = TLVBlockFileReader( - internalLogger = internalLogger, - fileReaderWriter = fileReaderWriter - ), - dataStoreFileHelper = DataStoreFileHelper() + dataStoreFileReader = dataStoreFileReader, + datastoreFileWriter = dataStoreFileWriter ) } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt index 16328f7fe6..0ac6966c58 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt @@ -6,280 +6,49 @@ package com.datadog.android.core.internal.persistence.datastore -import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger import com.datadog.android.core.internal.persistence.Deserializer -import com.datadog.android.core.internal.persistence.datastore.ext.toByteArray -import com.datadog.android.core.internal.persistence.datastore.ext.toInt -import com.datadog.android.core.internal.persistence.datastore.ext.toLong -import com.datadog.android.core.internal.persistence.file.FileReaderWriter -import com.datadog.android.core.internal.persistence.file.deleteSafe -import com.datadog.android.core.internal.persistence.file.existsSafe -import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock -import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader -import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockType -import com.datadog.android.core.internal.utils.join import com.datadog.android.core.internal.utils.submitSafe import com.datadog.android.core.persistence.Serializer import com.datadog.android.core.persistence.datastore.DataStoreCallback -import com.datadog.android.core.persistence.datastore.DataStoreContent import com.datadog.android.core.persistence.datastore.DataStoreHandler -import java.io.File -import java.util.Locale import java.util.concurrent.ExecutorService -@Suppress("TooManyFunctions") internal class DataStoreFileHandler( private val executorService: ExecutorService, - private val storageDir: File, - private val featureName: String, private val internalLogger: InternalLogger, - private val fileReaderWriter: FileReaderWriter, - private val tlvBlockFileReader: TLVBlockFileReader, - private val dataStoreFileHelper: DataStoreFileHelper + private val dataStoreFileReader: DatastoreFileReader, + private val datastoreFileWriter: DatastoreFileWriter ) : DataStoreHandler { - @WorkerThread + override fun setValue( key: String, data: T, version: Int, serializer: Serializer ) { + @Suppress("ThreadSafety") // called in executor executorService.submitSafe("dataStoreWrite", internalLogger) { - writeEntry(key, data, serializer, version) + datastoreFileWriter.write(key, data, serializer, version) } } - @WorkerThread - override fun value( - key: String, - version: Int, - callback: DataStoreCallback, - deserializer: Deserializer - ) { - executorService.submitSafe("dataStoreRead", internalLogger) { - readEntry(key, deserializer, version, callback) - } - } - - @WorkerThread override fun removeValue(key: String) { + @Suppress("ThreadSafety") // called in executor executorService.submitSafe("dataStoreRemove", internalLogger) { - deleteFromDataStore(key) - } - } - - private fun deleteFromDataStore(key: String) { - val datastoreFile = dataStoreFileHelper.getDataStoreFile( - featureName = featureName, - storageDir = storageDir, - internalLogger = internalLogger, - key = key - ) - - if (datastoreFile.existsSafe(internalLogger)) { - datastoreFile.deleteSafe(internalLogger) + datastoreFileWriter.delete(key) } } - private fun readEntry( + override fun value( key: String, - deserializer: Deserializer, version: Int, - callback: DataStoreCallback - ) { - val datastoreFile = dataStoreFileHelper.getDataStoreFile( - featureName = featureName, - storageDir = storageDir, - internalLogger = internalLogger, - key = key - ) - - if (!datastoreFile.existsSafe(internalLogger)) { - callback.onNoData() - return - } - - readFromDataStoreFile(datastoreFile, deserializer, tlvBlockFileReader, version, callback) - } - - private fun writeEntry( - key: String, - data: T, - serializer: Serializer, - version: Int - ) { - val datastoreFile = dataStoreFileHelper.getDataStoreFile( - featureName = featureName, - storageDir = storageDir, - internalLogger = internalLogger, - key = key - ) - - val lastUpdateBlock = getLastUpdateDateBlock() - val versionCodeBlock = getVersionCodeBlock(version) - val dataBlock = getDataBlock(data, serializer) - - if (lastUpdateBlock == null || versionCodeBlock == null || dataBlock == null) { - return - } - - val dataToWrite = listOf(lastUpdateBlock, versionCodeBlock, dataBlock).join( - separator = byteArrayOf(), - internalLogger = internalLogger - ) - - writeToFile( - datastoreFile, - dataToWrite - ) - } - - @Suppress("ThreadSafety") - private fun writeToFile(dataStoreFile: File, data: ByteArray) { - fileReaderWriter.writeData( - file = dataStoreFile, - data = data, - append = false - ) - } - - private fun getDataBlock( - data: T, - serializer: Serializer - ): ByteArray? { - val serializedData = serializer.serialize(data)?.toByteArray() - - if (serializedData == null) { - logFailedToSerializeDataError() - return null - } - - val dataBlock = TLVBlock( - type = TLVBlockType.DATA, - data = serializedData, - internalLogger = internalLogger - ) - - return dataBlock.serialize() - } - - private fun getLastUpdateDateBlock(): ByteArray? { - val now = System.currentTimeMillis() - val lastUpdateDateByteArray = now.toByteArray() - val lastUpdateDateBlock = TLVBlock( - type = TLVBlockType.LAST_UPDATE_DATE, - data = lastUpdateDateByteArray, - internalLogger = internalLogger - ) - - return lastUpdateDateBlock.serialize() - } - - private fun getVersionCodeBlock(version: Int): ByteArray? { - val versionCodeByteArray = version.toByteArray() - val versionBlock = TLVBlock( - type = TLVBlockType.VERSION_CODE, - data = versionCodeByteArray, - internalLogger = internalLogger - ) - - return versionBlock.serialize() - } - - @Suppress("ReturnCount", "ThreadSafety") - private fun readFromDataStoreFile( - datastoreFile: File, - deserializer: Deserializer, - tlvBlockFileReader: TLVBlockFileReader, - requestedVersion: Int, - callback: DataStoreCallback + callback: DataStoreCallback, + deserializer: Deserializer ) { - val tlvBlocks = tlvBlockFileReader.read(datastoreFile) - - // there should be as many blocks read as there are block types - if (tlvBlocks.size != TLVBlockType.values().size) { - logInvalidNumberOfBlocksError(tlvBlocks.size) - callback.onFailure() - return - } - - val dataStoreContent = tryToMapToDataStoreContents(deserializer, tlvBlocks) - - if (dataStoreContent == null) { - callback.onFailure() - return - } - - if (requestedVersion != 0 && dataStoreContent.versionCode != requestedVersion) { - callback.onNoData() - return - } - - callback.onSuccess(dataStoreContent) - } - - @Suppress("ReturnCount") - private fun tryToMapToDataStoreContents( - deserializer: Deserializer, - tlvBlocks: List - ): DataStoreContent? { - // map the blocks to the actual types - val typesToBlocks = mutableMapOf() - for (block in tlvBlocks) { - val type = block.type - val blockTypeAlreadyExists = typesToBlocks[type] != null - - // verify that the same block doesn't appear more than once - if (blockTypeAlreadyExists) { - logSameBlockAppearsTwiceError(type) - return null - } - - typesToBlocks[type] = block + @Suppress("ThreadSafety") // called in executor + executorService.submitSafe("dataStoreRead", internalLogger) { + dataStoreFileReader.read(key, deserializer, version, callback) } - - val lastUpdateBlock = typesToBlocks[TLVBlockType.LAST_UPDATE_DATE] ?: return null - val versionCodeBlock = typesToBlocks[TLVBlockType.VERSION_CODE] ?: return null - val dataBlock = typesToBlocks[TLVBlockType.DATA] ?: return null - - return DataStoreContent( - lastUpdateDate = lastUpdateBlock.data.toLong(), - versionCode = versionCodeBlock.data.toInt(), - data = deserializer.deserialize(String(dataBlock.data)) - ) - } - - private fun logSameBlockAppearsTwiceError(type: TLVBlockType) { - internalLogger.log( - target = InternalLogger.Target.MAINTAINER, - level = InternalLogger.Level.ERROR, - messageBuilder = { SAME_BLOCK_APPEARS_TWICE_ERROR.format(Locale.US, type) } - ) - } - - private fun logInvalidNumberOfBlocksError(numberOfBlocks: Int) { - internalLogger.log( - level = InternalLogger.Level.ERROR, - target = InternalLogger.Target.MAINTAINER, - messageBuilder = { INVALID_NUMBER_OF_BLOCKS_ERROR.format(Locale.US, numberOfBlocks) } - ) - } - - private fun logFailedToSerializeDataError() { - internalLogger.log( - target = InternalLogger.Target.MAINTAINER, - level = InternalLogger.Level.ERROR, - messageBuilder = { FAILED_TO_SERIALIZE_DATA_ERROR } - ) - } - - internal companion object { - internal const val FAILED_TO_SERIALIZE_DATA_ERROR = - "Write error - Failed to serialize data for the datastore" - internal const val INVALID_NUMBER_OF_BLOCKS_ERROR = - "Read error - datastore file contains an invalid number of blocks. Was: %s" - internal const val SAME_BLOCK_APPEARS_TWICE_ERROR = - "Read error - same block appears twice in the datastore. Type: %s" } } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt index 320f3663e9..5dd16e7f7c 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt @@ -13,11 +13,12 @@ import com.datadog.android.core.persistence.datastore.DataStoreHandler import java.io.File import java.util.Locale -internal class DataStoreFileHelper { +internal class DataStoreFileHelper( + private val internalLogger: InternalLogger +) { internal fun getDataStoreFile( featureName: String, storageDir: File, - internalLogger: InternalLogger, key: String ): File { val dataStoreDirectory = createDataStoreDirectoryIfNecessary( @@ -51,7 +52,21 @@ internal class DataStoreFileHelper { return dataStoreDirectory } + internal fun isKeyInvalid(key: String): Boolean { + return key.contains("/") + } + + internal fun logInvalidKeyException() { + internalLogger.log( + level = InternalLogger.Level.WARN, + target = InternalLogger.Target.MAINTAINER, + messageBuilder = { INVALID_DATASTORE_KEY_FORMAT_EXCEPTION } + ) + } + internal companion object { internal const val DATASTORE_FOLDER_NAME = "datastore_v%s" + internal const val INVALID_DATASTORE_KEY_FORMAT_EXCEPTION = + "Datastore key must not be a path!" } } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt new file mode 100644 index 0000000000..710c54554d --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt @@ -0,0 +1,132 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.internal.persistence.datastore.ext.toInt +import com.datadog.android.core.internal.persistence.datastore.ext.toLong +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockType +import com.datadog.android.core.persistence.datastore.DataStoreCallback +import com.datadog.android.core.persistence.datastore.DataStoreContent +import java.io.File +import java.util.Locale + +internal class DatastoreFileReader( + private val dataStoreFileHelper: DataStoreFileHelper, + private val featureName: String, + private val storageDir: File, + private val internalLogger: InternalLogger, + private val tlvBlockFileReader: TLVBlockFileReader +) { + @WorkerThread + internal fun read( + key: String, + deserializer: Deserializer, + version: Int, + callback: DataStoreCallback + ) { + if (dataStoreFileHelper.isKeyInvalid(key)) { + dataStoreFileHelper.logInvalidKeyException() + return + } + + val datastoreFile = dataStoreFileHelper.getDataStoreFile( + featureName = featureName, + storageDir = storageDir, + key = key + ) + + if (!datastoreFile.existsSafe(internalLogger)) { + callback.onNoData() + return + } + + readFromDataStoreFile(datastoreFile, deserializer, tlvBlockFileReader, version, callback) + } + + @Suppress("ReturnCount", "ThreadSafety") + private fun readFromDataStoreFile( + datastoreFile: File, + deserializer: Deserializer, + tlvBlockFileReader: TLVBlockFileReader, + requestedVersion: Int, + callback: DataStoreCallback + ) { + val tlvBlocks = tlvBlockFileReader.read(datastoreFile) + + // there should be as many blocks read as there are block types + if (tlvBlocks.size != TLVBlockType.values().size) { + logInvalidNumberOfBlocksError(tlvBlocks.size) + callback.onFailure() + return + } + + val dataStoreContent = tryToMapToDataStoreContents(deserializer, tlvBlocks) + + if (dataStoreContent == null) { + callback.onFailure() + return + } + + if (requestedVersion != 0 && dataStoreContent.versionCode != requestedVersion) { + callback.onNoData() + return + } + + callback.onSuccess(dataStoreContent) + } + + private fun tryToMapToDataStoreContents( + deserializer: Deserializer, + tlvBlocks: List + ): DataStoreContent? { + if (tlvBlocks[0].type != TLVBlockType.LAST_UPDATE_DATE && + tlvBlocks[1].type != TLVBlockType.VERSION_CODE + ) { + logBlocksInUnexpectedBlocksOrderError() + return null + } + + val lastUpdateBlock = tlvBlocks[0] + val versionCodeBlock = tlvBlocks[1] + val dataBlock = tlvBlocks[2] + + return DataStoreContent( + lastUpdateDate = lastUpdateBlock.data.toLong(), + versionCode = versionCodeBlock.data.toInt(), + data = deserializer.deserialize(String(dataBlock.data)) + ) + } + + private fun logInvalidNumberOfBlocksError(numberOfBlocks: Int) { + internalLogger.log( + level = InternalLogger.Level.ERROR, + target = InternalLogger.Target.MAINTAINER, + messageBuilder = { INVALID_NUMBER_OF_BLOCKS_ERROR.format(Locale.US, numberOfBlocks) } + ) + } + + private fun logBlocksInUnexpectedBlocksOrderError() { + internalLogger.log( + level = InternalLogger.Level.ERROR, + target = InternalLogger.Target.MAINTAINER, + messageBuilder = { UNEXPECTED_BLOCKS_ORDER_ERROR } + ) + } + + internal companion object { + internal const val INVALID_NUMBER_OF_BLOCKS_ERROR = + "Read error - datastore file contains an invalid number of blocks. Was: %s" + internal const val UNEXPECTED_BLOCKS_ORDER_ERROR = + "Read error - blocks are in an unexpected order" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt new file mode 100644 index 0000000000..ad729fd826 --- /dev/null +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt @@ -0,0 +1,140 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.datastore + +import androidx.annotation.WorkerThread +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.datastore.ext.toByteArray +import com.datadog.android.core.internal.persistence.file.FileReaderWriter +import com.datadog.android.core.internal.persistence.file.deleteSafe +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockType +import com.datadog.android.core.internal.utils.join +import com.datadog.android.core.persistence.Serializer +import java.io.File + +internal class DatastoreFileWriter( + private val dataStoreFileHelper: DataStoreFileHelper, + private val featureName: String, + private val storageDir: File, + private val internalLogger: InternalLogger, + private val fileReaderWriter: FileReaderWriter +) { + @WorkerThread + internal fun write( + key: String, + data: T, + serializer: Serializer, + version: Int + ) { + if (dataStoreFileHelper.isKeyInvalid(key)) { + dataStoreFileHelper.logInvalidKeyException() + return + } + + val datastoreFile = dataStoreFileHelper.getDataStoreFile( + featureName = featureName, + storageDir = storageDir, + key = key + ) + + val lastUpdateBlock = getLastUpdateDateBlock() + val versionCodeBlock = getVersionCodeBlock(version) + val dataBlock = getDataBlock(data, serializer) + + // failed to serialize one or more blocks + if (lastUpdateBlock == null || versionCodeBlock == null || dataBlock == null) { + return + } + + val dataToWrite = listOf(lastUpdateBlock, versionCodeBlock, dataBlock).join( + separator = byteArrayOf(), + internalLogger = internalLogger + ) + + fileReaderWriter.writeData( + file = datastoreFile, + data = dataToWrite, + append = false + ) + } + + @WorkerThread + internal fun delete(key: String) { + if (dataStoreFileHelper.isKeyInvalid(key)) { + dataStoreFileHelper.logInvalidKeyException() + return + } + + val datastoreFile = dataStoreFileHelper.getDataStoreFile( + featureName = featureName, + storageDir = storageDir, + key = key + ) + + if (datastoreFile.existsSafe(internalLogger)) { + datastoreFile.deleteSafe(internalLogger) + } + } + + private fun getDataBlock( + data: T, + serializer: Serializer + ): ByteArray? { + val serializedData = serializer.serialize(data)?.toByteArray() + + if (serializedData == null) { + logFailedToSerializeDataError() + return null + } + + val dataBlock = TLVBlock( + type = TLVBlockType.DATA, + data = serializedData, + internalLogger = internalLogger + ) + + return dataBlock.serialize() + } + + private fun getLastUpdateDateBlock(): ByteArray? { + val now = System.currentTimeMillis() + val lastUpdateDateByteArray = now.toByteArray() + val lastUpdateDateBlock = TLVBlock( + type = TLVBlockType.LAST_UPDATE_DATE, + data = lastUpdateDateByteArray, + internalLogger = internalLogger + ) + + return lastUpdateDateBlock.serialize() + } + + private fun getVersionCodeBlock(version: Int): ByteArray? { + val versionCodeByteArray = version.toByteArray() + val versionBlock = TLVBlock( + type = TLVBlockType.VERSION_CODE, + data = versionCodeByteArray, + internalLogger = internalLogger + ) + + return versionBlock.serialize() + } + + private fun logFailedToSerializeDataError() { + internalLogger.log( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + messageBuilder = { FAILED_TO_SERIALIZE_DATA_ERROR } + ) + } + + internal companion object { + internal const val FAILED_TO_SERIALIZE_DATA_ERROR = + "Write error - Failed to serialize data for the datastore" + } +} diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileExt.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileExt.kt index 3fcf6ae72a..d3abeaa92a 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileExt.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/file/FileExt.kt @@ -79,13 +79,6 @@ internal fun File.deleteSafe(internalLogger: InternalLogger): Boolean { } } -internal fun File.createNewFileSafe(internalLogger: InternalLogger): Boolean { - return safeCall(default = false, internalLogger) { - @Suppress("UnsafeThirdPartyFunctionCall") - createNewFile() - } -} - /** * Non-throwing version of [File.exists]. If exception happens, false is returned. */ diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt index fa0f0673a9..d665ba0e65 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt @@ -15,42 +15,40 @@ internal class TLVBlock( val data: ByteArray, val internalLogger: InternalLogger ) { - internal fun serialize(maxLength: Int = MAXIMUM_DATA_SIZE_MB): ByteArray? { + @Suppress("ReturnCount") + internal fun serialize(maxEntrySize: Int = MAXIMUM_DATA_SIZE_MB): ByteArray? { if (data.isEmpty()) return null - val typeAsShort = type.rawValue.toShort() - val length = data.size - - // allocate IllegalArgumentException - cannot happen because - // capacity is always positive - // - // put BufferOverflowException - cannot happen because we calculate the capacity - // of the buffer to take into account the size of the TLV headers - // - // put/array ReadOnlyBufferException - cannot happen because ByteBuffer - // gives a mutable buffer - // - // array UnsupportedOperationException - ByteBuffer buffer is backed by array + val typeFieldSize = Short.SIZE_BYTES + val dataLengthFieldSize = Int.SIZE_BYTES + val dataFieldSize = data.size + + val entrySize = typeFieldSize + dataLengthFieldSize + dataFieldSize + + if (entrySize > maxEntrySize) { + logEntrySizeExceededError(maxEntrySize) + return null + } + + val tlvTypeAsShort = type.rawValue.toShort() + + // capacity is not a negative integer, buffer is not read only, + // has sufficient capacity and is backed by an array @Suppress("UnsafeThirdPartyFunctionCall") - val byteBuffer = ByteBuffer - .allocate(data.size + Int.SIZE_BYTES + Short.SIZE_BYTES) - .putShort(typeAsShort) - .putInt(length) + return ByteBuffer + .allocate(entrySize) + .putShort(tlvTypeAsShort) + .putInt(dataFieldSize) .put(data) .array() + } - val bufferLength = byteBuffer.size - - return if (bufferLength > maxLength) { - internalLogger.log( - target = InternalLogger.Target.MAINTAINER, - level = InternalLogger.Level.WARN, - messageBuilder = { BYTE_LENGTH_EXCEEDED_ERROR.format(Locale.US, maxLength) } - ) - null - } else { - byteBuffer - } + private fun logEntrySizeExceededError(maxEntrySize: Int) { + internalLogger.log( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.WARN, + messageBuilder = { BYTE_LENGTH_EXCEEDED_ERROR.format(Locale.US, maxEntrySize) } + ) } internal companion object { diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt index 99ee0a9dec..3b4fb6a167 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/CoreFeatureTest.kt @@ -21,7 +21,6 @@ import com.datadog.android.core.configuration.Configuration import com.datadog.android.core.internal.net.info.BroadcastReceiverNetworkInfoProvider import com.datadog.android.core.internal.net.info.CallbackNetworkInfoProvider import com.datadog.android.core.internal.net.info.NoOpNetworkInfoProvider -import com.datadog.android.core.internal.persistence.datastore.NoOpDataStoreHandler import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig import com.datadog.android.core.internal.persistence.file.batch.BatchFileReaderWriter import com.datadog.android.core.internal.privacy.ConsentProvider @@ -1248,23 +1247,6 @@ internal class CoreFeatureTest { assertThat(testedFeature.ndkCrashHandler).isInstanceOf(NoOpNdkCrashHandler::class.java) } - @Test - fun `M cleanup DataStoreHandler W stop()`() { - // Given - testedFeature.initialize( - appContext.mockInstance, - fakeSdkInstanceId, - fakeConfig, - fakeConsent - ) - - // When - testedFeature.stop() - - // Then - assertThat(testedFeature.dataStoreHandler).isInstanceOf(NoOpDataStoreHandler::class.java) - } - @Test fun `M cleanup app info W stop()`() { // Given diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt index cc3502144e..e5dca5b5a7 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt @@ -9,45 +9,28 @@ package com.datadog.android.core.internal.persistence.file.datastore import com.datadog.android.api.InternalLogger import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler -import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler.Companion.FAILED_TO_SERIALIZE_DATA_ERROR -import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler.Companion.INVALID_NUMBER_OF_BLOCKS_ERROR -import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler.Companion.SAME_BLOCK_APPEARS_TWICE_ERROR -import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper -import com.datadog.android.core.internal.persistence.file.FileReaderWriter -import com.datadog.android.core.internal.persistence.file.deleteSafe -import com.datadog.android.core.internal.persistence.file.existsSafe -import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock -import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader -import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockType +import com.datadog.android.core.internal.persistence.datastore.DatastoreFileReader +import com.datadog.android.core.internal.persistence.datastore.DatastoreFileWriter import com.datadog.android.core.persistence.Serializer import com.datadog.android.core.persistence.datastore.DataStoreCallback import com.datadog.android.core.persistence.datastore.DataStoreContent -import com.datadog.android.core.persistence.datastore.DataStoreHandler.Companion.CURRENT_DATASTORE_VERSION import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.IntForgery import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.Extensions -import org.junit.jupiter.api.io.TempDir import org.mockito.Mock import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.any import org.mockito.kotlin.doAnswer -import org.mockito.kotlin.eq -import org.mockito.kotlin.never import org.mockito.kotlin.verify -import org.mockito.kotlin.verifyNoInteractions import org.mockito.kotlin.whenever import org.mockito.quality.Strictness -import java.io.File -import java.nio.ByteBuffer -import java.util.Locale import java.util.concurrent.ExecutorService import java.util.concurrent.Future import java.util.concurrent.TimeUnit @@ -62,35 +45,26 @@ internal class DataStoreFileHandlerTest { private lateinit var testedDataStoreHandler: DataStoreFileHandler - @Mock - lateinit var mockFileReaderWriter: FileReaderWriter - @Mock lateinit var mockInternalLogger: InternalLogger @Mock - lateinit var mockDataStoreDirectory: File - - @Mock - lateinit var mockDataStoreFile: File + lateinit var mockExecutorService: ExecutorService @Mock - lateinit var mockSerializer: Serializer + lateinit var mockDataStoreFileReader: DatastoreFileReader @Mock lateinit var mockDeserializer: Deserializer @Mock - lateinit var mockTLVBlockFileReader: TLVBlockFileReader - - @Mock - lateinit var mockDataStoreFileHelper: DataStoreFileHelper + lateinit var mockDatastoreFileWriter: DatastoreFileWriter @Mock - lateinit var mockExecutorService: ExecutorService + lateinit var mockSerializer: Serializer - @TempDir - lateinit var mockStorageDir: File + @StringForgery + lateinit var fakeFeatureName: String @StringForgery lateinit var fakeKey: String @@ -98,370 +72,101 @@ internal class DataStoreFileHandlerTest { @StringForgery lateinit var fakeDataString: String - @StringForgery - lateinit var fakeFeatureName: String - - private lateinit var fakeDataBytes: ByteArray - private lateinit var blocksReturned: ArrayList + private lateinit var fileCallback: DataStoreCallback @BeforeEach fun setup() { - fakeDataBytes = fakeDataString.toByteArray(Charsets.UTF_8) - whenever(mockExecutorService.submit(any())) doAnswer { it.getArgument(0).run() StubFuture() } - whenever( - mockDataStoreFileHelper.getDataStoreFile( - featureName = eq(fakeFeatureName), - storageDir = eq(mockStorageDir), - internalLogger = eq(mockInternalLogger), - key = any() - ) - ).thenReturn(mockDataStoreFile) - - whenever(mockDataStoreDirectory.existsSafe(mockInternalLogger)).thenReturn(true) - whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(true) - whenever(mockSerializer.serialize(fakeDataString)).thenReturn(fakeDataString) - whenever(mockDeserializer.deserialize(fakeDataString)).thenReturn(fakeDataBytes) - - val versionBlock = createVersionBlock(true) - val lastUpdateDateBlock = createLastUpdateDateBlock() - val dataBlock = createDataBlock() - blocksReturned = arrayListOf(versionBlock, lastUpdateDateBlock, dataBlock) - whenever(mockTLVBlockFileReader.read(mockDataStoreFile)).thenReturn(blocksReturned) + fileCallback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) {} + override fun onFailure() {} + override fun onNoData() {} + } testedDataStoreHandler = DataStoreFileHandler( executorService = mockExecutorService, - fileReaderWriter = mockFileReaderWriter, - featureName = fakeFeatureName, internalLogger = mockInternalLogger, - storageDir = mockStorageDir, - tlvBlockFileReader = mockTLVBlockFileReader, - dataStoreFileHelper = mockDataStoreFileHelper + dataStoreFileReader = mockDataStoreFileReader, + datastoreFileWriter = mockDatastoreFileWriter ) } - // region read - @Test - fun `M return null W read() { datastore file does not exist }`() { - // Given - whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(false) - var gotNoData = false - + fun `M call dataStoreReader with version 0 W value() { default version }`() { // When testedDataStoreHandler.value( key = fakeKey, deserializer = mockDeserializer, - version = CURRENT_DATASTORE_VERSION, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { - // should not get here - assertThat(1).isEqualTo(2) - } - - override fun onFailure() { - // should not get here - assertThat(1).isEqualTo(2) - } - - override fun onNoData() { - gotNoData = true - } - } + callback = fileCallback ) // Then - assertThat(gotNoData).isTrue() - } - - @Test - fun `M return null W read() { invalid number of blocks }`() { - // Given - blocksReturned.removeLast() - var gotFailure = false - - // When - testedDataStoreHandler.value( + verify(mockDataStoreFileReader).read( key = fakeKey, deserializer = mockDeserializer, - version = CURRENT_DATASTORE_VERSION, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { - // should not get here - assertThat(1).isEqualTo(2) - } - - override fun onNoData() { - // should not get here - assertThat(1).isEqualTo(2) - } - - override fun onFailure() { - gotFailure = true - } - } + version = 0, + callback = fileCallback ) - - // Then - assertThat(gotFailure).isTrue() } @Test - fun `M log error W read() { invalid number of blocks }`() { - // Given - blocksReturned.removeLast() - - val expectedError = INVALID_NUMBER_OF_BLOCKS_ERROR.format(Locale.US, blocksReturned.size) - + fun `M call dataStoreReader W value()`( + @IntForgery fakeVersion: Int + ) { // When testedDataStoreHandler.value( key = fakeKey, deserializer = mockDeserializer, - version = CURRENT_DATASTORE_VERSION, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { - // should not get here - assertThat(1).isEqualTo(2) - } - - override fun onFailure() { - mockInternalLogger.verifyLog( - target = InternalLogger.Target.MAINTAINER, - level = InternalLogger.Level.ERROR, - message = expectedError - ) - } - - override fun onNoData() { - // should not get here - assertThat(1).isEqualTo(2) - } - } - ) - } - - @Test - fun `M return no data W value() { explicit version and versions don't match }`() { - // Given - var noData = false - - // When - testedDataStoreHandler.value( - key = fakeKey, - version = 99, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { - // should not get here - assertThat(1).isEqualTo(2) - } - - override fun onFailure() { - // should not get here - assertThat(1).isEqualTo(2) - } - - override fun onNoData() { - noData = true - } - }, - deserializer = mockDeserializer + version = fakeVersion, + callback = fileCallback ) // Then - assertThat(noData).isTrue() - } - - @Test - fun `M log error W read() { same block appears twice }`() { - // Given - blocksReturned.clear() - blocksReturned.add(createVersionBlock(true)) - blocksReturned.add(createVersionBlock(true)) - blocksReturned.add(createDataBlock()) - - val expectedError = SAME_BLOCK_APPEARS_TWICE_ERROR.format(Locale.US, TLVBlockType.VERSION_CODE) - - // When - testedDataStoreHandler.value( + verify(mockDataStoreFileReader).read( key = fakeKey, deserializer = mockDeserializer, - version = CURRENT_DATASTORE_VERSION, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { - // should not get here - assertThat(1).isEqualTo(2) - } - - override fun onFailure() { - mockInternalLogger.verifyLog( - level = InternalLogger.Level.ERROR, - target = InternalLogger.Target.MAINTAINER, - message = expectedError - ) - } - - override fun onNoData() { - // should not get here - assertThat(1).isEqualTo(2) - } - } + version = fakeVersion, + callback = fileCallback ) } @Test - fun `M return deserialized data W read()`() { - // Given - blocksReturned.clear() - blocksReturned.add(createLastUpdateDateBlock()) - blocksReturned.add(createVersionBlock(true)) - blocksReturned.add(createDataBlock()) - - // When - testedDataStoreHandler.value( - key = fakeKey, - deserializer = mockDeserializer, - version = CURRENT_DATASTORE_VERSION, - callback = object : DataStoreCallback { - - override fun onSuccess(dataStoreContent: DataStoreContent) { - assertThat(dataStoreContent.data).isEqualTo(fakeDataBytes) - } - - override fun onFailure() { - // should not get here - assertThat(1).isEqualTo(2) - } - - override fun onNoData() { - // should not get here - assertThat(1).isEqualTo(2) - } - } - ) - } - - // endregion - - // region write - - @Test - fun `M not write to file W write() { unable to serialize data }`() { - // Given - whenever(mockSerializer.serialize(fakeDataString)).thenReturn(null) - - // When - testedDataStoreHandler.setValue( - key = fakeKey, - serializer = mockSerializer, - data = fakeDataString - ) - - // Then - verifyNoInteractions(mockFileReaderWriter) - } - - @Test - fun `M log error W write() { unable to serialize data }`() { - // Given - whenever(mockSerializer.serialize(fakeDataString)).thenReturn(null) - + fun `M call dataStoreWriter with version 0 W setValue()`( + @IntForgery fakeVersion: Int + ) { // When testedDataStoreHandler.setValue( key = fakeKey, data = fakeDataString, + version = fakeVersion, serializer = mockSerializer ) // Then - mockInternalLogger.verifyLog( - target = InternalLogger.Target.MAINTAINER, - level = InternalLogger.Level.ERROR, - message = FAILED_TO_SERIALIZE_DATA_ERROR - ) - } - - @Test - fun `M write to file W setValue()`() { - // When - testedDataStoreHandler.setValue( + verify(mockDatastoreFileWriter).write( key = fakeKey, data = fakeDataString, - serializer = mockSerializer - ) - - // Then - verify(mockFileReaderWriter).writeData( - eq(mockDataStoreFile), - any(), - eq(false) + serializer = mockSerializer, + version = fakeVersion ) } - // endregion - - // region removeValue - - @Test - fun `M call deleteSafe W removeValue() { file exists }`() { - // Given - whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(true) - - // When - testedDataStoreHandler.removeValue(fakeKey) - - // Then - verify(mockDataStoreFile).deleteSafe(mockInternalLogger) - } - @Test - fun `M not call deleteSafe W removeValue() { file does not exist }`() { - // Given - whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(false) - + fun `M call dataStoreWriter W removeValue()`() { // When - testedDataStoreHandler.removeValue(fakeKey) - - // Then - verify(mockDataStoreFile, never()).deleteSafe(mockInternalLogger) - } - - // endregion - - private fun createVersionBlock(valid: Boolean, newVersion: Int = 0): TLVBlock { - return if (valid) { - TLVBlock( - type = TLVBlockType.VERSION_CODE, - data = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(newVersion).array(), - internalLogger = mockInternalLogger - ) - } else { - TLVBlock( - type = TLVBlockType.VERSION_CODE, - data = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(newVersion - 1).array(), - internalLogger = mockInternalLogger - ) - } - } - - private fun createLastUpdateDateBlock(): TLVBlock = - TLVBlock( - type = TLVBlockType.LAST_UPDATE_DATE, - data = ByteBuffer.allocate(Long.SIZE_BYTES) - .putLong(System.currentTimeMillis()) - .array(), - internalLogger = mockInternalLogger + testedDataStoreHandler.removeValue( + key = fakeKey ) - private fun createDataBlock(dataBytes: ByteArray = fakeDataBytes): TLVBlock = - TLVBlock( - type = TLVBlockType.DATA, - data = dataBytes, - internalLogger = mockInternalLogger + // Then + verify(mockDatastoreFileWriter).delete( + key = fakeKey ) + } private class StubFuture : Future { override fun cancel(mayInterruptIfRunning: Boolean) = diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHelperTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHelperTest.kt new file mode 100644 index 0000000000..0a5131cec3 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHelperTest.kt @@ -0,0 +1,80 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.datastore + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper +import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper.Companion.INVALID_DATASTORE_KEY_FORMAT_EXCEPTION +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.quality.Strictness + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DataStoreFileHelperTest { + private lateinit var testedFileHelper: DataStoreFileHelper + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @BeforeEach + fun setup() { + testedFileHelper = DataStoreFileHelper( + internalLogger = mockInternalLogger + ) + } + + @Test + fun `M log correct exception W logInvalidKeyException()`() { + // When + testedFileHelper.logInvalidKeyException() + + // Then + mockInternalLogger.verifyLog( + level = InternalLogger.Level.WARN, + target = InternalLogger.Target.MAINTAINER, + message = INVALID_DATASTORE_KEY_FORMAT_EXCEPTION + ) + } + + @Test + fun `M return true W isKeyInvalid() { invalid key }`( + @StringForgery(regex = "[^a-zA-Z0-9]") fakeKey: String + ) { + // When + val result = testedFileHelper.isKeyInvalid("$fakeKey/$fakeKey") + + // Then + assertThat(result).isTrue() + } + + @Test + fun `M return false W isKeyInvalid() { valid key }`( + @StringForgery(regex = "[^a-zA-Z0-9]") fakeKey: String + ) { + // When + val result = testedFileHelper.isKeyInvalid(fakeKey) + + // Then + assertThat(result).isFalse() + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt new file mode 100644 index 0000000000..38b0f2bf87 --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt @@ -0,0 +1,365 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.datastore + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.Deserializer +import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper +import com.datadog.android.core.internal.persistence.datastore.DatastoreFileReader +import com.datadog.android.core.internal.persistence.datastore.DatastoreFileReader.Companion.INVALID_NUMBER_OF_BLOCKS_ERROR +import com.datadog.android.core.internal.persistence.datastore.DatastoreFileReader.Companion.UNEXPECTED_BLOCKS_ORDER_ERROR +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader +import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockType +import com.datadog.android.core.persistence.datastore.DataStoreCallback +import com.datadog.android.core.persistence.datastore.DataStoreContent +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File +import java.nio.ByteBuffer +import java.util.Locale + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DataStoreFileReaderTest { + private lateinit var testedDatastoreFileReader: DatastoreFileReader + + @Mock + lateinit var mockDataStoreFileHelper: DataStoreFileHelper + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockTLVBlockFileReader: TLVBlockFileReader + + @Mock + lateinit var mockDataStoreFile: File + + @TempDir + lateinit var mockStorageDir: File + + @Mock + lateinit var mockDataStoreDirectory: File + + @Mock + lateinit var mockDeserializer: Deserializer + + @StringForgery + lateinit var fakeFeatureName: String + + @StringForgery + lateinit var fakeDataString: String + + @StringForgery + lateinit var fakeKey: String + + private lateinit var fakeDataBytes: ByteArray + private lateinit var versionBlock: TLVBlock + private lateinit var lastUpdateDateBlock: TLVBlock + private lateinit var dataBlock: TLVBlock + private lateinit var blocksReturned: ArrayList + + @BeforeEach + fun setup() { + fakeDataBytes = fakeDataString.toByteArray(Charsets.UTF_8) + + whenever( + mockDataStoreFileHelper.getDataStoreFile( + featureName = eq(fakeFeatureName), + storageDir = eq(mockStorageDir), + key = any() + ) + ).thenReturn(mockDataStoreFile) + + whenever(mockDataStoreDirectory.existsSafe(mockInternalLogger)).thenReturn(true) + whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(true) + whenever(mockDeserializer.deserialize(fakeDataString)).thenReturn(fakeDataBytes) + + lastUpdateDateBlock = createLastUpdateDateBlock() + versionBlock = createVersionBlock(true) + dataBlock = createDataBlock() + blocksReturned = arrayListOf(lastUpdateDateBlock, versionBlock, dataBlock) + whenever(mockTLVBlockFileReader.read(mockDataStoreFile)).thenReturn(blocksReturned) + + testedDatastoreFileReader = DatastoreFileReader( + dataStoreFileHelper = mockDataStoreFileHelper, + featureName = fakeFeatureName, + internalLogger = mockInternalLogger, + storageDir = mockStorageDir, + tlvBlockFileReader = mockTLVBlockFileReader + ) + } + + @Test + fun `M return noData W read() { datastore file does not exist }`() { + // Given + whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(false) + var gotNoData = false + + // When + testedDatastoreFileReader.read( + key = fakeKey, + deserializer = mockDeserializer, + version = 0, + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) { + // should not get here + Assertions.assertThat(1).isEqualTo(2) + } + + override fun onFailure() { + // should not get here + Assertions.assertThat(1).isEqualTo(2) + } + + override fun onNoData() { + gotNoData = true + } + } + ) + + // Then + Assertions.assertThat(gotNoData).isTrue() + } + + @Test + fun `M log error W read() { invalid number of blocks }`() { + // Given + blocksReturned.removeLast() + + val expectedError = INVALID_NUMBER_OF_BLOCKS_ERROR.format(Locale.US, blocksReturned.size) + + // When + testedDatastoreFileReader.read( + key = fakeKey, + deserializer = mockDeserializer, + version = 0, + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) { + // should not get here + Assertions.assertThat(1).isEqualTo(2) + } + + override fun onFailure() { + mockInternalLogger.verifyLog( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + message = expectedError + ) + } + + override fun onNoData() { + // should not get here + Assertions.assertThat(1).isEqualTo(2) + } + } + ) + } + + @Test + fun `M return noData W value() { explicit version and versions don't match }`() { + // Given + var noData = false + + // When + testedDatastoreFileReader.read( + key = fakeKey, + version = 99, + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) { + // should not get here + Assertions.assertThat(1).isEqualTo(2) + } + + override fun onFailure() { + // should not get here + Assertions.assertThat(1).isEqualTo(2) + } + + override fun onNoData() { + noData = true + } + }, + deserializer = mockDeserializer + ) + + // Then + Assertions.assertThat(noData).isTrue() + } + + @Test + fun `M return deserialized data W read()`() { + // Given + blocksReturned.clear() + blocksReturned.add(createLastUpdateDateBlock()) + blocksReturned.add(createVersionBlock(true)) + blocksReturned.add(createDataBlock()) + + // When + testedDatastoreFileReader.read( + key = fakeKey, + deserializer = mockDeserializer, + version = 0, + callback = object : DataStoreCallback { + + override fun onSuccess(dataStoreContent: DataStoreContent) { + Assertions.assertThat(dataStoreContent.data).isEqualTo(fakeDataBytes) + } + + override fun onFailure() { + // should not get here + Assertions.assertThat(1).isEqualTo(2) + } + + override fun onNoData() { + // should not get here + Assertions.assertThat(1).isEqualTo(2) + } + } + ) + } + + @Test + fun `M return onFailure W read() { invalid number of blocks }`() { + // Given + blocksReturned.removeLast() + var gotFailure = false + + // When + testedDatastoreFileReader.read( + key = fakeKey, + deserializer = mockDeserializer, + version = 0, + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) { + // should not get here + Assertions.assertThat(1).isEqualTo(2) + } + + override fun onNoData() { + // should not get here + Assertions.assertThat(1).isEqualTo(2) + } + + override fun onFailure() { + gotFailure = true + } + } + ) + + // Then + Assertions.assertThat(gotFailure).isTrue() + } + + @Test + fun `M log unexpectedBlocksOrder error W read() { unexpected block order }`() { + // Given + blocksReturned = arrayListOf(versionBlock, lastUpdateDateBlock, dataBlock) + whenever(mockTLVBlockFileReader.read(mockDataStoreFile)).thenReturn(blocksReturned) + + // When + testedDatastoreFileReader.read( + key = fakeKey, + deserializer = mockDeserializer, + version = 0, + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) { + // should not get here + Assertions.assertThat(1).isEqualTo(2) + } + + override fun onFailure() { + mockInternalLogger.verifyLog( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + message = UNEXPECTED_BLOCKS_ORDER_ERROR + ) + } + + override fun onNoData() { + // should not get here + Assertions.assertThat(1).isEqualTo(2) + } + } + ) + } + + @Test + fun `M log invalid key exception W read() { invalid key }`() { + // Given + whenever(mockDataStoreFileHelper.isKeyInvalid(fakeKey)).thenReturn(true) + + // When + testedDatastoreFileReader.read( + key = fakeKey, + deserializer = mockDeserializer, + version = 0, + callback = object : DataStoreCallback { + override fun onSuccess(dataStoreContent: DataStoreContent) {} + override fun onFailure() {} + override fun onNoData() {} + } + ) + + verify(mockDataStoreFileHelper).logInvalidKeyException() + } + + private fun createVersionBlock(valid: Boolean, newVersion: Int = 0): TLVBlock { + return if (valid) { + TLVBlock( + type = TLVBlockType.VERSION_CODE, + data = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(newVersion).array(), + internalLogger = mockInternalLogger + ) + } else { + TLVBlock( + type = TLVBlockType.VERSION_CODE, + data = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(newVersion - 1).array(), + internalLogger = mockInternalLogger + ) + } + } + + private fun createLastUpdateDateBlock(): TLVBlock = + TLVBlock( + type = TLVBlockType.LAST_UPDATE_DATE, + data = ByteBuffer.allocate(Long.SIZE_BYTES) + .putLong(System.currentTimeMillis()) + .array(), + internalLogger = mockInternalLogger + ) + + private fun createDataBlock(dataBytes: ByteArray = fakeDataBytes): TLVBlock = + TLVBlock( + type = TLVBlockType.DATA, + data = dataBytes, + internalLogger = mockInternalLogger + ) +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileWriterTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileWriterTest.kt new file mode 100644 index 0000000000..90d75900df --- /dev/null +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileWriterTest.kt @@ -0,0 +1,201 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +package com.datadog.android.core.internal.persistence.file.datastore + +import com.datadog.android.api.InternalLogger +import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper +import com.datadog.android.core.internal.persistence.datastore.DatastoreFileWriter +import com.datadog.android.core.internal.persistence.datastore.DatastoreFileWriter.Companion.FAILED_TO_SERIALIZE_DATA_ERROR +import com.datadog.android.core.internal.persistence.file.FileReaderWriter +import com.datadog.android.core.internal.persistence.file.deleteSafe +import com.datadog.android.core.internal.persistence.file.existsSafe +import com.datadog.android.core.persistence.Serializer +import com.datadog.android.utils.forge.Configurator +import com.datadog.android.utils.verifyLog +import fr.xgouchet.elmyr.annotation.StringForgery +import fr.xgouchet.elmyr.junit5.ForgeConfiguration +import fr.xgouchet.elmyr.junit5.ForgeExtension +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.Extensions +import org.junit.jupiter.api.io.TempDir +import org.mockito.Mock +import org.mockito.junit.jupiter.MockitoExtension +import org.mockito.junit.jupiter.MockitoSettings +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoInteractions +import org.mockito.kotlin.whenever +import org.mockito.quality.Strictness +import java.io.File + +@Extensions( + ExtendWith(MockitoExtension::class), + ExtendWith(ForgeExtension::class) +) +@MockitoSettings(strictness = Strictness.LENIENT) +@ForgeConfiguration(Configurator::class) +internal class DataStoreFileWriterTest { + private lateinit var testedDatastoreFileWriter: DatastoreFileWriter + + @Mock + lateinit var mockDataStoreFileHelper: DataStoreFileHelper + + @Mock + lateinit var mockInternalLogger: InternalLogger + + @Mock + lateinit var mockFileReaderWriter: FileReaderWriter + + @Mock + lateinit var mockSerializer: Serializer + + @Mock + lateinit var mockDataStoreDirectory: File + + @Mock + lateinit var mockDataStoreFile: File + + @TempDir + lateinit var mockStorageDir: File + + @StringForgery + lateinit var fakeFeatureName: String + + @StringForgery + lateinit var fakeDataString: String + + @StringForgery + lateinit var fakeKey: String + + private lateinit var fakeDataBytes: ByteArray + + @BeforeEach + fun setup() { + fakeDataBytes = fakeDataString.toByteArray(Charsets.UTF_8) + + whenever( + mockDataStoreFileHelper.getDataStoreFile( + featureName = eq(fakeFeatureName), + storageDir = eq(mockStorageDir), + key = any() + ) + ).thenReturn(mockDataStoreFile) + + whenever(mockDataStoreDirectory.existsSafe(mockInternalLogger)).thenReturn(true) + whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(true) + whenever(mockSerializer.serialize(fakeDataString)).thenReturn(fakeDataString) + + testedDatastoreFileWriter = DatastoreFileWriter( + dataStoreFileHelper = mockDataStoreFileHelper, + featureName = fakeFeatureName, + internalLogger = mockInternalLogger, + storageDir = mockStorageDir, + fileReaderWriter = mockFileReaderWriter + ) + } + + @Test + fun `M not write to file W write() { unable to serialize data }`() { + // Given + whenever(mockSerializer.serialize(fakeDataString)).thenReturn(null) + + // When + testedDatastoreFileWriter.write( + key = fakeKey, + serializer = mockSerializer, + data = fakeDataString, + version = 0 + ) + + // Then + verifyNoInteractions(mockFileReaderWriter) + } + + @Test + fun `M log error W write() { unable to serialize data }`() { + // Given + whenever(mockSerializer.serialize(fakeDataString)).thenReturn(null) + + // When + testedDatastoreFileWriter.write( + key = fakeKey, + data = fakeDataString, + serializer = mockSerializer, + version = 0 + ) + + // Then + mockInternalLogger.verifyLog( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + message = FAILED_TO_SERIALIZE_DATA_ERROR + ) + } + + @Test + fun `M write to file W setValue()`() { + // When + testedDatastoreFileWriter.write( + key = fakeKey, + data = fakeDataString, + serializer = mockSerializer, + version = 0 + ) + + // Then + verify(mockFileReaderWriter).writeData( + eq(mockDataStoreFile), + any(), + eq(false) + ) + } + + @Test + fun `M call deleteSafe W removeValue() { file exists }`() { + // Given + whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(true) + + // When + testedDatastoreFileWriter.delete(fakeKey) + + // Then + verify(mockDataStoreFile).deleteSafe(mockInternalLogger) + } + + @Test + fun `M not call deleteSafe W removeValue() { file does not exist }`() { + // Given + whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(false) + + // When + testedDatastoreFileWriter.delete(fakeKey) + + // Then + verify(mockDataStoreFile, never()).deleteSafe(mockInternalLogger) + } + + @Test + fun `M log invalid key exception W read() { invalid key }`() { + // Given + whenever(mockDataStoreFileHelper.isKeyInvalid(fakeKey)).thenReturn(true) + + // When + testedDatastoreFileWriter.write( + key = fakeKey, + serializer = mockSerializer, + data = fakeDataString, + version = 0 + ) + + // Then + verify(mockDataStoreFileHelper).logInvalidKeyException() + } +} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt index 72baa7c0d7..92d4c60f85 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt @@ -90,7 +90,7 @@ internal class TLVBlockTest { } @Test - fun `M log error W serialize() { exceeds max data length }`( + fun `M log error W serialize() { exceeds max entry size }`( @StringForgery fakeString: String, @IntForgery(min = 0, max = 10) fakeTypeAsInt: Int ) { From 1465862011764c91040c175618b9acd6f9acf737 Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Sun, 2 Jun 2024 19:33:15 +0200 Subject: [PATCH 09/14] RUM-4098: Remove update date from tlv type --- dd-sdk-android-core/api/apiSurface | 2 +- .../api/dd-sdk-android-core.api | 12 ++++----- .../datastore/DatastoreFileReader.kt | 11 +++----- .../datastore/DatastoreFileWriter.kt | 17 ++---------- .../persistence/tlvformat/TLVBlockType.kt | 5 ++-- .../datastore/DataStoreCallback.kt | 2 +- .../persistence/datastore/DataStoreContent.kt | 2 -- .../file/datastore/DataStoreFileReaderTest.kt | 16 ++--------- .../tlvformat/TLVBlockFileReaderTest.kt | 27 +++---------------- .../persistence/tlvformat/TLVBlockTypeTest.kt | 4 +-- 10 files changed, 23 insertions(+), 75 deletions(-) diff --git a/dd-sdk-android-core/api/apiSurface b/dd-sdk-android-core/api/apiSurface index e31a3994f0..6478fc41e7 100644 --- a/dd-sdk-android-core/api/apiSurface +++ b/dd-sdk-android-core/api/apiSurface @@ -296,7 +296,7 @@ interface com.datadog.android.core.persistence.datastore.DataStoreCallback - constructor(Long, Int, T?) + constructor(Int, T?) interface com.datadog.android.core.persistence.datastore.DataStoreHandler fun setValue(String, T, Int = 0, com.datadog.android.core.persistence.Serializer) fun value(String, Int = 0, DataStoreCallback, com.datadog.android.core.internal.persistence.Deserializer) diff --git a/dd-sdk-android-core/api/dd-sdk-android-core.api b/dd-sdk-android-core/api/dd-sdk-android-core.api index c2439c6d1b..de20031ddc 100644 --- a/dd-sdk-android-core/api/dd-sdk-android-core.api +++ b/dd-sdk-android-core/api/dd-sdk-android-core.api @@ -798,15 +798,13 @@ public abstract interface class com/datadog/android/core/persistence/datastore/D } public final class com/datadog/android/core/persistence/datastore/DataStoreContent { - public fun (JILjava/lang/Object;)V - public final fun component1 ()J - public final fun component2 ()I - public final fun component3 ()Ljava/lang/Object; - public final fun copy (JILjava/lang/Object;)Lcom/datadog/android/core/persistence/datastore/DataStoreContent; - public static synthetic fun copy$default (Lcom/datadog/android/core/persistence/datastore/DataStoreContent;JILjava/lang/Object;ILjava/lang/Object;)Lcom/datadog/android/core/persistence/datastore/DataStoreContent; + public fun (ILjava/lang/Object;)V + public final fun component1 ()I + public final fun component2 ()Ljava/lang/Object; + public final fun copy (ILjava/lang/Object;)Lcom/datadog/android/core/persistence/datastore/DataStoreContent; + public static synthetic fun copy$default (Lcom/datadog/android/core/persistence/datastore/DataStoreContent;ILjava/lang/Object;ILjava/lang/Object;)Lcom/datadog/android/core/persistence/datastore/DataStoreContent; public fun equals (Ljava/lang/Object;)Z public final fun getData ()Ljava/lang/Object; - public final fun getLastUpdateDate ()J public final fun getVersionCode ()I public fun hashCode ()I public fun toString ()Ljava/lang/String; diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt index 710c54554d..ab6d87aa12 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt @@ -10,7 +10,6 @@ import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.internal.persistence.datastore.ext.toInt -import com.datadog.android.core.internal.persistence.datastore.ext.toLong import com.datadog.android.core.internal.persistence.file.existsSafe import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader @@ -89,19 +88,17 @@ internal class DatastoreFileReader( deserializer: Deserializer, tlvBlocks: List ): DataStoreContent? { - if (tlvBlocks[0].type != TLVBlockType.LAST_UPDATE_DATE && - tlvBlocks[1].type != TLVBlockType.VERSION_CODE + if (tlvBlocks[0].type != TLVBlockType.VERSION_CODE && + tlvBlocks[1].type != TLVBlockType.DATA ) { logBlocksInUnexpectedBlocksOrderError() return null } - val lastUpdateBlock = tlvBlocks[0] - val versionCodeBlock = tlvBlocks[1] - val dataBlock = tlvBlocks[2] + val versionCodeBlock = tlvBlocks[0] + val dataBlock = tlvBlocks[1] return DataStoreContent( - lastUpdateDate = lastUpdateBlock.data.toLong(), versionCode = versionCodeBlock.data.toInt(), data = deserializer.deserialize(String(dataBlock.data)) ) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt index ad729fd826..834feab400 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt @@ -43,16 +43,15 @@ internal class DatastoreFileWriter( key = key ) - val lastUpdateBlock = getLastUpdateDateBlock() val versionCodeBlock = getVersionCodeBlock(version) val dataBlock = getDataBlock(data, serializer) // failed to serialize one or more blocks - if (lastUpdateBlock == null || versionCodeBlock == null || dataBlock == null) { + if (versionCodeBlock == null || dataBlock == null) { return } - val dataToWrite = listOf(lastUpdateBlock, versionCodeBlock, dataBlock).join( + val dataToWrite = listOf(versionCodeBlock, dataBlock).join( separator = byteArrayOf(), internalLogger = internalLogger ) @@ -102,18 +101,6 @@ internal class DatastoreFileWriter( return dataBlock.serialize() } - private fun getLastUpdateDateBlock(): ByteArray? { - val now = System.currentTimeMillis() - val lastUpdateDateByteArray = now.toByteArray() - val lastUpdateDateBlock = TLVBlock( - type = TLVBlockType.LAST_UPDATE_DATE, - data = lastUpdateDateByteArray, - internalLogger = internalLogger - ) - - return lastUpdateDateBlock.serialize() - } - private fun getVersionCodeBlock(version: Int): ByteArray? { val versionCodeByteArray = version.toByteArray() val versionBlock = TLVBlock( diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockType.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockType.kt index 55bd06c725..163df94f1a 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockType.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockType.kt @@ -7,9 +7,8 @@ package com.datadog.android.core.internal.persistence.tlvformat internal enum class TLVBlockType(val rawValue: UShort) { - LAST_UPDATE_DATE(0x00u), - VERSION_CODE(0x01u), - DATA(0x02u); + VERSION_CODE(0x00u), + DATA(0x01u); companion object { private val map = values().associateBy { it.rawValue } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreCallback.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreCallback.kt index 02c1feb1bf..c174861d3f 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreCallback.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreCallback.kt @@ -14,7 +14,7 @@ interface DataStoreCallback { /** * Called on successfully fetching data from the datastore. * - * @param dataStoreContent contains the datastore data, version and lastUpdateDate. + * @param dataStoreContent contains the datastore data and version. */ fun onSuccess(dataStoreContent: DataStoreContent) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreContent.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreContent.kt index 25945bc0cf..e6e8c2590e 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreContent.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreContent.kt @@ -10,12 +10,10 @@ package com.datadog.android.core.persistence.datastore * Datastore entry contents and metadata. * * @param T type of data used by this entry in the datastore. - * @property lastUpdateDate date when the entry was written. * @property versionCode version used by the entry. * @property data content of the entry. */ data class DataStoreContent( - val lastUpdateDate: Long, val versionCode: Int, val data: T? ) diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt index 38b0f2bf87..9aa2225923 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt @@ -82,7 +82,6 @@ internal class DataStoreFileReaderTest { private lateinit var fakeDataBytes: ByteArray private lateinit var versionBlock: TLVBlock - private lateinit var lastUpdateDateBlock: TLVBlock private lateinit var dataBlock: TLVBlock private lateinit var blocksReturned: ArrayList @@ -102,10 +101,9 @@ internal class DataStoreFileReaderTest { whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(true) whenever(mockDeserializer.deserialize(fakeDataString)).thenReturn(fakeDataBytes) - lastUpdateDateBlock = createLastUpdateDateBlock() versionBlock = createVersionBlock(true) dataBlock = createDataBlock() - blocksReturned = arrayListOf(lastUpdateDateBlock, versionBlock, dataBlock) + blocksReturned = arrayListOf(versionBlock, dataBlock) whenever(mockTLVBlockFileReader.read(mockDataStoreFile)).thenReturn(blocksReturned) testedDatastoreFileReader = DatastoreFileReader( @@ -218,7 +216,6 @@ internal class DataStoreFileReaderTest { fun `M return deserialized data W read()`() { // Given blocksReturned.clear() - blocksReturned.add(createLastUpdateDateBlock()) blocksReturned.add(createVersionBlock(true)) blocksReturned.add(createDataBlock()) @@ -281,7 +278,7 @@ internal class DataStoreFileReaderTest { @Test fun `M log unexpectedBlocksOrder error W read() { unexpected block order }`() { // Given - blocksReturned = arrayListOf(versionBlock, lastUpdateDateBlock, dataBlock) + blocksReturned = arrayListOf(dataBlock, versionBlock) whenever(mockTLVBlockFileReader.read(mockDataStoreFile)).thenReturn(blocksReturned) // When @@ -347,15 +344,6 @@ internal class DataStoreFileReaderTest { } } - private fun createLastUpdateDateBlock(): TLVBlock = - TLVBlock( - type = TLVBlockType.LAST_UPDATE_DATE, - data = ByteBuffer.allocate(Long.SIZE_BYTES) - .putLong(System.currentTimeMillis()) - .array(), - internalLogger = mockInternalLogger - ) - private fun createDataBlock(dataBytes: ByteArray = fakeDataBytes): TLVBlock = TLVBlock( type = TLVBlockType.DATA, diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt index e5747cd411..0d7e87023d 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt @@ -7,7 +7,6 @@ package com.datadog.android.core.internal.persistence.tlvformat import com.datadog.android.api.InternalLogger -import com.datadog.android.core.internal.persistence.datastore.ext.toByteArray import com.datadog.android.core.internal.persistence.file.FileReaderWriter import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader.Companion.FAILED_TO_DESERIALIZE_ERROR import com.datadog.android.utils.forge.Configurator @@ -54,17 +53,15 @@ internal class TLVBlockFileReaderTest { @StringForgery(regex = "^(\\w{3})\$") // a minimal number of chars to avoid flakiness private lateinit var fakeDataString: String - private lateinit var fakeUpdateBytes: ByteArray private lateinit var fakeVersionBytes: ByteArray private lateinit var fakeDataBytes: ByteArray private lateinit var fakeBufferBytes: ByteArray @BeforeEach fun setup(@IntForgery(min = 0, max = 10) fakeVersion: Int) { - val lastUpdateBytes = createLastUpdateBytes() val versionBytes = createVersionBytes(fakeVersion) val dataBytes = createDataBytes() - val dataToWrite = lastUpdateBytes + versionBytes + dataBytes + val dataToWrite = versionBytes + dataBytes whenever(mockFileReaderWriter.readData(mockFile)).thenReturn(dataToWrite) @@ -118,13 +115,10 @@ internal class TLVBlockFileReaderTest { val tlvArray = testedReader.read(file = mockFile) // Then - assertThat(tlvArray.size).isEqualTo(3) - val lastUpdateObject = tlvArray[0] - val versionObject = tlvArray[1] - val dataObject = tlvArray[2] + assertThat(tlvArray.size).isEqualTo(2) + val versionObject = tlvArray[0] + val dataObject = tlvArray[1] - assertThat(lastUpdateObject.type).isEqualTo(TLVBlockType.LAST_UPDATE_DATE) - assertThat(lastUpdateObject.data).isEqualTo(fakeUpdateBytes) assertThat(versionObject.type).isEqualTo(TLVBlockType.VERSION_CODE) assertThat(versionObject.data).isEqualTo(fakeVersionBytes) assertThat(dataObject.type).isEqualTo(TLVBlockType.DATA) @@ -168,19 +162,6 @@ internal class TLVBlockFileReaderTest { ) } - private fun createLastUpdateBytes(): ByteArray { - val now = System.currentTimeMillis() - fakeUpdateBytes = now.toByteArray() - val lastUpdateType = TLVBlockType.LAST_UPDATE_DATE.rawValue.toShort() - - return ByteBuffer - .allocate(fakeUpdateBytes.size + Int.SIZE_BYTES + Short.SIZE_BYTES) - .putShort(lastUpdateType) - .putInt(fakeUpdateBytes.size) - .put(fakeUpdateBytes) - .array() - } - private fun createVersionBytes(fakeVersion: Int): ByteArray { val versionType = TLVBlockType.VERSION_CODE.rawValue.toShort() fakeVersionBytes = ByteBuffer.allocate(Int.SIZE_BYTES).putInt(fakeVersion).array() diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTypeTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTypeTest.kt index e475bc30be..804c551a7b 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTypeTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTypeTest.kt @@ -27,10 +27,10 @@ internal class TLVBlockTypeTest { @Test fun `M return type value W fromValue() { existing value }`() { // When - val shortValue = TLVBlockType.fromValue(TLVBlockType.LAST_UPDATE_DATE.rawValue) + val shortValue = TLVBlockType.fromValue(TLVBlockType.VERSION_CODE.rawValue) // Then - assertThat(shortValue).isEqualTo(TLVBlockType.LAST_UPDATE_DATE) + assertThat(shortValue).isEqualTo(TLVBlockType.VERSION_CODE) } @Test From 1c3f646121d086aa5963566f1115d6e0ad3f99bf Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Mon, 3 Jun 2024 16:05:34 +0200 Subject: [PATCH 10/14] RUM-4098: Remove datastore key format validation --- .../datastore/DataStoreFileHelper.kt | 14 ---- .../datastore/DatastoreFileReader.kt | 5 -- .../datastore/DatastoreFileWriter.kt | 10 --- .../file/datastore/DataStoreFileHelperTest.kt | 80 ------------------- .../file/datastore/DataStoreFileReaderTest.kt | 21 ----- .../file/datastore/DataStoreFileWriterTest.kt | 17 ---- 6 files changed, 147 deletions(-) delete mode 100644 dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHelperTest.kt diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt index 5dd16e7f7c..a71ad5001f 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt @@ -52,21 +52,7 @@ internal class DataStoreFileHelper( return dataStoreDirectory } - internal fun isKeyInvalid(key: String): Boolean { - return key.contains("/") - } - - internal fun logInvalidKeyException() { - internalLogger.log( - level = InternalLogger.Level.WARN, - target = InternalLogger.Target.MAINTAINER, - messageBuilder = { INVALID_DATASTORE_KEY_FORMAT_EXCEPTION } - ) - } - internal companion object { internal const val DATASTORE_FOLDER_NAME = "datastore_v%s" - internal const val INVALID_DATASTORE_KEY_FORMAT_EXCEPTION = - "Datastore key must not be a path!" } } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt index ab6d87aa12..0c7a3fce56 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt @@ -33,11 +33,6 @@ internal class DatastoreFileReader( version: Int, callback: DataStoreCallback ) { - if (dataStoreFileHelper.isKeyInvalid(key)) { - dataStoreFileHelper.logInvalidKeyException() - return - } - val datastoreFile = dataStoreFileHelper.getDataStoreFile( featureName = featureName, storageDir = storageDir, diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt index 834feab400..5162ba6052 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt @@ -32,11 +32,6 @@ internal class DatastoreFileWriter( serializer: Serializer, version: Int ) { - if (dataStoreFileHelper.isKeyInvalid(key)) { - dataStoreFileHelper.logInvalidKeyException() - return - } - val datastoreFile = dataStoreFileHelper.getDataStoreFile( featureName = featureName, storageDir = storageDir, @@ -65,11 +60,6 @@ internal class DatastoreFileWriter( @WorkerThread internal fun delete(key: String) { - if (dataStoreFileHelper.isKeyInvalid(key)) { - dataStoreFileHelper.logInvalidKeyException() - return - } - val datastoreFile = dataStoreFileHelper.getDataStoreFile( featureName = featureName, storageDir = storageDir, diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHelperTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHelperTest.kt deleted file mode 100644 index 0a5131cec3..0000000000 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHelperTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. - * This product includes software developed at Datadog (https://www.datadoghq.com/). - * Copyright 2016-Present Datadog, Inc. - */ - -package com.datadog.android.core.internal.persistence.file.datastore - -import com.datadog.android.api.InternalLogger -import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper -import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper.Companion.INVALID_DATASTORE_KEY_FORMAT_EXCEPTION -import com.datadog.android.utils.forge.Configurator -import com.datadog.android.utils.verifyLog -import fr.xgouchet.elmyr.annotation.StringForgery -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.Mock -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness - -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) -internal class DataStoreFileHelperTest { - private lateinit var testedFileHelper: DataStoreFileHelper - - @Mock - lateinit var mockInternalLogger: InternalLogger - - @BeforeEach - fun setup() { - testedFileHelper = DataStoreFileHelper( - internalLogger = mockInternalLogger - ) - } - - @Test - fun `M log correct exception W logInvalidKeyException()`() { - // When - testedFileHelper.logInvalidKeyException() - - // Then - mockInternalLogger.verifyLog( - level = InternalLogger.Level.WARN, - target = InternalLogger.Target.MAINTAINER, - message = INVALID_DATASTORE_KEY_FORMAT_EXCEPTION - ) - } - - @Test - fun `M return true W isKeyInvalid() { invalid key }`( - @StringForgery(regex = "[^a-zA-Z0-9]") fakeKey: String - ) { - // When - val result = testedFileHelper.isKeyInvalid("$fakeKey/$fakeKey") - - // Then - assertThat(result).isTrue() - } - - @Test - fun `M return false W isKeyInvalid() { valid key }`( - @StringForgery(regex = "[^a-zA-Z0-9]") fakeKey: String - ) { - // When - val result = testedFileHelper.isKeyInvalid(fakeKey) - - // Then - assertThat(result).isFalse() - } -} diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt index 9aa2225923..fa76a26a73 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt @@ -34,7 +34,6 @@ import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.any import org.mockito.kotlin.eq -import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import java.io.File @@ -308,26 +307,6 @@ internal class DataStoreFileReaderTest { ) } - @Test - fun `M log invalid key exception W read() { invalid key }`() { - // Given - whenever(mockDataStoreFileHelper.isKeyInvalid(fakeKey)).thenReturn(true) - - // When - testedDatastoreFileReader.read( - key = fakeKey, - deserializer = mockDeserializer, - version = 0, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) {} - override fun onFailure() {} - override fun onNoData() {} - } - ) - - verify(mockDataStoreFileHelper).logInvalidKeyException() - } - private fun createVersionBlock(valid: Boolean, newVersion: Int = 0): TLVBlock { return if (valid) { TLVBlock( diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileWriterTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileWriterTest.kt index 90d75900df..24ac613545 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileWriterTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileWriterTest.kt @@ -181,21 +181,4 @@ internal class DataStoreFileWriterTest { // Then verify(mockDataStoreFile, never()).deleteSafe(mockInternalLogger) } - - @Test - fun `M log invalid key exception W read() { invalid key }`() { - // Given - whenever(mockDataStoreFileHelper.isKeyInvalid(fakeKey)).thenReturn(true) - - // When - testedDatastoreFileWriter.write( - key = fakeKey, - serializer = mockSerializer, - data = fakeDataString, - version = 0 - ) - - // Then - verify(mockDataStoreFileHelper).logInvalidKeyException() - } } From 8a8668e5883609fb0c004aa5276de61208c91a4c Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:39:19 +0200 Subject: [PATCH 11/14] RUM-4098: Move datastore interfaces to api --- dd-sdk-android-core/api/apiSurface | 21 ++++---- .../api/dd-sdk-android-core.api | 49 +++++++++---------- .../android/api/feature/FeatureScope.kt | 2 +- .../storage}/datastore/DataStoreCallback.kt | 17 +++---- .../storage}/datastore/DataStoreHandler.kt | 2 +- .../android/core/internal/SdkFeature.kt | 2 +- .../datastore/DataStoreFileHandler.kt | 7 +-- .../datastore/DataStoreFileHelper.kt | 2 +- .../datastore/DatastoreFileReader.kt | 6 +-- .../datastore/NoOpDataStoreHandler.kt | 4 +- .../android/core/internal/SdkFeatureTest.kt | 2 +- .../datastore/DataStoreFileHandlerTest.kt | 2 +- .../file/datastore/DataStoreFileReaderTest.kt | 2 +- 13 files changed, 55 insertions(+), 63 deletions(-) rename dd-sdk-android-core/src/main/kotlin/com/datadog/android/{core/persistence => api/storage}/datastore/DataStoreCallback.kt (50%) rename dd-sdk-android-core/src/main/kotlin/com/datadog/android/{core/persistence => api/storage}/datastore/DataStoreHandler.kt (97%) diff --git a/dd-sdk-android-core/api/apiSurface b/dd-sdk-android-core/api/apiSurface index 6478fc41e7..bc0cc1ea11 100644 --- a/dd-sdk-android-core/api/apiSurface +++ b/dd-sdk-android-core/api/apiSurface @@ -102,7 +102,7 @@ interface com.datadog.android.api.feature.FeatureContextUpdateReceiver interface com.datadog.android.api.feature.FeatureEventReceiver fun onReceive(Any) interface com.datadog.android.api.feature.FeatureScope - var dataStore: com.datadog.android.core.persistence.datastore.DataStoreHandler + var dataStore: com.datadog.android.api.storage.datastore.DataStoreHandler fun withWriteContext(Boolean = false, (com.datadog.android.api.context.DatadogContext, com.datadog.android.api.storage.EventBatchWriter) -> Unit) fun sendEvent(Any) fun unwrap(): T @@ -152,6 +152,15 @@ data class com.datadog.android.api.storage.RawBatchEvent constructor(ByteArray, ByteArray = EMPTY_BYTE_ARRAY) override fun equals(Any?): Boolean override fun hashCode(): Int +interface com.datadog.android.api.storage.datastore.DataStoreCallback + fun onSuccess(com.datadog.android.core.persistence.datastore.DataStoreContent?) + fun onFailure() +interface com.datadog.android.api.storage.datastore.DataStoreHandler + fun setValue(String, T, Int = 0, com.datadog.android.core.persistence.Serializer) + fun value(String, Int = 0, DataStoreCallback, com.datadog.android.core.internal.persistence.Deserializer) + fun removeValue(String) + companion object + const val CURRENT_DATASTORE_VERSION: Int interface com.datadog.android.core.InternalSdkCore : com.datadog.android.api.feature.FeatureSdkCore val networkInfo: com.datadog.android.api.context.NetworkInfo val trackingConsent: com.datadog.android.privacy.TrackingConsent @@ -291,18 +300,8 @@ interface com.datadog.android.core.persistence.Serializer fun serialize(T): String? companion object fun Serializer.serializeToByteArray(T, com.datadog.android.api.InternalLogger): ByteArray? -interface com.datadog.android.core.persistence.datastore.DataStoreCallback - fun onSuccess(DataStoreContent) - fun onFailure() - fun onNoData() data class com.datadog.android.core.persistence.datastore.DataStoreContent constructor(Int, T?) -interface com.datadog.android.core.persistence.datastore.DataStoreHandler - fun setValue(String, T, Int = 0, com.datadog.android.core.persistence.Serializer) - fun value(String, Int = 0, DataStoreCallback, com.datadog.android.core.internal.persistence.Deserializer) - fun removeValue(String) - companion object - const val CURRENT_DATASTORE_VERSION: Int class com.datadog.android.core.sampling.RateBasedSampler : Sampler constructor(() -> Float) constructor(Float) diff --git a/dd-sdk-android-core/api/dd-sdk-android-core.api b/dd-sdk-android-core/api/dd-sdk-android-core.api index de20031ddc..da7edccc1b 100644 --- a/dd-sdk-android-core/api/dd-sdk-android-core.api +++ b/dd-sdk-android-core/api/dd-sdk-android-core.api @@ -325,9 +325,9 @@ public abstract interface class com/datadog/android/api/feature/FeatureEventRece } public abstract interface class com/datadog/android/api/feature/FeatureScope { - public abstract fun getDataStore ()Lcom/datadog/android/core/persistence/datastore/DataStoreHandler; + public abstract fun getDataStore ()Lcom/datadog/android/api/storage/datastore/DataStoreHandler; public abstract fun sendEvent (Ljava/lang/Object;)V - public abstract fun setDataStore (Lcom/datadog/android/core/persistence/datastore/DataStoreHandler;)V + public abstract fun setDataStore (Lcom/datadog/android/api/storage/datastore/DataStoreHandler;)V public abstract fun unwrap ()Lcom/datadog/android/api/feature/Feature; public abstract fun withWriteContext (ZLkotlin/jvm/functions/Function2;)V } @@ -464,6 +464,28 @@ public final class com/datadog/android/api/storage/RawBatchEvent { public fun toString ()Ljava/lang/String; } +public abstract interface class com/datadog/android/api/storage/datastore/DataStoreCallback { + public abstract fun onFailure ()V + public abstract fun onSuccess (Lcom/datadog/android/core/persistence/datastore/DataStoreContent;)V +} + +public abstract interface class com/datadog/android/api/storage/datastore/DataStoreHandler { + public static final field CURRENT_DATASTORE_VERSION I + public static final field Companion Lcom/datadog/android/api/storage/datastore/DataStoreHandler$Companion; + public abstract fun removeValue (Ljava/lang/String;)V + public abstract fun setValue (Ljava/lang/String;Ljava/lang/Object;ILcom/datadog/android/core/persistence/Serializer;)V + public abstract fun value (Ljava/lang/String;ILcom/datadog/android/api/storage/datastore/DataStoreCallback;Lcom/datadog/android/core/internal/persistence/Deserializer;)V +} + +public final class com/datadog/android/api/storage/datastore/DataStoreHandler$Companion { + public static final field CURRENT_DATASTORE_VERSION I +} + +public final class com/datadog/android/api/storage/datastore/DataStoreHandler$DefaultImpls { + public static synthetic fun setValue$default (Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Ljava/lang/String;Ljava/lang/Object;ILcom/datadog/android/core/persistence/Serializer;ILjava/lang/Object;)V + public static synthetic fun value$default (Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Ljava/lang/String;ILcom/datadog/android/api/storage/datastore/DataStoreCallback;Lcom/datadog/android/core/internal/persistence/Deserializer;ILjava/lang/Object;)V +} + public abstract interface class com/datadog/android/core/InternalSdkCore : com/datadog/android/api/feature/FeatureSdkCore { public abstract fun deleteLastViewEvent ()V public abstract fun getAllFeatures ()Ljava/util/List; @@ -791,12 +813,6 @@ public final class com/datadog/android/core/persistence/SerializerKt { public static final fun serializeToByteArray (Lcom/datadog/android/core/persistence/Serializer;Ljava/lang/Object;Lcom/datadog/android/api/InternalLogger;)[B } -public abstract interface class com/datadog/android/core/persistence/datastore/DataStoreCallback { - public abstract fun onFailure ()V - public abstract fun onNoData ()V - public abstract fun onSuccess (Lcom/datadog/android/core/persistence/datastore/DataStoreContent;)V -} - public final class com/datadog/android/core/persistence/datastore/DataStoreContent { public fun (ILjava/lang/Object;)V public final fun component1 ()I @@ -810,23 +826,6 @@ public final class com/datadog/android/core/persistence/datastore/DataStoreConte public fun toString ()Ljava/lang/String; } -public abstract interface class com/datadog/android/core/persistence/datastore/DataStoreHandler { - public static final field CURRENT_DATASTORE_VERSION I - public static final field Companion Lcom/datadog/android/core/persistence/datastore/DataStoreHandler$Companion; - public abstract fun removeValue (Ljava/lang/String;)V - public abstract fun setValue (Ljava/lang/String;Ljava/lang/Object;ILcom/datadog/android/core/persistence/Serializer;)V - public abstract fun value (Ljava/lang/String;ILcom/datadog/android/core/persistence/datastore/DataStoreCallback;Lcom/datadog/android/core/internal/persistence/Deserializer;)V -} - -public final class com/datadog/android/core/persistence/datastore/DataStoreHandler$Companion { - public static final field CURRENT_DATASTORE_VERSION I -} - -public final class com/datadog/android/core/persistence/datastore/DataStoreHandler$DefaultImpls { - public static synthetic fun setValue$default (Lcom/datadog/android/core/persistence/datastore/DataStoreHandler;Ljava/lang/String;Ljava/lang/Object;ILcom/datadog/android/core/persistence/Serializer;ILjava/lang/Object;)V - public static synthetic fun value$default (Lcom/datadog/android/core/persistence/datastore/DataStoreHandler;Ljava/lang/String;ILcom/datadog/android/core/persistence/datastore/DataStoreCallback;Lcom/datadog/android/core/internal/persistence/Deserializer;ILjava/lang/Object;)V -} - public final class com/datadog/android/core/sampling/RateBasedSampler : com/datadog/android/core/sampling/Sampler { public static final field SAMPLE_ALL_RATE F public fun (D)V diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt index 295ffd1d4b..d4429a5d76 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt @@ -9,7 +9,7 @@ package com.datadog.android.api.feature import androidx.annotation.AnyThread import com.datadog.android.api.context.DatadogContext import com.datadog.android.api.storage.EventBatchWriter -import com.datadog.android.core.persistence.datastore.DataStoreHandler +import com.datadog.android.api.storage.datastore.DataStoreHandler /** * Represents a Datadog feature. diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreCallback.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreCallback.kt similarity index 50% rename from dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreCallback.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreCallback.kt index c174861d3f..4d7bb48a9d 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreCallback.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreCallback.kt @@ -4,7 +4,9 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.core.persistence.datastore +package com.datadog.android.api.storage.datastore + +import com.datadog.android.core.persistence.datastore.DataStoreContent /** * Callback for asynchronous operations on the datastore. @@ -12,19 +14,14 @@ package com.datadog.android.core.persistence.datastore interface DataStoreCallback { /** - * Called on successfully fetching data from the datastore. + * Triggered on successfully fetching data from the datastore. * - * @param dataStoreContent contains the datastore data and version. + * @param dataStoreContent contains the datastore content if there was data to fetch. */ - fun onSuccess(dataStoreContent: DataStoreContent) + fun onSuccess(dataStoreContent: DataStoreContent?) /** - * Called when an exception occurred getting data from the datastore. + * Triggered when an exception occurred getting data from the datastore. */ fun onFailure() - - /** - * Called when no data is found for the given key. - */ - fun onNoData() } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreHandler.kt similarity index 97% rename from dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreHandler.kt rename to dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreHandler.kt index eba4f70e1e..5516dc4a7b 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/persistence/datastore/DataStoreHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreHandler.kt @@ -4,7 +4,7 @@ * Copyright 2016-Present Datadog, Inc. */ -package com.datadog.android.core.persistence.datastore +package com.datadog.android.api.storage.datastore import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.persistence.Serializer diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt index 21b8db0bde..bbb5b5cabd 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/SdkFeature.kt @@ -20,6 +20,7 @@ import com.datadog.android.api.feature.StorageBackedFeature import com.datadog.android.api.net.RequestFactory import com.datadog.android.api.storage.EventBatchWriter import com.datadog.android.api.storage.FeatureStorageConfiguration +import com.datadog.android.api.storage.datastore.DataStoreHandler import com.datadog.android.core.internal.configuration.DataUploadConfiguration import com.datadog.android.core.internal.data.upload.DataFlusher import com.datadog.android.core.internal.data.upload.DataOkHttpUploader @@ -50,7 +51,6 @@ import com.datadog.android.core.internal.persistence.file.advanced.FeatureFileOr import com.datadog.android.core.internal.persistence.file.batch.BatchFileReaderWriter import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader import com.datadog.android.core.persistence.PersistenceStrategy -import com.datadog.android.core.persistence.datastore.DataStoreHandler import com.datadog.android.privacy.TrackingConsentProviderCallback import com.datadog.android.security.Encryption import java.util.Collections diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt index 0ac6966c58..ccfe316212 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt @@ -7,11 +7,11 @@ package com.datadog.android.core.internal.persistence.datastore import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.datastore.DataStoreCallback +import com.datadog.android.api.storage.datastore.DataStoreHandler import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.internal.utils.submitSafe import com.datadog.android.core.persistence.Serializer -import com.datadog.android.core.persistence.datastore.DataStoreCallback -import com.datadog.android.core.persistence.datastore.DataStoreHandler import java.util.concurrent.ExecutorService internal class DataStoreFileHandler( @@ -27,14 +27,12 @@ internal class DataStoreFileHandler( version: Int, serializer: Serializer ) { - @Suppress("ThreadSafety") // called in executor executorService.submitSafe("dataStoreWrite", internalLogger) { datastoreFileWriter.write(key, data, serializer, version) } } override fun removeValue(key: String) { - @Suppress("ThreadSafety") // called in executor executorService.submitSafe("dataStoreRemove", internalLogger) { datastoreFileWriter.delete(key) } @@ -46,7 +44,6 @@ internal class DataStoreFileHandler( callback: DataStoreCallback, deserializer: Deserializer ) { - @Suppress("ThreadSafety") // called in executor executorService.submitSafe("dataStoreRead", internalLogger) { dataStoreFileReader.read(key, deserializer, version, callback) } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt index a71ad5001f..f2d057fce0 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt @@ -7,9 +7,9 @@ package com.datadog.android.core.internal.persistence.datastore import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.datastore.DataStoreHandler import com.datadog.android.core.internal.persistence.file.existsSafe import com.datadog.android.core.internal.persistence.file.mkdirsSafe -import com.datadog.android.core.persistence.datastore.DataStoreHandler import java.io.File import java.util.Locale diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt index 0c7a3fce56..eab174a28e 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt @@ -8,13 +8,13 @@ package com.datadog.android.core.internal.persistence.datastore import androidx.annotation.WorkerThread import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.datastore.DataStoreCallback import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.internal.persistence.datastore.ext.toInt import com.datadog.android.core.internal.persistence.file.existsSafe import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockType -import com.datadog.android.core.persistence.datastore.DataStoreCallback import com.datadog.android.core.persistence.datastore.DataStoreContent import java.io.File import java.util.Locale @@ -40,7 +40,7 @@ internal class DatastoreFileReader( ) if (!datastoreFile.existsSafe(internalLogger)) { - callback.onNoData() + callback.onSuccess(null) return } @@ -72,7 +72,7 @@ internal class DatastoreFileReader( } if (requestedVersion != 0 && dataStoreContent.versionCode != requestedVersion) { - callback.onNoData() + callback.onSuccess(null) return } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt index e42673f36f..57c7458a5e 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt @@ -6,10 +6,10 @@ package com.datadog.android.core.internal.persistence.datastore +import com.datadog.android.api.storage.datastore.DataStoreCallback +import com.datadog.android.api.storage.datastore.DataStoreHandler import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.persistence.Serializer -import com.datadog.android.core.persistence.datastore.DataStoreCallback -import com.datadog.android.core.persistence.datastore.DataStoreHandler internal class NoOpDataStoreHandler : DataStoreHandler { override fun setValue( diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt index f114cf5d3b..d1d2d2c84a 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt @@ -16,6 +16,7 @@ import com.datadog.android.api.feature.FeatureEventReceiver import com.datadog.android.api.feature.StorageBackedFeature import com.datadog.android.api.storage.EventBatchWriter import com.datadog.android.api.storage.FeatureStorageConfiguration +import com.datadog.android.api.storage.datastore.DataStoreHandler import com.datadog.android.core.configuration.BatchProcessingLevel import com.datadog.android.core.configuration.BatchSize import com.datadog.android.core.configuration.UploadFrequency @@ -38,7 +39,6 @@ import com.datadog.android.core.internal.persistence.file.FilePersistenceConfig import com.datadog.android.core.internal.persistence.file.NoOpFileOrchestrator import com.datadog.android.core.internal.persistence.file.batch.BatchFileOrchestrator import com.datadog.android.core.persistence.PersistenceStrategy -import com.datadog.android.core.persistence.datastore.DataStoreHandler import com.datadog.android.privacy.TrackingConsent import com.datadog.android.privacy.TrackingConsentProviderCallback import com.datadog.android.utils.config.ApplicationContextTestConfiguration diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt index e5dca5b5a7..e92b0472c0 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt @@ -7,12 +7,12 @@ package com.datadog.android.core.internal.persistence.file.datastore import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.datastore.DataStoreCallback import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHandler import com.datadog.android.core.internal.persistence.datastore.DatastoreFileReader import com.datadog.android.core.internal.persistence.datastore.DatastoreFileWriter import com.datadog.android.core.persistence.Serializer -import com.datadog.android.core.persistence.datastore.DataStoreCallback import com.datadog.android.core.persistence.datastore.DataStoreContent import com.datadog.android.utils.forge.Configurator import fr.xgouchet.elmyr.annotation.IntForgery diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt index fa76a26a73..966deea3b6 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt @@ -7,6 +7,7 @@ package com.datadog.android.core.internal.persistence.file.datastore import com.datadog.android.api.InternalLogger +import com.datadog.android.api.storage.datastore.DataStoreCallback import com.datadog.android.core.internal.persistence.Deserializer import com.datadog.android.core.internal.persistence.datastore.DataStoreFileHelper import com.datadog.android.core.internal.persistence.datastore.DatastoreFileReader @@ -16,7 +17,6 @@ import com.datadog.android.core.internal.persistence.file.existsSafe import com.datadog.android.core.internal.persistence.tlvformat.TLVBlock import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockFileReader import com.datadog.android.core.internal.persistence.tlvformat.TLVBlockType -import com.datadog.android.core.persistence.datastore.DataStoreCallback import com.datadog.android.core.persistence.datastore.DataStoreContent import com.datadog.android.utils.forge.Configurator import com.datadog.android.utils.verifyLog From 2f0b5906a30417bb4415081d3e3a2d175fbfe239 Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Thu, 6 Jun 2024 09:53:24 +0200 Subject: [PATCH 12/14] RUM-4098: Change DS path --- .../datastore/DataStoreFileHelper.kt | 2 +- .../datastore/DatastoreFileReader.kt | 19 +++-- .../datastore/DatastoreFileWriter.kt | 3 +- .../persistence/tlvformat/TLVBlock.kt | 8 +- .../android/core/internal/SdkFeatureTest.kt | 3 - .../datastore/DataStoreFileHandlerTest.kt | 3 +- .../file/datastore/DataStoreFileReaderTest.kt | 82 +++++-------------- detekt_custom.yml | 1 - 8 files changed, 42 insertions(+), 79 deletions(-) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt index f2d057fce0..06760e675c 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHelper.kt @@ -42,7 +42,7 @@ internal class DataStoreFileHelper( val dataStoreDirectory = File( storageDir, - "$featureName/$folderName" + "$folderName/$featureName" ) if (!dataStoreDirectory.existsSafe(internalLogger)) { diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt index eab174a28e..e6f792258b 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt @@ -58,13 +58,15 @@ internal class DatastoreFileReader( val tlvBlocks = tlvBlockFileReader.read(datastoreFile) // there should be as many blocks read as there are block types - if (tlvBlocks.size != TLVBlockType.values().size) { - logInvalidNumberOfBlocksError(tlvBlocks.size) + val numberBlocksFound = tlvBlocks.size + val numberBlocksExpected = TLVBlockType.values().size + if (numberBlocksFound != numberBlocksExpected) { + logInvalidNumberOfBlocksError(numberBlocksFound, numberBlocksExpected) callback.onFailure() return } - val dataStoreContent = tryToMapToDataStoreContents(deserializer, tlvBlocks) + val dataStoreContent = mapToDataStoreContents(deserializer, tlvBlocks) if (dataStoreContent == null) { callback.onFailure() @@ -79,7 +81,7 @@ internal class DatastoreFileReader( callback.onSuccess(dataStoreContent) } - private fun tryToMapToDataStoreContents( + private fun mapToDataStoreContents( deserializer: Deserializer, tlvBlocks: List ): DataStoreContent? { @@ -99,11 +101,14 @@ internal class DatastoreFileReader( ) } - private fun logInvalidNumberOfBlocksError(numberOfBlocks: Int) { + private fun logInvalidNumberOfBlocksError(numberBlocksFound: Int, numberBlocksExpected: Int) { internalLogger.log( level = InternalLogger.Level.ERROR, target = InternalLogger.Target.MAINTAINER, - messageBuilder = { INVALID_NUMBER_OF_BLOCKS_ERROR.format(Locale.US, numberOfBlocks) } + messageBuilder = { + INVALID_NUMBER_OF_BLOCKS_ERROR + .format(Locale.US, numberBlocksFound, numberBlocksExpected) + } ) } @@ -117,7 +122,7 @@ internal class DatastoreFileReader( internal companion object { internal const val INVALID_NUMBER_OF_BLOCKS_ERROR = - "Read error - datastore file contains an invalid number of blocks. Was: %s" + "Read error - datastore entry has invalid number of blocks. Was: %s expected: %s" internal const val UNEXPECTED_BLOCKS_ORDER_ERROR = "Read error - blocks are in an unexpected order" } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt index 5162ba6052..dd84ec3cc8 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileWriter.kt @@ -47,7 +47,7 @@ internal class DatastoreFileWriter( } val dataToWrite = listOf(versionCodeBlock, dataBlock).join( - separator = byteArrayOf(), + separator = EMPTY_BYTE_ARRAY, internalLogger = internalLogger ) @@ -113,5 +113,6 @@ internal class DatastoreFileWriter( internal companion object { internal const val FAILED_TO_SERIALIZE_DATA_ERROR = "Write error - Failed to serialize data for the datastore" + private val EMPTY_BYTE_ARRAY = ByteArray(0) } } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt index d665ba0e65..d79bf7cb1c 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlock.kt @@ -26,7 +26,7 @@ internal class TLVBlock( val entrySize = typeFieldSize + dataLengthFieldSize + dataFieldSize if (entrySize > maxEntrySize) { - logEntrySizeExceededError(maxEntrySize) + logEntrySizeExceededError(entrySize, maxEntrySize) return null } @@ -43,11 +43,11 @@ internal class TLVBlock( .array() } - private fun logEntrySizeExceededError(maxEntrySize: Int) { + private fun logEntrySizeExceededError(entrySize: Int, maxEntrySize: Int) { internalLogger.log( target = InternalLogger.Target.MAINTAINER, level = InternalLogger.Level.WARN, - messageBuilder = { BYTE_LENGTH_EXCEEDED_ERROR.format(Locale.US, maxEntrySize) } + messageBuilder = { BYTE_LENGTH_EXCEEDED_ERROR.format(Locale.US, maxEntrySize, entrySize) } ) } @@ -55,6 +55,6 @@ internal class TLVBlock( // The maximum length of data (Value) in TLV block defining key data. private const val MAXIMUM_DATA_SIZE_MB = 10 * 1024 * 1024 // 10 mb internal const val BYTE_LENGTH_EXCEEDED_ERROR = - "DataBlock length exceeds limit of %s bytes" + "DataBlock length exceeds limit of %s bytes, was %s" } } diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt index d1d2d2c84a..d0fd272a07 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/SdkFeatureTest.kt @@ -16,7 +16,6 @@ import com.datadog.android.api.feature.FeatureEventReceiver import com.datadog.android.api.feature.StorageBackedFeature import com.datadog.android.api.storage.EventBatchWriter import com.datadog.android.api.storage.FeatureStorageConfiguration -import com.datadog.android.api.storage.datastore.DataStoreHandler import com.datadog.android.core.configuration.BatchProcessingLevel import com.datadog.android.core.configuration.BatchSize import com.datadog.android.core.configuration.UploadFrequency @@ -433,7 +432,6 @@ internal class SdkFeatureTest { testedFeature.initialize(appContext.mockInstance, fakeInstanceId) assertThat(testedFeature.dataStore) - .isInstanceOf(DataStoreHandler::class.java) .isNotInstanceOf(NoOpDataStoreHandler::class.java) // When @@ -450,7 +448,6 @@ internal class SdkFeatureTest { // Then assertThat(testedFeature.dataStore) - .isInstanceOf(DataStoreHandler::class.java) .isNotInstanceOf(NoOpDataStoreHandler::class.java) } diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt index e92b0472c0..0939000257 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt @@ -82,9 +82,8 @@ internal class DataStoreFileHandlerTest { } fileCallback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) {} + override fun onSuccess(dataStoreContent: DataStoreContent?) {} override fun onFailure() {} - override fun onNoData() {} } testedDataStoreHandler = DataStoreFileHandler( diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt index 966deea3b6..27ae82bca7 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt @@ -23,7 +23,7 @@ import com.datadog.android.utils.verifyLog import fr.xgouchet.elmyr.annotation.StringForgery import fr.xgouchet.elmyr.junit5.ForgeConfiguration import fr.xgouchet.elmyr.junit5.ForgeExtension -import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -115,10 +115,9 @@ internal class DataStoreFileReaderTest { } @Test - fun `M return noData W read() { datastore file does not exist }`() { + fun `M return no data W read() { datastore file does not exist }`() { // Given whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(false) - var gotNoData = false // When testedDatastoreFileReader.read( @@ -126,24 +125,16 @@ internal class DataStoreFileReaderTest { deserializer = mockDeserializer, version = 0, callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { - // should not get here - Assertions.assertThat(1).isEqualTo(2) + override fun onSuccess(dataStoreContent: DataStoreContent?) { + assertThat(dataStoreContent).isNull() } override fun onFailure() { // should not get here - Assertions.assertThat(1).isEqualTo(2) - } - - override fun onNoData() { - gotNoData = true + assertThat(1).isEqualTo(2) } } ) - - // Then - Assertions.assertThat(gotNoData).isTrue() } @Test @@ -151,7 +142,9 @@ internal class DataStoreFileReaderTest { // Given blocksReturned.removeLast() - val expectedError = INVALID_NUMBER_OF_BLOCKS_ERROR.format(Locale.US, blocksReturned.size) + val foundBlocks = blocksReturned.size + val expectedBlocks = TLVBlockType.values().size + val expectedError = INVALID_NUMBER_OF_BLOCKS_ERROR.format(Locale.US, foundBlocks, expectedBlocks) // When testedDatastoreFileReader.read( @@ -159,9 +152,9 @@ internal class DataStoreFileReaderTest { deserializer = mockDeserializer, version = 0, callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { + override fun onSuccess(dataStoreContent: DataStoreContent?) { // should not get here - Assertions.assertThat(1).isEqualTo(2) + assertThat(1).isEqualTo(2) } override fun onFailure() { @@ -171,44 +164,28 @@ internal class DataStoreFileReaderTest { message = expectedError ) } - - override fun onNoData() { - // should not get here - Assertions.assertThat(1).isEqualTo(2) - } } ) } @Test - fun `M return noData W value() { explicit version and versions don't match }`() { - // Given - var noData = false - + fun `M return no data W value() { explicit version and versions don't match }`() { // When testedDatastoreFileReader.read( key = fakeKey, version = 99, callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { - // should not get here - Assertions.assertThat(1).isEqualTo(2) + override fun onSuccess(dataStoreContent: DataStoreContent?) { + assertThat(dataStoreContent).isNull() } override fun onFailure() { // should not get here - Assertions.assertThat(1).isEqualTo(2) - } - - override fun onNoData() { - noData = true + assertThat(1).isEqualTo(2) } }, deserializer = mockDeserializer ) - - // Then - Assertions.assertThat(noData).isTrue() } @Test @@ -225,18 +202,13 @@ internal class DataStoreFileReaderTest { version = 0, callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { - Assertions.assertThat(dataStoreContent.data).isEqualTo(fakeDataBytes) + override fun onSuccess(dataStoreContent: DataStoreContent?) { + assertThat(dataStoreContent?.data).isEqualTo(fakeDataBytes) } override fun onFailure() { // should not get here - Assertions.assertThat(1).isEqualTo(2) - } - - override fun onNoData() { - // should not get here - Assertions.assertThat(1).isEqualTo(2) + assertThat(1).isEqualTo(2) } } ) @@ -254,14 +226,9 @@ internal class DataStoreFileReaderTest { deserializer = mockDeserializer, version = 0, callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { + override fun onSuccess(dataStoreContent: DataStoreContent?) { // should not get here - Assertions.assertThat(1).isEqualTo(2) - } - - override fun onNoData() { - // should not get here - Assertions.assertThat(1).isEqualTo(2) + assertThat(1).isEqualTo(2) } override fun onFailure() { @@ -271,7 +238,7 @@ internal class DataStoreFileReaderTest { ) // Then - Assertions.assertThat(gotFailure).isTrue() + assertThat(gotFailure).isTrue() } @Test @@ -286,9 +253,9 @@ internal class DataStoreFileReaderTest { deserializer = mockDeserializer, version = 0, callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent) { + override fun onSuccess(dataStoreContent: DataStoreContent?) { // should not get here - Assertions.assertThat(1).isEqualTo(2) + assertThat(1).isEqualTo(2) } override fun onFailure() { @@ -298,11 +265,6 @@ internal class DataStoreFileReaderTest { message = UNEXPECTED_BLOCKS_ORDER_ERROR ) } - - override fun onNoData() { - // should not get here - Assertions.assertThat(1).isEqualTo(2) - } } ) } diff --git a/detekt_custom.yml b/detekt_custom.yml index 6c893ce8b1..8cc8073961 100644 --- a/detekt_custom.yml +++ b/detekt_custom.yml @@ -1011,7 +1011,6 @@ datadog: - "kotlin.Boolean.hashCode()" - "kotlin.Byte.toInt()" - "kotlin.ByteArray.constructor(kotlin.Int)" - - "kotlin.ByteArray.inputStream()" - "kotlin.Char.isLowerCase()" - "kotlin.Char.titlecase(java.util.Locale)" - "kotlin.CharArray.constructor(kotlin.Int, kotlin.Function1)" From d25e5201f726a28b911099302467cbb2b7936efb Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Thu, 6 Jun 2024 11:33:04 +0200 Subject: [PATCH 13/14] RUM-4098: Make version default null instead of default 0 --- dd-sdk-android-core/api/apiSurface | 2 +- dd-sdk-android-core/api/dd-sdk-android-core.api | 4 ++-- .../android/api/storage/datastore/DataStoreHandler.kt | 2 +- .../internal/persistence/datastore/DataStoreFileHandler.kt | 2 +- .../internal/persistence/datastore/DatastoreFileReader.kt | 7 ++++--- .../internal/persistence/datastore/NoOpDataStoreHandler.kt | 2 +- .../persistence/file/datastore/DataStoreFileHandlerTest.kt | 1 - .../persistence/file/datastore/DataStoreFileReaderTest.kt | 5 ----- 8 files changed, 10 insertions(+), 15 deletions(-) diff --git a/dd-sdk-android-core/api/apiSurface b/dd-sdk-android-core/api/apiSurface index bc0cc1ea11..3a2af67f32 100644 --- a/dd-sdk-android-core/api/apiSurface +++ b/dd-sdk-android-core/api/apiSurface @@ -157,7 +157,7 @@ interface com.datadog.android.api.storage.datastore.DataStoreCallback fun onFailure() interface com.datadog.android.api.storage.datastore.DataStoreHandler fun setValue(String, T, Int = 0, com.datadog.android.core.persistence.Serializer) - fun value(String, Int = 0, DataStoreCallback, com.datadog.android.core.internal.persistence.Deserializer) + fun value(String, Int? = null, DataStoreCallback, com.datadog.android.core.internal.persistence.Deserializer) fun removeValue(String) companion object const val CURRENT_DATASTORE_VERSION: Int diff --git a/dd-sdk-android-core/api/dd-sdk-android-core.api b/dd-sdk-android-core/api/dd-sdk-android-core.api index da7edccc1b..d259241a04 100644 --- a/dd-sdk-android-core/api/dd-sdk-android-core.api +++ b/dd-sdk-android-core/api/dd-sdk-android-core.api @@ -474,7 +474,7 @@ public abstract interface class com/datadog/android/api/storage/datastore/DataSt public static final field Companion Lcom/datadog/android/api/storage/datastore/DataStoreHandler$Companion; public abstract fun removeValue (Ljava/lang/String;)V public abstract fun setValue (Ljava/lang/String;Ljava/lang/Object;ILcom/datadog/android/core/persistence/Serializer;)V - public abstract fun value (Ljava/lang/String;ILcom/datadog/android/api/storage/datastore/DataStoreCallback;Lcom/datadog/android/core/internal/persistence/Deserializer;)V + public abstract fun value (Ljava/lang/String;Ljava/lang/Integer;Lcom/datadog/android/api/storage/datastore/DataStoreCallback;Lcom/datadog/android/core/internal/persistence/Deserializer;)V } public final class com/datadog/android/api/storage/datastore/DataStoreHandler$Companion { @@ -483,7 +483,7 @@ public final class com/datadog/android/api/storage/datastore/DataStoreHandler$Co public final class com/datadog/android/api/storage/datastore/DataStoreHandler$DefaultImpls { public static synthetic fun setValue$default (Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Ljava/lang/String;Ljava/lang/Object;ILcom/datadog/android/core/persistence/Serializer;ILjava/lang/Object;)V - public static synthetic fun value$default (Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Ljava/lang/String;ILcom/datadog/android/api/storage/datastore/DataStoreCallback;Lcom/datadog/android/core/internal/persistence/Deserializer;ILjava/lang/Object;)V + public static synthetic fun value$default (Lcom/datadog/android/api/storage/datastore/DataStoreHandler;Ljava/lang/String;Ljava/lang/Integer;Lcom/datadog/android/api/storage/datastore/DataStoreCallback;Lcom/datadog/android/core/internal/persistence/Deserializer;ILjava/lang/Object;)V } public abstract interface class com/datadog/android/core/InternalSdkCore : com/datadog/android/api/feature/FeatureSdkCore { diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreHandler.kt index 5516dc4a7b..4dd86910e1 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreHandler.kt @@ -43,7 +43,7 @@ interface DataStoreHandler { */ fun value( key: String, - version: Int = 0, + version: Int? = null, callback: DataStoreCallback, deserializer: Deserializer ) diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt index ccfe316212..b4e949f3fb 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt @@ -40,7 +40,7 @@ internal class DataStoreFileHandler( override fun value( key: String, - version: Int, + version: Int?, callback: DataStoreCallback, deserializer: Deserializer ) { diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt index e6f792258b..2e38df4590 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt @@ -30,7 +30,7 @@ internal class DatastoreFileReader( internal fun read( key: String, deserializer: Deserializer, - version: Int, + version: Int? = null, callback: DataStoreCallback ) { val datastoreFile = dataStoreFileHelper.getDataStoreFile( @@ -52,7 +52,7 @@ internal class DatastoreFileReader( datastoreFile: File, deserializer: Deserializer, tlvBlockFileReader: TLVBlockFileReader, - requestedVersion: Int, + requestedVersion: Int?, callback: DataStoreCallback ) { val tlvBlocks = tlvBlockFileReader.read(datastoreFile) @@ -73,7 +73,8 @@ internal class DatastoreFileReader( return } - if (requestedVersion != 0 && dataStoreContent.versionCode != requestedVersion) { + // if an optional version is specified then only return data if the entry version exactly matches + if (requestedVersion != null && requestedVersion != dataStoreContent.versionCode) { callback.onSuccess(null) return } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt index 57c7458a5e..78300f08ed 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/NoOpDataStoreHandler.kt @@ -23,7 +23,7 @@ internal class NoOpDataStoreHandler : DataStoreHandler { override fun value( key: String, - version: Int, + version: Int?, callback: DataStoreCallback, deserializer: Deserializer ) { diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt index 0939000257..b9e68f2572 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt @@ -107,7 +107,6 @@ internal class DataStoreFileHandlerTest { verify(mockDataStoreFileReader).read( key = fakeKey, deserializer = mockDeserializer, - version = 0, callback = fileCallback ) } diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt index 27ae82bca7..4f03ca43e3 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt @@ -123,7 +123,6 @@ internal class DataStoreFileReaderTest { testedDatastoreFileReader.read( key = fakeKey, deserializer = mockDeserializer, - version = 0, callback = object : DataStoreCallback { override fun onSuccess(dataStoreContent: DataStoreContent?) { assertThat(dataStoreContent).isNull() @@ -150,7 +149,6 @@ internal class DataStoreFileReaderTest { testedDatastoreFileReader.read( key = fakeKey, deserializer = mockDeserializer, - version = 0, callback = object : DataStoreCallback { override fun onSuccess(dataStoreContent: DataStoreContent?) { // should not get here @@ -199,7 +197,6 @@ internal class DataStoreFileReaderTest { testedDatastoreFileReader.read( key = fakeKey, deserializer = mockDeserializer, - version = 0, callback = object : DataStoreCallback { override fun onSuccess(dataStoreContent: DataStoreContent?) { @@ -224,7 +221,6 @@ internal class DataStoreFileReaderTest { testedDatastoreFileReader.read( key = fakeKey, deserializer = mockDeserializer, - version = 0, callback = object : DataStoreCallback { override fun onSuccess(dataStoreContent: DataStoreContent?) { // should not get here @@ -251,7 +247,6 @@ internal class DataStoreFileReaderTest { testedDatastoreFileReader.read( key = fakeKey, deserializer = mockDeserializer, - version = 0, callback = object : DataStoreCallback { override fun onSuccess(dataStoreContent: DataStoreContent?) { // should not get here From cc27469040ebbcf11ff5bbf702b07c7e32951f14 Mon Sep 17 00:00:00 2001 From: jonathanmos <48201295+jonathanmos@users.noreply.github.com> Date: Wed, 19 Jun 2024 16:03:11 +0300 Subject: [PATCH 14/14] RUM-4098: Make datastore immutable in featurescope --- dd-sdk-android-core/api/apiSurface | 2 +- .../api/dd-sdk-android-core.api | 1 - .../android/api/feature/FeatureScope.kt | 4 +- .../storage/datastore/DataStoreCallback.kt | 5 +- .../api/storage/datastore/DataStoreHandler.kt | 3 + .../datastore/DataStoreFileHandler.kt | 8 +- .../datastore/DatastoreFileReader.kt | 2 +- .../datastore/DataStoreFileHandlerTest.kt | 16 +- .../file/datastore/DataStoreFileReaderTest.kt | 137 +++++++++--------- .../tlvformat/TLVBlockFileReaderTest.kt | 6 +- .../persistence/tlvformat/TLVBlockTest.kt | 15 +- .../persistence/tlvformat/TLVBlockTypeTest.kt | 14 -- 12 files changed, 92 insertions(+), 121 deletions(-) diff --git a/dd-sdk-android-core/api/apiSurface b/dd-sdk-android-core/api/apiSurface index 3a2af67f32..4625931e30 100644 --- a/dd-sdk-android-core/api/apiSurface +++ b/dd-sdk-android-core/api/apiSurface @@ -102,7 +102,7 @@ interface com.datadog.android.api.feature.FeatureContextUpdateReceiver interface com.datadog.android.api.feature.FeatureEventReceiver fun onReceive(Any) interface com.datadog.android.api.feature.FeatureScope - var dataStore: com.datadog.android.api.storage.datastore.DataStoreHandler + val dataStore: com.datadog.android.api.storage.datastore.DataStoreHandler fun withWriteContext(Boolean = false, (com.datadog.android.api.context.DatadogContext, com.datadog.android.api.storage.EventBatchWriter) -> Unit) fun sendEvent(Any) fun unwrap(): T diff --git a/dd-sdk-android-core/api/dd-sdk-android-core.api b/dd-sdk-android-core/api/dd-sdk-android-core.api index d259241a04..94c2b374c0 100644 --- a/dd-sdk-android-core/api/dd-sdk-android-core.api +++ b/dd-sdk-android-core/api/dd-sdk-android-core.api @@ -327,7 +327,6 @@ public abstract interface class com/datadog/android/api/feature/FeatureEventRece public abstract interface class com/datadog/android/api/feature/FeatureScope { public abstract fun getDataStore ()Lcom/datadog/android/api/storage/datastore/DataStoreHandler; public abstract fun sendEvent (Ljava/lang/Object;)V - public abstract fun setDataStore (Lcom/datadog/android/api/storage/datastore/DataStoreHandler;)V public abstract fun unwrap ()Lcom/datadog/android/api/feature/Feature; public abstract fun withWriteContext (ZLkotlin/jvm/functions/Function2;)V } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt index d4429a5d76..7c0b8928b9 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/feature/FeatureScope.kt @@ -17,9 +17,9 @@ import com.datadog.android.api.storage.datastore.DataStoreHandler interface FeatureScope { /** - * Property to enable interaction with the DataStore. + * Property to enable interaction with the data store. */ - var dataStore: DataStoreHandler + val dataStore: DataStoreHandler /** * Utility to write an event, asynchronously. diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreCallback.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreCallback.kt index 4d7bb48a9d..b4399b394f 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreCallback.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreCallback.kt @@ -10,18 +10,19 @@ import com.datadog.android.core.persistence.datastore.DataStoreContent /** * Callback for asynchronous operations on the datastore. + * @param T the datatype being retrieved. */ interface DataStoreCallback { /** * Triggered on successfully fetching data from the datastore. * - * @param dataStoreContent contains the datastore content if there was data to fetch. + * @param dataStoreContent (nullable) contains the datastore content if there was data to fetch, else null. */ fun onSuccess(dataStoreContent: DataStoreContent?) /** - * Triggered when an exception occurred getting data from the datastore. + * Triggered when we failed to get data from the datastore. */ fun onFailure() } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreHandler.kt index 4dd86910e1..bf66c59f5a 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/api/storage/datastore/DataStoreHandler.kt @@ -16,6 +16,7 @@ interface DataStoreHandler { /** * Write data to the datastore. + * This executes on a worker thread and not on the caller thread. * * @param T datatype of the data to write to the datastore. * @param key name of the datastore entry. @@ -33,6 +34,7 @@ interface DataStoreHandler { /** * Read data from the datastore. + * This executes on a worker thread and not on the caller thread. * * @param T datatype of the data to read from the datastore. * @param key name of the datastore entry. @@ -50,6 +52,7 @@ interface DataStoreHandler { /** * Remove an entry from the datastore. + * This executes on a worker thread and not on the caller thread. * * @param key name of the datastore entry */ diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt index b4e949f3fb..3c866f759a 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DataStoreFileHandler.kt @@ -10,7 +10,7 @@ import com.datadog.android.api.InternalLogger import com.datadog.android.api.storage.datastore.DataStoreCallback import com.datadog.android.api.storage.datastore.DataStoreHandler import com.datadog.android.core.internal.persistence.Deserializer -import com.datadog.android.core.internal.utils.submitSafe +import com.datadog.android.core.internal.utils.executeSafe import com.datadog.android.core.persistence.Serializer import java.util.concurrent.ExecutorService @@ -27,13 +27,13 @@ internal class DataStoreFileHandler( version: Int, serializer: Serializer ) { - executorService.submitSafe("dataStoreWrite", internalLogger) { + executorService.executeSafe("dataStoreWrite", internalLogger) { datastoreFileWriter.write(key, data, serializer, version) } } override fun removeValue(key: String) { - executorService.submitSafe("dataStoreRemove", internalLogger) { + executorService.executeSafe("dataStoreRemove", internalLogger) { datastoreFileWriter.delete(key) } } @@ -44,7 +44,7 @@ internal class DataStoreFileHandler( callback: DataStoreCallback, deserializer: Deserializer ) { - executorService.submitSafe("dataStoreRead", internalLogger) { + executorService.executeSafe("dataStoreRead", internalLogger) { dataStoreFileReader.read(key, deserializer, version, callback) } } diff --git a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt index 2e38df4590..b183db105f 100644 --- a/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt +++ b/dd-sdk-android-core/src/main/kotlin/com/datadog/android/core/internal/persistence/datastore/DatastoreFileReader.kt @@ -123,7 +123,7 @@ internal class DatastoreFileReader( internal companion object { internal const val INVALID_NUMBER_OF_BLOCKS_ERROR = - "Read error - datastore entry has invalid number of blocks. Was: %s expected: %s" + "Read error - datastore entry has invalid number of blocks. Was: %d, expected: %d" internal const val UNEXPECTED_BLOCKS_ORDER_ERROR = "Read error - blocks are in an unexpected order" } diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt index b9e68f2572..be628416a1 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileHandlerTest.kt @@ -32,8 +32,6 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import java.util.concurrent.ExecutorService -import java.util.concurrent.Future -import java.util.concurrent.TimeUnit @Extensions( ExtendWith(MockitoExtension::class), @@ -76,9 +74,8 @@ internal class DataStoreFileHandlerTest { @BeforeEach fun setup() { - whenever(mockExecutorService.submit(any())) doAnswer { + whenever(mockExecutorService.execute(any())) doAnswer { it.getArgument(0).run() - StubFuture() } fileCallback = object : DataStoreCallback { @@ -165,15 +162,4 @@ internal class DataStoreFileHandlerTest { key = fakeKey ) } - - private class StubFuture : Future { - override fun cancel(mayInterruptIfRunning: Boolean) = - error("Not supposed to be called") - - override fun isCancelled(): Boolean = error("Not supposed to be called") - override fun isDone(): Boolean = error("Not supposed to be called") - override fun get(): Any = error("Not supposed to be called") - override fun get(timeout: Long, unit: TimeUnit?): Any = - error("Not supposed to be called") - } } diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt index 4f03ca43e3..e81f9faf3d 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/file/datastore/DataStoreFileReaderTest.kt @@ -30,10 +30,14 @@ import org.junit.jupiter.api.extension.ExtendWith import org.junit.jupiter.api.extension.Extensions import org.junit.jupiter.api.io.TempDir import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.junit.jupiter.MockitoExtension import org.mockito.junit.jupiter.MockitoSettings import org.mockito.kotlin.any import org.mockito.kotlin.eq +import org.mockito.kotlin.nullableArgumentCaptor +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import java.io.File @@ -118,22 +122,21 @@ internal class DataStoreFileReaderTest { fun `M return no data W read() { datastore file does not exist }`() { // Given whenever(mockDataStoreFile.existsSafe(mockInternalLogger)).thenReturn(false) + val mockCallback = mock>() // When testedDatastoreFileReader.read( key = fakeKey, deserializer = mockDeserializer, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent?) { - assertThat(dataStoreContent).isNull() - } - - override fun onFailure() { - // should not get here - assertThat(1).isEqualTo(2) - } - } + callback = mockCallback ) + + // Then + nullableArgumentCaptor> { + verify(mockCallback).onSuccess(capture()) + assertThat(lastValue).isNull() + verifyNoMoreInteractions(mockCallback) + } } @Test @@ -144,46 +147,44 @@ internal class DataStoreFileReaderTest { val foundBlocks = blocksReturned.size val expectedBlocks = TLVBlockType.values().size val expectedError = INVALID_NUMBER_OF_BLOCKS_ERROR.format(Locale.US, foundBlocks, expectedBlocks) + val mockCallback = mock>() // When testedDatastoreFileReader.read( key = fakeKey, deserializer = mockDeserializer, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent?) { - // should not get here - assertThat(1).isEqualTo(2) - } - - override fun onFailure() { - mockInternalLogger.verifyLog( - target = InternalLogger.Target.MAINTAINER, - level = InternalLogger.Level.ERROR, - message = expectedError - ) - } - } + callback = mockCallback + ) + + // Then + verify(mockCallback).onFailure() + mockInternalLogger.verifyLog( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + message = expectedError ) + verifyNoMoreInteractions(mockCallback) } @Test fun `M return no data W value() { explicit version and versions don't match }`() { + // Given + val mockCallback = mock>() + // When testedDatastoreFileReader.read( key = fakeKey, version = 99, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent?) { - assertThat(dataStoreContent).isNull() - } - - override fun onFailure() { - // should not get here - assertThat(1).isEqualTo(2) - } - }, + callback = mockCallback, deserializer = mockDeserializer ) + + // Then + nullableArgumentCaptor> { + verify(mockCallback).onSuccess(capture()) + assertThat(lastValue).isNull() + verifyNoMoreInteractions(mockCallback) + } } @Test @@ -192,49 +193,46 @@ internal class DataStoreFileReaderTest { blocksReturned.clear() blocksReturned.add(createVersionBlock(true)) blocksReturned.add(createDataBlock()) + val mockCallback = mock>() // When testedDatastoreFileReader.read( key = fakeKey, deserializer = mockDeserializer, - callback = object : DataStoreCallback { - - override fun onSuccess(dataStoreContent: DataStoreContent?) { - assertThat(dataStoreContent?.data).isEqualTo(fakeDataBytes) - } - - override fun onFailure() { - // should not get here - assertThat(1).isEqualTo(2) - } - } + callback = mockCallback ) + + // Then + nullableArgumentCaptor> { + verify(mockCallback).onSuccess(capture()) + assertThat(lastValue?.data).isEqualTo(fakeDataBytes) + verifyNoMoreInteractions(mockCallback) + } } @Test fun `M return onFailure W read() { invalid number of blocks }`() { // Given blocksReturned.removeLast() - var gotFailure = false + val expectedMessage = + INVALID_NUMBER_OF_BLOCKS_ERROR.format(Locale.US, blocksReturned.size, TLVBlockType.values().size) + val mockCallback = mock>() // When testedDatastoreFileReader.read( key = fakeKey, deserializer = mockDeserializer, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent?) { - // should not get here - assertThat(1).isEqualTo(2) - } - - override fun onFailure() { - gotFailure = true - } - } + callback = mockCallback ) // Then - assertThat(gotFailure).isTrue() + verify(mockCallback).onFailure() + mockInternalLogger.verifyLog( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + message = expectedMessage + ) + verifyNoMoreInteractions(mockCallback) } @Test @@ -242,26 +240,23 @@ internal class DataStoreFileReaderTest { // Given blocksReturned = arrayListOf(dataBlock, versionBlock) whenever(mockTLVBlockFileReader.read(mockDataStoreFile)).thenReturn(blocksReturned) + val mockCallback = mock>() // When testedDatastoreFileReader.read( key = fakeKey, deserializer = mockDeserializer, - callback = object : DataStoreCallback { - override fun onSuccess(dataStoreContent: DataStoreContent?) { - // should not get here - assertThat(1).isEqualTo(2) - } - - override fun onFailure() { - mockInternalLogger.verifyLog( - target = InternalLogger.Target.MAINTAINER, - level = InternalLogger.Level.ERROR, - message = UNEXPECTED_BLOCKS_ORDER_ERROR - ) - } - } + callback = mockCallback + ) + + // Then + verify(mockCallback).onFailure() + mockInternalLogger.verifyLog( + target = InternalLogger.Target.MAINTAINER, + level = InternalLogger.Level.ERROR, + message = UNEXPECTED_BLOCKS_ORDER_ERROR ) + verifyNoMoreInteractions(mockCallback) } private fun createVersionBlock(valid: Boolean, newVersion: Int = 0): TLVBlock { diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt index 0d7e87023d..66c2e291ee 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockFileReaderTest.kt @@ -50,7 +50,7 @@ internal class TLVBlockFileReaderTest { @Mock private lateinit var mockInternalLogger: InternalLogger - @StringForgery(regex = "^(\\w{3})\$") // a minimal number of chars to avoid flakiness + @StringForgery private lateinit var fakeDataString: String private lateinit var fakeVersionBytes: ByteArray @@ -58,7 +58,7 @@ internal class TLVBlockFileReaderTest { private lateinit var fakeBufferBytes: ByteArray @BeforeEach - fun setup(@IntForgery(min = 0, max = 10) fakeVersion: Int) { + fun setup(@IntForgery(min = 0) fakeVersion: Int) { val versionBytes = createVersionBytes(fakeVersion) val dataBytes = createDataBytes() val dataToWrite = versionBytes + dataBytes @@ -115,7 +115,7 @@ internal class TLVBlockFileReaderTest { val tlvArray = testedReader.read(file = mockFile) // Then - assertThat(tlvArray.size).isEqualTo(2) + assertThat(tlvArray).hasSize(2) val versionObject = tlvArray[0] val dataObject = tlvArray[1] diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt index 92d4c60f85..a174e71bb1 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTest.kt @@ -78,15 +78,16 @@ internal class TLVBlockTest { val block = testedTLVBlock.serialize() // Then - assertThat(block?.size).isEqualTo(fakeString.length + 6) - val type = block?.copyOfRange(0, 2) - val length = block?.copyOfRange(2, 6) - val data = block?.copyOfRange(6, block.size) - val typeAsShort = type?.let { ByteBuffer.wrap(it).getShort() } - val lengthAsInt = length?.let { ByteBuffer.wrap(it).getInt() } + checkNotNull(block) + assertThat(block.size).isEqualTo(fakeString.length + 6) + val type = block.copyOfRange(0, 2) + val length = block.copyOfRange(2, 6) + val data = block.copyOfRange(6, block.size) + val typeAsShort = type.let { ByteBuffer.wrap(it).getShort() } + val lengthAsInt = length.let { ByteBuffer.wrap(it).getInt() } assertThat(typeAsShort).isEqualTo(fakeTLVType) - assertThat(lengthAsInt).isEqualTo(data?.size) + assertThat(lengthAsInt).isEqualTo(data.size) } @Test diff --git a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTypeTest.kt b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTypeTest.kt index 804c551a7b..acf7ff5c31 100644 --- a/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTypeTest.kt +++ b/dd-sdk-android-core/src/test/kotlin/com/datadog/android/core/internal/persistence/tlvformat/TLVBlockTypeTest.kt @@ -6,23 +6,9 @@ package com.datadog.android.core.internal.persistence.tlvformat -import com.datadog.android.utils.forge.Configurator -import fr.xgouchet.elmyr.junit5.ForgeConfiguration -import fr.xgouchet.elmyr.junit5.ForgeExtension import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.Extensions -import org.mockito.junit.jupiter.MockitoExtension -import org.mockito.junit.jupiter.MockitoSettings -import org.mockito.quality.Strictness -@Extensions( - ExtendWith(MockitoExtension::class), - ExtendWith(ForgeExtension::class) -) -@MockitoSettings(strictness = Strictness.LENIENT) -@ForgeConfiguration(Configurator::class) internal class TLVBlockTypeTest { @Test fun `M return type value W fromValue() { existing value }`() {