From d0d6eb9ff9e54ba40868423f610654fb0408205d Mon Sep 17 00:00:00 2001 From: Tomasz Rybakiewicz Date: Wed, 11 Jan 2023 13:16:57 -0500 Subject: [PATCH 1/2] Optimized RoadShield loading: - deleted duplicate image cache - ShieldsCache and ShieldByteArrayCache were caching same data twice - separated download from caching logic by introducing ResourceLoader and CacheResourceLoader local interfaces - renamed ShieldResultCache -> RoadShieldLoader and loosely coupled it with sprites and image loaders - updated RoadShieldContentManagerImpl - loosely coupled it with loader class --- .../RoadShieldContentManagerContainer.kt | 22 +- .../ui/shield/RoadShieldContentManagerImpl.kt | 10 +- .../navigation/ui/shield/ShieldsCache.kt | 227 ------------- .../{ => internal}/RoadShieldDownloader.kt | 17 +- .../internal/loader/CachedResourceLoader.kt | 26 ++ .../internal/loader/ResourceDownloader.kt | 43 +++ .../shield/internal/loader/ResourceLoader.kt | 7 + .../internal/loader/RoadShieldLoader.kt | 98 ++++++ .../loader/ShieldSpritesDownloader.kt | 29 ++ .../RoadShieldContentManagerImplTest.kt | 113 +++---- .../ui/shield/RoadShieldDownloaderTest.kt | 1 + .../ui/shield/ShieldByteArrayCacheTest.kt | 315 ------------------ .../loader/CachedResourceLoaderTest.kt | 50 +++ .../internal/loader/ResourceDownloaderTest.kt | 83 +++++ .../loader/RoadShieldLoaderTest.kt} | 79 +++-- .../loader/ShieldSpritesDownloaderTest.kt} | 38 +-- 16 files changed, 474 insertions(+), 684 deletions(-) delete mode 100644 libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/ShieldsCache.kt rename libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/{ => internal}/RoadShieldDownloader.kt (67%) create mode 100644 libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/CachedResourceLoader.kt create mode 100644 libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceDownloader.kt create mode 100644 libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceLoader.kt create mode 100644 libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/RoadShieldLoader.kt create mode 100644 libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ShieldSpritesDownloader.kt delete mode 100644 libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/ShieldByteArrayCacheTest.kt create mode 100644 libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/CachedResourceLoaderTest.kt create mode 100644 libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceDownloaderTest.kt rename libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/{ShieldResultCacheTest.kt => internal/loader/RoadShieldLoaderTest.kt} (79%) rename libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/{ShieldSpritesCacheTest.kt => internal/loader/ShieldSpritesDownloaderTest.kt} (75%) diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerContainer.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerContainer.kt index 534c8dd1f12..a2ea4d9bbce 100644 --- a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerContainer.kt +++ b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerContainer.kt @@ -1,5 +1,9 @@ package com.mapbox.navigation.ui.shield +import com.mapbox.navigation.ui.shield.internal.RoadShieldDownloader +import com.mapbox.navigation.ui.shield.internal.loader.CachedResourceLoader +import com.mapbox.navigation.ui.shield.internal.loader.RoadShieldLoader +import com.mapbox.navigation.ui.shield.internal.loader.ShieldSpritesDownloader import com.mapbox.navigation.ui.shield.internal.model.RouteShieldToDownload /** @@ -7,8 +11,24 @@ import com.mapbox.navigation.ui.shield.internal.model.RouteShieldToDownload * so that the same cache can be reused through the app processes life. */ internal object RoadShieldContentManagerContainer : RoadShieldContentManager { + private const val SPRITES_CACHE_SIZE = 8 // entries + private const val IMAGES_CACHE_SIZE = 40 // entries + private val contentManager: RoadShieldContentManager by lazy { - RoadShieldContentManagerImpl() + RoadShieldContentManagerImpl( + shieldLoader = CachedResourceLoader( + IMAGES_CACHE_SIZE, + RoadShieldLoader( + spritesLoader = CachedResourceLoader( + SPRITES_CACHE_SIZE, + ShieldSpritesDownloader() + ), + imageLoader = { url -> + RoadShieldDownloader.download(url) + } + ) + ) + ) } override suspend fun getShields(shieldsToDownload: List) = diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerImpl.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerImpl.kt index d83af2bc652..710b9b05f0a 100644 --- a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerImpl.kt +++ b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerImpl.kt @@ -3,7 +3,9 @@ package com.mapbox.navigation.ui.shield import com.mapbox.api.directions.v5.models.BannerComponents import com.mapbox.bindgen.Expected import com.mapbox.bindgen.ExpectedFactory +import com.mapbox.navigation.ui.shield.internal.loader.ResourceLoader import com.mapbox.navigation.ui.shield.internal.model.RouteShieldToDownload +import com.mapbox.navigation.ui.shield.model.RouteShield import com.mapbox.navigation.ui.shield.model.RouteShieldError import com.mapbox.navigation.ui.shield.model.RouteShieldOrigin import com.mapbox.navigation.ui.shield.model.RouteShieldResult @@ -67,7 +69,7 @@ import kotlin.coroutines.resume * - If request fails: repeat step 1. */ internal class RoadShieldContentManagerImpl( - private val shieldResultCache: ShieldResultCache = ShieldResultCache() + private val shieldLoader: ResourceLoader ) : RoadShieldContentManager { internal companion object { internal const val CANCELED_MESSAGE = "canceled" @@ -116,11 +118,11 @@ internal class RoadShieldContentManagerImpl( mainJob.scope.launch { when (toDownload) { is RouteShieldToDownload.MapboxDesign -> { - val mapboxDesignShieldResult = shieldResultCache.getOrRequest(toDownload) + val mapboxDesignShieldResult = shieldLoader.load(toDownload) resultMap[request] = if (mapboxDesignShieldResult.isError) { val legacyFallback = toDownload.legacyFallback if (legacyFallback != null) { - shieldResultCache.getOrRequest(legacyFallback).fold( + shieldLoader.load(legacyFallback).fold( { error -> ExpectedFactory.createError( RouteShieldError( @@ -172,7 +174,7 @@ internal class RoadShieldContentManagerImpl( } } is RouteShieldToDownload.MapboxLegacy -> { - resultMap[request] = shieldResultCache.getOrRequest(toDownload).fold( + resultMap[request] = shieldLoader.load(toDownload).fold( { error -> ExpectedFactory.createError( RouteShieldError( diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/ShieldsCache.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/ShieldsCache.kt deleted file mode 100644 index 9e7ca416bbe..00000000000 --- a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/ShieldsCache.kt +++ /dev/null @@ -1,227 +0,0 @@ -package com.mapbox.navigation.ui.shield - -import android.util.LruCache -import com.google.gson.JsonSyntaxException -import com.mapbox.api.directions.v5.models.ShieldSprites -import com.mapbox.api.directions.v5.models.ShieldSvg -import com.mapbox.bindgen.Expected -import com.mapbox.bindgen.ExpectedFactory -import com.mapbox.navigation.ui.shield.internal.model.RouteShieldToDownload -import com.mapbox.navigation.ui.shield.internal.model.generateSpriteSheetUrl -import com.mapbox.navigation.ui.shield.internal.model.getSpriteFrom -import com.mapbox.navigation.ui.shield.model.RouteShield -import com.mapbox.navigation.utils.internal.ifNonNull -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlin.coroutines.resume - -internal abstract class ResourceCache(cacheSize: Int) { - - internal companion object { - internal const val CANCELED_MESSAGE = "canceled" - } - - private val cache = LruCache>(cacheSize) - private val ongoingRequest = mutableSetOf() - private val awaitingCallbacks = mutableListOf<() -> Boolean>() - - /** - * Returns an [Expected] with value or error depending on whether the resource was generated - * successfully or not. - * - * The provided **[argument] is considered as a unique key** for the cache entry. - * Whenever there's a successful result generated for a given [argument], - * that result is saved to the cache and returned immediately upon calls to [getOrRequest] with the same [argument] again. - * - * If a successful result for given [argument] is not available in the cache, the result will be attempted to be generated. - * If the generation is asynchronous, any [getOrRequest] calls with the same [argument] will suspend - * until the already ongoing request returns to avoid duplicating work. - * - * If the generation fails, the erroneous result is returned to original and all awaiting callers. - * Once [getOrRequest] with the same [argument] is called again, the generation will be re-attempted. - * - * Calls to this function should be executed from a single thread, it's not thread safe. - */ - suspend fun getOrRequest(argument: Argument): Expected { - return cache.get(argument)?.value?.let { ExpectedFactory.createValue(it) } ?: run { - if (ongoingRequest.contains(argument)) { - suspendCancellableCoroutine { continuation -> - /** - * The callback checks if the result for this argument is available. - * Returns if true or keeps waiting if false. - */ - val callback = { - ifNonNull(cache.get(argument)) { expected -> - if (!continuation.isCancelled) { - continuation.resume(expected) - } - true - } ?: false - } - - // immediately verify if the result is available in case it was generated - // while we were initializing the coroutine - if (callback()) { - return@suspendCancellableCoroutine - } - - // if the result is not available, wait - awaitingCallbacks.add(callback) - continuation.invokeOnCancellation { - awaitingCallbacks.remove(callback) - } - } - } else { - try { - ongoingRequest.add(argument) - val result = obtainResource(argument) - cache.put(argument, result) - ongoingRequest.remove(argument) - invalidate() - result - } catch (ex: CancellationException) { - ongoingRequest.remove(argument) - val result = ExpectedFactory.createError(CANCELED_MESSAGE) - cache.put(argument, result) - invalidate() - result - } - } - } - } - - /** - * Notifies all awaiting callbacks that the result might be available. - */ - private fun invalidate() { - val iterator = awaitingCallbacks.iterator() - while (iterator.hasNext()) { - val remove = iterator.next().invoke() - if (remove) { - iterator.remove() - } - } - } - - /** - * Produces a result for a given [argument] if it was not found in the cache. - */ - protected abstract suspend fun obtainResource(argument: Argument): Expected -} - -internal class ShieldResultCache( - private val shieldSpritesCache: ShieldSpritesCache = ShieldSpritesCache(), - private val shieldByteArrayCache: ShieldByteArrayCache = ShieldByteArrayCache(), -) : ResourceCache(40) { - override suspend fun obtainResource( - argument: RouteShieldToDownload - ): Expected { - return when (argument) { - is RouteShieldToDownload.MapboxDesign -> prepareMapboxDesignShield(argument) - is RouteShieldToDownload.MapboxLegacy -> prepareMapboxLegacyShield(argument) - } - } - - private suspend fun prepareMapboxDesignShield( - toDownload: RouteShieldToDownload.MapboxDesign - ): Expected { - val spriteUrl = toDownload.generateSpriteSheetUrl() - val shieldSpritesResult = shieldSpritesCache.getOrRequest(spriteUrl) - val shieldSprites = if (shieldSpritesResult.isValue) { - shieldSpritesResult.value!! - } else { - return ExpectedFactory.createError( - """ - Error when downloading image sprite. - url: $spriteUrl - result: ${shieldSpritesResult.error!!} - """.trimIndent() - ) - } - val sprite = toDownload.getSpriteFrom(shieldSprites) - ?: return ExpectedFactory.createError( - "Sprite not found for ${toDownload.mapboxShield.name()} in $shieldSprites." - ) - val placeholder = sprite.spriteAttributes().placeholder() - if (placeholder.isNullOrEmpty()) { - return ExpectedFactory.createError( - """ - Mapbox shield sprite placeholder was null or empty in: $sprite - """.trimIndent() - ) - } - - val mapboxShieldUrl = toDownload.url - return shieldByteArrayCache.getOrRequest(mapboxShieldUrl).mapValue { shieldByteArray -> - val svgJson = String(shieldByteArray) - val svg = appendTextToShield( - text = toDownload.mapboxShield.displayRef(), - shieldSvg = ShieldSvg.fromJson(svgJson).svg(), - textColor = toDownload.mapboxShield.textColor(), - placeholder = placeholder - ).toByteArray() - RouteShield.MapboxDesignedShield( - mapboxShieldUrl, - svg, - toDownload.mapboxShield, - sprite - ) - } - } - - private fun appendTextToShield( - text: String, - shieldSvg: String, - textColor: String, - placeholder: List - ): String { - val textTagX = placeholder[0] + placeholder[2] / 2 - val textTagY = placeholder[3] - val textSize = placeholder[3] - placeholder[1] + 3 - val shieldText = "\t$text" - return shieldSvg.replace("", shieldText.plus("")) - } - - private suspend fun prepareMapboxLegacyShield( - toDownload: RouteShieldToDownload.MapboxLegacy - ): Expected { - val shieldUrl = toDownload.url - return shieldByteArrayCache.getOrRequest(shieldUrl).mapValue { byteArray -> - RouteShield.MapboxLegacyShield( - toDownload.url, - byteArray, - toDownload.initialUrl - ) - } - } -} - -internal class ShieldSpritesCache : ResourceCache(8) { - override suspend fun obtainResource(argument: String): Expected { - val result = RoadShieldDownloader.download(argument) - return try { - result.mapValue { data -> - val spriteJson = String(data) - val shieldSprites = ShieldSprites.fromJson(spriteJson) - shieldSprites - } - } catch (exception: JsonSyntaxException) { - val json = result.value?.let { String(it) } ?: "null" - ExpectedFactory.createError( - """ - |Error parsing shield sprites: - |exception: $exception - |json: $json - """.trimMargin() - ) - } - } -} - -internal class ShieldByteArrayCache : ResourceCache(15) { - override suspend fun obtainResource(argument: String): Expected { - return RoadShieldDownloader.download(argument) - } -} diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldDownloader.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/RoadShieldDownloader.kt similarity index 67% rename from libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldDownloader.kt rename to libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/RoadShieldDownloader.kt index 6f54dd45c5d..54f2226f111 100644 --- a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldDownloader.kt +++ b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/RoadShieldDownloader.kt @@ -1,8 +1,7 @@ -package com.mapbox.navigation.ui.shield +package com.mapbox.navigation.ui.shield.internal import com.mapbox.bindgen.Expected -import com.mapbox.bindgen.ExpectedFactory.createError -import com.mapbox.bindgen.ExpectedFactory.createValue +import com.mapbox.bindgen.ExpectedFactory import com.mapbox.common.ResourceLoadStatus import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoadRequest import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoader @@ -21,19 +20,19 @@ internal object RoadShieldDownloader { ResourceLoadStatus.AVAILABLE -> { val blob: ByteArray = responseData.data?.data ?: byteArrayOf() if (blob.isNotEmpty()) { - createValue(blob) + ExpectedFactory.createValue(blob) } else { - createError("No data available.") + ExpectedFactory.createError("No data available.") } } ResourceLoadStatus.UNAUTHORIZED -> - createError("Your token cannot access this resource.") + ExpectedFactory.createError("Your token cannot access this resource.") ResourceLoadStatus.NOT_FOUND -> - createError("Resource is missing.") + ExpectedFactory.createError("Resource is missing.") else -> - createError("Unknown error (status: ${responseData.status}).") + ExpectedFactory.createError("Unknown error (status: ${responseData.status}).") } - } ?: createError(response.error?.message ?: "No data available.") + } ?: ExpectedFactory.createError(response.error?.message ?: "No data available.") } private suspend fun ResourceLoader.load(url: String) = load(ResourceLoadRequest(url)) diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/CachedResourceLoader.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/CachedResourceLoader.kt new file mode 100644 index 00000000000..1d8dffa946b --- /dev/null +++ b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/CachedResourceLoader.kt @@ -0,0 +1,26 @@ +package com.mapbox.navigation.ui.shield.internal.loader + +import android.util.LruCache +import com.mapbox.bindgen.Expected + +/** + * Resource Loader backed by LruCache + */ +internal class CachedResourceLoader( + cacheSize: Int, + private val loader: ResourceLoader +) : ResourceLoader { + + private val cache = LruCache>(cacheSize) + + override suspend fun load(argument: Argument): Expected { + var value = cache.get(argument) + if (value != null) { + return value + } + + value = loader.load(argument) + cache.put(argument, value) + return value + } +} diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceDownloader.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceDownloader.kt new file mode 100644 index 00000000000..f3f337848b8 --- /dev/null +++ b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceDownloader.kt @@ -0,0 +1,43 @@ +package com.mapbox.navigation.ui.shield.internal.loader + +import com.mapbox.bindgen.Expected +import com.mapbox.bindgen.ExpectedFactory +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +private typealias LoaderCallback = (Expected) -> Unit + +internal abstract class ResourceDownloader : ResourceLoader { + internal companion object { + internal const val CANCELED_MESSAGE = "canceled" + } + + private val ongoingRequest = mutableMapOf>>() + + override suspend fun load(argument: I): Expected { + return if (ongoingRequest.contains(argument)) { + suspendCancellableCoroutine { continuation -> + val callback: LoaderCallback = { result -> + continuation.resume(result) + } + + ongoingRequest[argument]?.add(callback) + continuation.invokeOnCancellation { + ongoingRequest[argument]?.remove(callback) + } + } + } else { + ongoingRequest[argument] = mutableListOf() + val result = try { + download(argument) + } catch (ex: CancellationException) { + ExpectedFactory.createError(CANCELED_MESSAGE) + } + ongoingRequest.remove(argument)?.onEach { it(result) } + result + } + } + + protected abstract suspend fun download(argument: I): Expected +} diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceLoader.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceLoader.kt new file mode 100644 index 00000000000..97209309892 --- /dev/null +++ b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceLoader.kt @@ -0,0 +1,7 @@ +package com.mapbox.navigation.ui.shield.internal.loader + +import com.mapbox.bindgen.Expected + +internal fun interface ResourceLoader { + suspend fun load(argument: Argument): Expected +} diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/RoadShieldLoader.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/RoadShieldLoader.kt new file mode 100644 index 00000000000..8e2a71b685f --- /dev/null +++ b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/RoadShieldLoader.kt @@ -0,0 +1,98 @@ +package com.mapbox.navigation.ui.shield.internal.loader + +import com.mapbox.api.directions.v5.models.ShieldSprites +import com.mapbox.api.directions.v5.models.ShieldSvg +import com.mapbox.bindgen.Expected +import com.mapbox.bindgen.ExpectedFactory +import com.mapbox.navigation.ui.shield.internal.model.RouteShieldToDownload +import com.mapbox.navigation.ui.shield.internal.model.generateSpriteSheetUrl +import com.mapbox.navigation.ui.shield.internal.model.getSpriteFrom +import com.mapbox.navigation.ui.shield.model.RouteShield + +internal class RoadShieldLoader( + private val spritesLoader: ResourceLoader, + private val imageLoader: ResourceLoader +) : ResourceLoader { + + override suspend fun load(argument: RouteShieldToDownload): Expected { + return when (argument) { + is RouteShieldToDownload.MapboxDesign -> loadMapboxDesignShield(argument) + is RouteShieldToDownload.MapboxLegacy -> loadMapboxLegacyShield(argument) + } + } + + private suspend fun loadMapboxDesignShield( + toDownload: RouteShieldToDownload.MapboxDesign + ): Expected { + val spriteUrl = toDownload.generateSpriteSheetUrl() + val shieldSpritesResult = spritesLoader.load(spriteUrl) + val shieldSprites = if (shieldSpritesResult.isValue) { + shieldSpritesResult.value!! + } else { + return ExpectedFactory.createError( + """ + Error when downloading image sprite. + url: $spriteUrl + result: ${shieldSpritesResult.error!!} + """.trimIndent() + ) + } + val sprite = toDownload.getSpriteFrom(shieldSprites) + ?: return ExpectedFactory.createError( + "Sprite not found for ${toDownload.mapboxShield.name()} in $shieldSprites." + ) + val placeholder = sprite.spriteAttributes().placeholder() + if (placeholder.isNullOrEmpty()) { + return ExpectedFactory.createError( + """ + Mapbox shield sprite placeholder was null or empty in: $sprite + """.trimIndent() + ) + } + + val mapboxShieldUrl = toDownload.url + return imageLoader.load(mapboxShieldUrl).mapValue { shieldByteArray -> + val svgJson = String(shieldByteArray) + val svg = appendTextToShield( + text = toDownload.mapboxShield.displayRef(), + shieldSvg = ShieldSvg.fromJson(svgJson).svg(), + textColor = toDownload.mapboxShield.textColor(), + placeholder = placeholder + ).toByteArray() + RouteShield.MapboxDesignedShield( + mapboxShieldUrl, + svg, + toDownload.mapboxShield, + sprite + ) + } + } + + private fun appendTextToShield( + text: String, + shieldSvg: String, + textColor: String, + placeholder: List + ): String { + val textTagX = placeholder[0] + placeholder[2] / 2 + val textTagY = placeholder[3] + val textSize = placeholder[3] - placeholder[1] + 3 + val shieldText = "\t$text" + return shieldSvg.replace("", shieldText.plus("")) + } + + private suspend fun loadMapboxLegacyShield( + toDownload: RouteShieldToDownload.MapboxLegacy + ): Expected { + val shieldUrl = toDownload.url + return imageLoader.load(shieldUrl).mapValue { byteArray -> + RouteShield.MapboxLegacyShield( + toDownload.url, + byteArray, + toDownload.initialUrl + ) + } + } +} diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ShieldSpritesDownloader.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ShieldSpritesDownloader.kt new file mode 100644 index 00000000000..10c9fbfb1e5 --- /dev/null +++ b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ShieldSpritesDownloader.kt @@ -0,0 +1,29 @@ +package com.mapbox.navigation.ui.shield.internal.loader + +import com.google.gson.JsonSyntaxException +import com.mapbox.api.directions.v5.models.ShieldSprites +import com.mapbox.bindgen.Expected +import com.mapbox.bindgen.ExpectedFactory +import com.mapbox.navigation.ui.shield.internal.RoadShieldDownloader + +internal class ShieldSpritesDownloader : ResourceDownloader() { + override suspend fun download(argument: String): Expected { + val result = RoadShieldDownloader.download(argument) + return try { + result.mapValue { data -> + val spriteJson = String(data) + val shieldSprites = ShieldSprites.fromJson(spriteJson) + shieldSprites + } + } catch (exception: JsonSyntaxException) { + val json = result.value?.let { String(it) } ?: "null" + ExpectedFactory.createError( + """ + |Error parsing shield sprites: + |exception: $exception + |json: $json + """.trimMargin() + ) + } + } +} diff --git a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerImplTest.kt b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerImplTest.kt index 25aa61d4c55..a9200883ece 100644 --- a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerImplTest.kt +++ b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerImplTest.kt @@ -5,6 +5,7 @@ import com.mapbox.api.directions.v5.models.ShieldSprite import com.mapbox.bindgen.Expected import com.mapbox.bindgen.ExpectedFactory import com.mapbox.navigation.testing.MainCoroutineRule +import com.mapbox.navigation.ui.shield.internal.loader.ResourceLoader import com.mapbox.navigation.ui.shield.internal.model.RouteShieldToDownload import com.mapbox.navigation.ui.shield.model.RouteShield import com.mapbox.navigation.ui.shield.model.RouteShieldError @@ -18,6 +19,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.junit.Assert.assertEquals +import org.junit.Before import org.junit.Rule import org.junit.Test @@ -27,12 +29,18 @@ class RoadShieldContentManagerImplTest { @get:Rule var coroutineRule = MainCoroutineRule() + private lateinit var sut: RoadShieldContentManagerImpl + private lateinit var loader: ResourceLoader + + @Before + fun setUp() { + loader = mockk() + sut = RoadShieldContentManagerImpl(loader) + } + @Test fun `request waits for all results to be available (success and manual cancellation), async`() = coroutineRule.runBlockingTest { - val cache = mockk() - val contentManager = createContentManager(cache) - val legacyUrl = "url_legacy" val downloadUrl = legacyUrl.plus(".svg") val toDownloadLegacy = mockk { @@ -53,7 +61,7 @@ class RoadShieldContentManagerImplTest { ) ) coEvery { - cache.getOrRequest(toDownloadLegacy) + loader.load(toDownloadLegacy) } coAnswers { delay(500L) ExpectedFactory.createValue(expectedLegacyShield) @@ -69,7 +77,7 @@ class RoadShieldContentManagerImplTest { RoadShieldContentManagerImpl.CANCELED_MESSAGE ) coEvery { - cache.getOrRequest(toDownloadDesign) + loader.load(toDownloadDesign) } coAnswers { try { delay(1000L) @@ -82,13 +90,13 @@ class RoadShieldContentManagerImplTest { var result: List>? = null pauseDispatcher { launch { - result = contentManager.getShields(listOf(toDownloadLegacy, toDownloadDesign)) + result = sut.getShields(listOf(toDownloadLegacy, toDownloadDesign)) } // run coroutines until they hit the download delays runCurrent() // advance by enough to only download one shield advanceTimeBy(600L) - contentManager.cancelAll() + sut.cancelAll() } assertEquals(expectedLegacyResult, result!![0].value) assertEquals(expectedDesignResult, result!![1].error) @@ -97,9 +105,6 @@ class RoadShieldContentManagerImplTest { @Test fun `request waits for all results to be available (success and job cancellation), async`() = coroutineRule.runBlockingTest { - val cache = mockk() - val contentManager = createContentManager(cache) - val legacyUrl = "url_legacy" val downloadUrl = legacyUrl.plus(".svg") val toDownloadLegacy = mockk { @@ -120,7 +125,7 @@ class RoadShieldContentManagerImplTest { ) ) coEvery { - cache.getOrRequest(toDownloadLegacy) + loader.load(toDownloadLegacy) } coAnswers { delay(500L) ExpectedFactory.createValue(expectedLegacyShield) @@ -136,7 +141,7 @@ class RoadShieldContentManagerImplTest { RoadShieldContentManagerImpl.CANCELED_MESSAGE ) coEvery { - cache.getOrRequest(toDownloadDesign) + loader.load(toDownloadDesign) } coAnswers { delay(1000L) ExpectedFactory.createError("error") @@ -145,7 +150,7 @@ class RoadShieldContentManagerImplTest { var result: List>? = null pauseDispatcher { val job = launch { - result = contentManager.getShields(listOf(toDownloadLegacy, toDownloadDesign)) + result = sut.getShields(listOf(toDownloadLegacy, toDownloadDesign)) } // run coroutines until they hit the download delays runCurrent() @@ -161,9 +166,6 @@ class RoadShieldContentManagerImplTest { @Test fun `request waits for all results to be available (success and failure), async`() = coroutineRule.runBlockingTest { - val cache = mockk() - val contentManager = createContentManager(cache) - val legacyUrl = "url_legacy" val downloadUrl = legacyUrl.plus(".svg") val toDownloadLegacy = mockk { @@ -184,7 +186,7 @@ class RoadShieldContentManagerImplTest { ) ) coEvery { - cache.getOrRequest(toDownloadLegacy) + loader.load(toDownloadLegacy) } coAnswers { delay(1000L) ExpectedFactory.createValue(expectedLegacyShield) @@ -200,7 +202,7 @@ class RoadShieldContentManagerImplTest { "error" ) coEvery { - cache.getOrRequest(toDownloadDesign) + loader.load(toDownloadDesign) } coAnswers { delay(500L) ExpectedFactory.createError("error") @@ -208,7 +210,7 @@ class RoadShieldContentManagerImplTest { var result: List>? = null pauseDispatcher { - result = contentManager.getShields(listOf(toDownloadLegacy, toDownloadDesign)) + result = sut.getShields(listOf(toDownloadLegacy, toDownloadDesign)) } assertEquals(expectedLegacyResult, result!![0].value) assertEquals(expectedDesignResult, result!![1].error) @@ -217,9 +219,6 @@ class RoadShieldContentManagerImplTest { @Test fun `request waits for all results to be available (success and failure), sync`() = coroutineRule.runBlockingTest { - val cache = mockk() - val contentManager = createContentManager(cache) - val legacyUrl = "url_legacy" val downloadUrl = legacyUrl.plus(".svg") val toDownloadLegacy = mockk { @@ -240,7 +239,7 @@ class RoadShieldContentManagerImplTest { ) ) coEvery { - cache.getOrRequest(toDownloadLegacy) + loader.load(toDownloadLegacy) } returns ExpectedFactory.createValue(expectedLegacyShield) val designShieldUrl = "url" @@ -253,10 +252,10 @@ class RoadShieldContentManagerImplTest { "error" ) coEvery { - cache.getOrRequest(toDownloadDesign) + loader.load(toDownloadDesign) } returns ExpectedFactory.createError("error") - val result = contentManager.getShields(listOf(toDownloadLegacy, toDownloadDesign)) + val result = sut.getShields(listOf(toDownloadLegacy, toDownloadDesign)) assertEquals(expectedLegacyResult, result[0].value) assertEquals(expectedDesignResult, result[1].error) @@ -265,9 +264,6 @@ class RoadShieldContentManagerImplTest { @Test fun `unsuccessful design shield results with successful fallback`() = coroutineRule.runBlockingTest { - val cache = mockk() - val contentManager = createContentManager(cache) - val initialUrl = "url_legacy" val downloadUrl = initialUrl.plus(".svg") val expectedLegacyShield = RouteShield.MapboxLegacyShield( @@ -291,13 +287,13 @@ class RoadShieldContentManagerImplTest { ) ) coEvery { - cache.getOrRequest(toDownload) + loader.load(toDownload) } returns ExpectedFactory.createError("error") coEvery { - cache.getOrRequest(legacyToDownload) + loader.load(legacyToDownload) } returns ExpectedFactory.createValue(expectedLegacyShield) - val result = contentManager.getShields(listOf(toDownload)) + val result = sut.getShields(listOf(toDownload)) assertEquals(expectedResult, result.first().value) } @@ -305,9 +301,6 @@ class RoadShieldContentManagerImplTest { @Test fun `unsuccessful design shield results with unsuccessful fallback`() = coroutineRule.runBlockingTest { - val cache = mockk() - val contentManager = createContentManager(cache) - val legacyUrl = "url_legacy" val legacyToDownload = RouteShieldToDownload.MapboxLegacy(legacyUrl) val shieldUrl = "url_design" @@ -318,32 +311,29 @@ class RoadShieldContentManagerImplTest { val expectedResult = RouteShieldError( shieldUrl, """ - |original request failed with: - |url: url_design - |error: error - | - |fallback request failed with: - |url: url_legacy.svg - |error: error_legacy + |original request failed with: + |url: url_design + |error: error + | + |fallback request failed with: + |url: url_legacy.svg + |error: error_legacy """.trimMargin() ) coEvery { - cache.getOrRequest(toDownload) + loader.load(toDownload) } returns ExpectedFactory.createError("error") coEvery { - cache.getOrRequest(legacyToDownload) + loader.load(legacyToDownload) } returns ExpectedFactory.createError("error_legacy") - val result = contentManager.getShields(listOf(toDownload)) + val result = sut.getShields(listOf(toDownload)) assertEquals(expectedResult, result.first().error) } @Test fun `unsuccessful design shield results without fallback`() = coroutineRule.runBlockingTest { - val cache = mockk() - val contentManager = createContentManager(cache) - val shieldUrl = "url_design" val toDownload = mockk { every { url } returns shieldUrl @@ -355,19 +345,16 @@ class RoadShieldContentManagerImplTest { ) coEvery { - cache.getOrRequest(toDownload) + loader.load(toDownload) } returns ExpectedFactory.createError("error") - val result = contentManager.getShields(listOf(toDownload)) + val result = sut.getShields(listOf(toDownload)) assertEquals(expectedResult, result.first().error) } @Test fun `successful design shield results`() = coroutineRule.runBlockingTest { - val cache = mockk() - val contentManager = createContentManager(cache) - val shieldUrl = "url_design" val mapboxShield = mockk() val sprite = mockk() @@ -390,19 +377,16 @@ class RoadShieldContentManagerImplTest { ) coEvery { - cache.getOrRequest(toDownload) + loader.load(toDownload) } returns ExpectedFactory.createValue(expectedShield) - val result = contentManager.getShields(listOf(toDownload)) + val result = sut.getShields(listOf(toDownload)) assertEquals(expectedResult, result.first().value) } @Test fun `successful legacy shield results`() = coroutineRule.runBlockingTest { - val cache = mockk() - val contentManager = createContentManager(cache) - val initialUrl = "url_legacy" val downloadUrl = initialUrl.plus(".svg") val toDownload = RouteShieldToDownload.MapboxLegacy(initialUrl) @@ -421,19 +405,16 @@ class RoadShieldContentManagerImplTest { ) coEvery { - cache.getOrRequest(toDownload) + loader.load(toDownload) } returns ExpectedFactory.createValue(expectedShield) - val result = contentManager.getShields(listOf(toDownload)) + val result = sut.getShields(listOf(toDownload)) assertEquals(expectedResult, result.first().value) } @Test fun `unsuccessful legacy shield results`() = coroutineRule.runBlockingTest { - val cache = mockk() - val contentManager = createContentManager(cache) - val initialUrl = "url_legacy" val toDownload = RouteShieldToDownload.MapboxLegacy(initialUrl) val expectedResult = RouteShieldError( @@ -442,15 +423,11 @@ class RoadShieldContentManagerImplTest { ) coEvery { - cache.getOrRequest(toDownload) + loader.load(toDownload) } returns ExpectedFactory.createError("error") - val result = contentManager.getShields(listOf(toDownload)) + val result = sut.getShields(listOf(toDownload)) assertEquals(expectedResult, result.first().error) } - - private fun createContentManager( - shieldResultCache: ShieldResultCache = mockk() - ): RoadShieldContentManagerImpl = RoadShieldContentManagerImpl(shieldResultCache) } diff --git a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/RoadShieldDownloaderTest.kt b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/RoadShieldDownloaderTest.kt index 3d2cee19da3..e1db39cb3a2 100644 --- a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/RoadShieldDownloaderTest.kt +++ b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/RoadShieldDownloaderTest.kt @@ -6,6 +6,7 @@ import com.mapbox.common.ResourceData import com.mapbox.common.ResourceLoadError import com.mapbox.common.ResourceLoadResult import com.mapbox.common.ResourceLoadStatus +import com.mapbox.navigation.ui.shield.internal.RoadShieldDownloader import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoadCallback import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoadRequest import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoader diff --git a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/ShieldByteArrayCacheTest.kt b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/ShieldByteArrayCacheTest.kt deleted file mode 100644 index 92d24166e6e..00000000000 --- a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/ShieldByteArrayCacheTest.kt +++ /dev/null @@ -1,315 +0,0 @@ -package com.mapbox.navigation.ui.shield - -import com.mapbox.bindgen.Expected -import com.mapbox.bindgen.ExpectedFactory -import com.mapbox.navigation.testing.MainCoroutineRule -import io.mockk.Call -import io.mockk.MockKAnswerScope -import io.mockk.coEvery -import io.mockk.coVerify -import io.mockk.coVerifyOrder -import io.mockk.mockkObject -import io.mockk.unmockkObject -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import org.junit.After -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner - -private typealias Result = Expected - -private const val RequestDelay = 500L - -/** - * This tests the entire logic of [ResourceCache] since [ShieldByteArrayCache] is the simplest implementation. - */ -@OptIn(ExperimentalCoroutinesApi::class) -@RunWith(RobolectricTestRunner::class) -class ShieldByteArrayCacheTest { - - @get:Rule - var coroutineRule = MainCoroutineRule() - - private val cache = ShieldByteArrayCache() - private val downloadValueResult = ExpectedFactory.createValue(byteArrayOf()) - private val downloadErrorResult = ExpectedFactory.createError("error") - private val downloadValueAsyncAnswer: - suspend MockKAnswerScope.(Call) -> Result = { - delay(RequestDelay) - downloadValueResult - } - private val downloadErrorAsyncAnswer: - suspend MockKAnswerScope.(Call) -> Result = { - delay(RequestDelay) - downloadErrorResult - } - - @Before - fun setup() { - mockkObject(RoadShieldDownloader) - } - - @Test - fun `single request returns success`() = coroutineRule.runBlockingTest { - val argument = "url" - coEvery { RoadShieldDownloader.download(argument) } returns downloadValueResult - - val result = cache.getOrRequest(argument) - - assertEquals(downloadValueResult.value, result.value) - } - - @Test - fun `single request returns failure`() = coroutineRule.runBlockingTest { - val argument = "url" - coEvery { RoadShieldDownloader.download(argument) } returns downloadErrorResult - - val result = cache.getOrRequest(argument) - - assertEquals(downloadErrorResult.error, result.error) - } - - @Test - fun `cache hit for previous success on synchronous attempt`() = - coroutineRule.runBlockingTest { - val argument = "url" - coEvery { RoadShieldDownloader.download(argument) } returns downloadValueResult - - val firstResult = cache.getOrRequest(argument) - val secondResult = cache.getOrRequest(argument) - - coVerify(exactly = 1) { - RoadShieldDownloader.download(argument) - } - assertEquals(downloadValueResult.value, firstResult.value) - assertEquals(downloadValueResult.value, secondResult.value) - } - - @Test - fun `retry for previous failure on synchronous attempt, still fails`() = - coroutineRule.runBlockingTest { - val argument = "url" - coEvery { RoadShieldDownloader.download(argument) } returns downloadErrorResult - - val firstResult = cache.getOrRequest(argument) - val secondResult = cache.getOrRequest(argument) - - // all coroutines are run synchronously, so the second one is not suspending - coVerify(exactly = 2) { - RoadShieldDownloader.download(argument) - } - assertEquals(downloadErrorResult.error, firstResult.error) - assertEquals(downloadErrorResult.error, secondResult.error) - } - - @Test - fun `retry for previous failure on synchronous attempt, succeeds`() = - coroutineRule.runBlockingTest { - val argument = "url" - - coEvery { RoadShieldDownloader.download(argument) } returns downloadErrorResult - val firstResult = cache.getOrRequest(argument) - coEvery { RoadShieldDownloader.download(argument) } returns downloadValueResult - val secondResult = cache.getOrRequest(argument) - - // all coroutines are run synchronously, so the second one is not suspending - coVerify(exactly = 2) { - RoadShieldDownloader.download(argument) - } - assertEquals(downloadErrorResult.error, firstResult.error) - assertEquals(downloadValueResult.value, secondResult.value) - } - - @Test - fun `duplicate async request awaits results for success`() = - coroutineRule.runBlockingTest { - val argument = "url" - coEvery { RoadShieldDownloader.download(argument) } coAnswers downloadValueAsyncAnswer - - var firstResult: Result? = null - var secondResult: Result? = null - pauseDispatcher { - launch { - firstResult = cache.getOrRequest(argument) - } - launch { - secondResult = cache.getOrRequest(argument) - } - } - - coVerify(exactly = 1) { - RoadShieldDownloader.download(argument) - } - assertEquals(downloadValueResult.value, firstResult!!.value) - assertEquals(downloadValueResult.value, secondResult!!.value) - } - - @Test - fun `duplicate async request awaits results for failure`() = - coroutineRule.runBlockingTest { - val argument = "url" - coEvery { RoadShieldDownloader.download(argument) } coAnswers downloadErrorAsyncAnswer - - var firstResult: Result? = null - var secondResult: Result? = null - pauseDispatcher { - launch { - firstResult = cache.getOrRequest(argument) - } - launch { - secondResult = cache.getOrRequest(argument) - } - } - - coVerify(exactly = 1) { - RoadShieldDownloader.download(argument) - } - assertEquals(downloadErrorResult.error, firstResult!!.error) - assertEquals(downloadErrorResult.error, secondResult!!.error) - } - - @Test - fun `requests for different resources are executed in parallel`() = - coroutineRule.runBlockingTest { - val argument1 = "url1" - val argument2 = "url2" - val expectedResult1 = ExpectedFactory.createValue(byteArrayOf(0, 1)) - val expectedResult2 = ExpectedFactory.createValue(byteArrayOf(2, 3)) - coEvery { RoadShieldDownloader.download(argument1) } coAnswers { - delay(1000L) - expectedResult1 - } - coEvery { RoadShieldDownloader.download(argument2) } coAnswers { - delay(500L) - expectedResult2 - } - - var firstResult: Result? = null - var secondResult: Result? = null - pauseDispatcher { - launch { - firstResult = cache.getOrRequest(argument1) - } - launch { - secondResult = cache.getOrRequest(argument2) - } - - // advance to let second get downloaded but not the first - advanceTimeBy(750L) - - // verify that indeed first was scheduled to be downloaded earlier than second - coVerifyOrder { - RoadShieldDownloader.download(argument1) - RoadShieldDownloader.download(argument2) - } - - // second (shorter) request should already finish while first (longer) is ongoing - assertEquals(expectedResult2.value, secondResult!!.value) - assertNull(firstResult) - } - - // both requests eventually finish - assertEquals(expectedResult1.value, firstResult!!.value) - assertEquals(expectedResult2.value, secondResult!!.value) - } - - @Test - fun `non-overlapping duplicate async request hit cache for success`() = - coroutineRule.runBlockingTest { - val argument = "url" - coEvery { RoadShieldDownloader.download(argument) } coAnswers downloadValueAsyncAnswer - - var firstResult: Result? = null - var secondResult: Result? = null - pauseDispatcher { - launch { - firstResult = cache.getOrRequest(argument) - } - // let first request finish - advanceTimeBy(RequestDelay + 100) - launch { - secondResult = cache.getOrRequest(argument) - } - } - - coVerify(exactly = 1) { - RoadShieldDownloader.download(argument) - } - assertEquals(downloadValueResult.value, firstResult!!.value) - assertEquals(downloadValueResult.value, secondResult!!.value) - } - - @Test - fun `non-overlapping duplicate async request retry for failure`() = - coroutineRule.runBlockingTest { - val argument = "url" - coEvery { RoadShieldDownloader.download(argument) } coAnswers downloadErrorAsyncAnswer - - var firstResult: Result? = null - var secondResult: Result? = null - pauseDispatcher { - launch { - firstResult = cache.getOrRequest(argument) - } - // let first request finish - advanceTimeBy(RequestDelay + 100) - coEvery { - RoadShieldDownloader.download(argument) - } coAnswers downloadValueAsyncAnswer - launch { - secondResult = cache.getOrRequest(argument) - } - } - - coVerify(exactly = 2) { - RoadShieldDownloader.download(argument) - } - assertEquals(downloadErrorResult.error, firstResult!!.error) - assertEquals(downloadValueResult.value, secondResult!!.value) - } - - /** - * This should be fixed so that instead of awaiting requests being canceled, - * they attempt to get the resource themselves if the ongoing request is canceled. - */ - @Test - fun `awaiting duplicate async request gets canceled if original is canceled`() = - coroutineRule.runBlockingTest { - val argument = "url" - coEvery { RoadShieldDownloader.download(argument) } coAnswers downloadValueAsyncAnswer - - var firstResult: Result? = null - var secondResult: Result? = null - pauseDispatcher { - val original = launch { - firstResult = cache.getOrRequest(argument) - } - // let first run by not finish yet - advanceTimeBy(RequestDelay - 100) - launch { - secondResult = cache.getOrRequest(argument) - } - // cancel the first one - original.cancel() - } - - coVerify(exactly = 1) { - RoadShieldDownloader.download(argument) - } - - // all request get canceled - assertEquals(ResourceCache.CANCELED_MESSAGE, firstResult!!.error) - assertEquals(ResourceCache.CANCELED_MESSAGE, secondResult!!.error) - } - - @After - fun tearDown() { - unmockkObject(RoadShieldDownloader) - } -} diff --git a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/CachedResourceLoaderTest.kt b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/CachedResourceLoaderTest.kt new file mode 100644 index 00000000000..2ad3b9263ee --- /dev/null +++ b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/CachedResourceLoaderTest.kt @@ -0,0 +1,50 @@ +package com.mapbox.navigation.ui.shield.internal.loader + +import com.mapbox.bindgen.ExpectedFactory.createValue +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(RobolectricTestRunner::class) +class CachedResourceLoaderTest { + + private lateinit var loader: ResourceLoader + private lateinit var sut: CachedResourceLoader + + @Before + fun setUp() { + loader = mockk { + coEvery { load(any()) } coAnswers { + createValue(firstArg().toString()) + } + } + sut = CachedResourceLoader(2, loader) + } + + @Test + fun `should load non-cached value`() = runBlockingTest { + val r = sut.load(1) + + coVerify(exactly = 1) { loader.load(1) } + assertEquals("1", r.value) + } + + @Test + fun `should return cached value`() = runBlockingTest { + val results = listOf( + sut.load(1), + sut.load(1) + ) + + coVerify(exactly = 1) { loader.load(1) } + assertEquals(listOf("1", "1"), results.map { it.value }) + } +} diff --git a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceDownloaderTest.kt b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceDownloaderTest.kt new file mode 100644 index 00000000000..e00c9a0ee39 --- /dev/null +++ b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceDownloaderTest.kt @@ -0,0 +1,83 @@ +package com.mapbox.navigation.ui.shield.internal.loader + +import com.mapbox.bindgen.Expected +import com.mapbox.bindgen.ExpectedFactory.createValue +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Assert.assertEquals +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class ResourceDownloaderTest { + + @Test + fun `load should call download method and return its value`() = runBlockingTest { + val sut = object : ResourceDownloader() { + override suspend fun download(argument: Int): Expected = + createValue("$argument") + } + val r = sut.load(1234) + + assertEquals("1234", r.value) + } + + @Test + fun `load should call download only once`() = + runBlockingTest { + val stateFlow = MutableStateFlow(null) + val downloadCalls = mutableListOf() + val sut = object : ResourceDownloader() { + override suspend fun download(argument: Int): Expected { + downloadCalls.add(argument) + val v = stateFlow.filter { it == "$argument" }.filterNotNull().first() + return createValue(v) + } + } + + val loaded = mutableListOf() + val j = launch { + launch { loaded.add(sut.load(1).value) } + launch { loaded.add(sut.load(1).value) } + } + stateFlow.value = "1" + j.join() + + assertEquals(1, downloadCalls.size) + } + + @Test + fun `load should notify all callbacks when the value has been downloaded`() = + runBlockingTest { + // Simulate scenario where resource (1) has been requested multiple times and takes longer + // to load than resource (2). + // + // |-------------(1)------(1)-----(2)-----------------[2]-[1]-------> + // resource 1: {-------------- loading 1 -----------} + // resource 2: {-- loading 2 --} + // + val stateFlow = MutableStateFlow(null) + val sut = object : ResourceDownloader() { + override suspend fun download(argument: Int): Expected { + val v = stateFlow.filter { it == "$argument" }.filterNotNull().first() + return createValue(v) + } + } + + val loaded = mutableListOf() + val j = launch { + launch { loaded.add(sut.load(1).value) } + launch { loaded.add(sut.load(1).value) } + launch { loaded.add(sut.load(2).value) } + } + stateFlow.value = "2" // simulate faster load time of the resource (2) + stateFlow.value = "1" + j.join() + + assertEquals(listOf("2", "1", "1"), loaded) + } +} diff --git a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/ShieldResultCacheTest.kt b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/RoadShieldLoaderTest.kt similarity index 79% rename from libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/ShieldResultCacheTest.kt rename to libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/RoadShieldLoaderTest.kt index b6a4012ba61..f531e2d9cbf 100644 --- a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/ShieldResultCacheTest.kt +++ b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/RoadShieldLoaderTest.kt @@ -1,9 +1,8 @@ -package com.mapbox.navigation.ui.shield +package com.mapbox.navigation.ui.shield.internal.loader import com.mapbox.api.directions.v5.models.ShieldSprite import com.mapbox.api.directions.v5.models.ShieldSprites import com.mapbox.bindgen.ExpectedFactory -import com.mapbox.navigation.testing.MainCoroutineRule import com.mapbox.navigation.ui.shield.internal.model.RouteShieldToDownload import com.mapbox.navigation.ui.shield.internal.model.generateSpriteSheetUrl import com.mapbox.navigation.ui.shield.internal.model.getSpriteFrom @@ -12,37 +11,43 @@ import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic -import io.mockk.unmockkStatic +import io.mockk.unmockkAll import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runBlockingTest import org.junit.After import org.junit.Assert.assertEquals import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner @OptIn(ExperimentalCoroutinesApi::class) @RunWith(RobolectricTestRunner::class) -class ShieldResultCacheTest { +class RoadShieldLoaderTest { - @get:Rule - var coroutineRule = MainCoroutineRule() + private lateinit var sut: RoadShieldLoader - private val shieldSpritesCache: ShieldSpritesCache = mockk() - private val shieldByteArrayCache: ShieldByteArrayCache = mockk() - - private val cache = ShieldResultCache(shieldSpritesCache, shieldByteArrayCache) + private lateinit var spritesLoader: ResourceLoader + private lateinit var imageLoader: ResourceLoader @Before fun setup() { + spritesLoader = mockk() + imageLoader = mockk() + sut = RoadShieldLoader(spritesLoader, imageLoader) + mockkStatic(RouteShieldToDownload.MapboxDesign::generateSpriteSheetUrl) mockkStatic(RouteShieldToDownload.MapboxDesign::getSpriteFrom) } + @After + fun tearDown() { + unmockkAll() + } + @Test @Suppress("MaxLineLength") - fun `design shield - success`() = coroutineRule.runBlockingTest { + fun `design shield - success`() = runBlockingTest { val rawShieldJson = """ {"svg":""} @@ -66,10 +71,10 @@ class ShieldResultCacheTest { } } coEvery { - shieldSpritesCache.getOrRequest(spriteUrl) + spritesLoader.load(spriteUrl) } returns ExpectedFactory.createValue(shieldSprites) coEvery { - shieldByteArrayCache.getOrRequest(shieldUrl) + imageLoader.load(shieldUrl) } returns ExpectedFactory.createValue(rawShieldJson.toByteArray()) val expectedShieldString = @@ -82,13 +87,13 @@ class ShieldResultCacheTest { mapboxShield = toDownload.mapboxShield, shieldSprite = shieldSprite ) - val result = cache.getOrRequest(toDownload) + val result = sut.load(toDownload) assertEquals(expected, result.value) } @Test - fun `design shield - failure - shield download failure`() = coroutineRule.runBlockingTest { + fun `design shield - failure - shield download failure`() = runBlockingTest { val shieldUrl = "shield-url" val spriteUrl = "sprite-url" val shieldSprites = mockk() @@ -107,26 +112,26 @@ class ShieldResultCacheTest { } } coEvery { - shieldSpritesCache.getOrRequest(spriteUrl) + spritesLoader.load(spriteUrl) } returns ExpectedFactory.createValue(shieldSprites) coEvery { - shieldByteArrayCache.getOrRequest(shieldUrl) + imageLoader.load(shieldUrl) } returns ExpectedFactory.createError("error") val expected = "error" - val result = cache.getOrRequest(toDownload) + val result = sut.load(toDownload) assertEquals(expected, result.error) } @Test - fun `design shield - failure - missing sprites`() = coroutineRule.runBlockingTest { + fun `design shield - failure - missing sprites`() = runBlockingTest { val spriteUrl = "sprite-url" val toDownload = mockk { every { generateSpriteSheetUrl() } returns spriteUrl } coEvery { - shieldSpritesCache.getOrRequest(spriteUrl) + spritesLoader.load(spriteUrl) } returns ExpectedFactory.createError("error") val expected = """ @@ -134,13 +139,13 @@ class ShieldResultCacheTest { url: $spriteUrl result: error """.trimIndent() - val result = cache.getOrRequest(toDownload) + val result = sut.load(toDownload) assertEquals(expected, result.error) } @Test - fun `design shield - failure - missing sprite`() = coroutineRule.runBlockingTest { + fun `design shield - failure - missing sprite`() = runBlockingTest { val spriteUrl = "sprite-url" val shieldSprites = mockk() val toDownload = mockk { @@ -151,17 +156,17 @@ class ShieldResultCacheTest { } } coEvery { - shieldSpritesCache.getOrRequest(spriteUrl) + spritesLoader.load(spriteUrl) } returns ExpectedFactory.createValue(shieldSprites) val expected = "Sprite not found for ${toDownload.mapboxShield.name()} in $shieldSprites." - val result = cache.getOrRequest(toDownload) + val result = sut.load(toDownload) assertEquals(expected, result.error) } @Test - fun `design shield - failure - missing placeholder`() = coroutineRule.runBlockingTest { + fun `design shield - failure - missing placeholder`() = runBlockingTest { val spriteUrl = "sprite-url" val shieldSprites = mockk() val shieldSprite = mockk { @@ -174,17 +179,17 @@ class ShieldResultCacheTest { every { getSpriteFrom(shieldSprites) } returns shieldSprite } coEvery { - shieldSpritesCache.getOrRequest(spriteUrl) + spritesLoader.load(spriteUrl) } returns ExpectedFactory.createValue(shieldSprites) val expected = "Mapbox shield sprite placeholder was null or empty in: $shieldSprite" - val result = cache.getOrRequest(toDownload) + val result = sut.load(toDownload) assertEquals(expected, result.error) } @Test - fun `legacy shield - success`() = coroutineRule.runBlockingTest { + fun `legacy shield - success`() = runBlockingTest { val shieldByteArray = byteArrayOf() val shieldUrl = "shield-url" val toDownload = mockk { @@ -192,7 +197,7 @@ class ShieldResultCacheTest { every { url } returns shieldUrl.plus(".svg") } coEvery { - shieldByteArrayCache.getOrRequest(shieldUrl.plus(".svg")) + imageLoader.load(shieldUrl.plus(".svg")) } returns ExpectedFactory.createValue(shieldByteArray) val expected = RouteShield.MapboxLegacyShield( @@ -200,30 +205,24 @@ class ShieldResultCacheTest { byteArray = shieldByteArray, initialUrl = shieldUrl ) - val result = cache.getOrRequest(toDownload) + val result = sut.load(toDownload) assertEquals(expected, result.value) } @Test - fun `legacy shield - failure`() = coroutineRule.runBlockingTest { + fun `legacy shield - failure`() = runBlockingTest { val shieldUrl = "shield-url" val toDownload = mockk { every { url } returns shieldUrl } coEvery { - shieldByteArrayCache.getOrRequest(shieldUrl) + imageLoader.load(shieldUrl) } returns ExpectedFactory.createError("error") val expected = "error" - val result = cache.getOrRequest(toDownload) + val result = sut.load(toDownload) assertEquals(expected, result.error) } - - @After - fun tearDown() { - unmockkStatic(RouteShieldToDownload.MapboxDesign::generateSpriteSheetUrl) - unmockkStatic(RouteShieldToDownload.MapboxDesign::getSpriteFrom) - } } diff --git a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/ShieldSpritesCacheTest.kt b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/ShieldSpritesDownloaderTest.kt similarity index 75% rename from libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/ShieldSpritesCacheTest.kt rename to libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/ShieldSpritesDownloaderTest.kt index 34714066e43..7a7f5469643 100644 --- a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/ShieldSpritesCacheTest.kt +++ b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/ShieldSpritesDownloaderTest.kt @@ -1,53 +1,56 @@ -package com.mapbox.navigation.ui.shield +package com.mapbox.navigation.ui.shield.internal.loader import com.mapbox.api.directions.v5.models.ShieldSprites import com.mapbox.bindgen.ExpectedFactory -import com.mapbox.navigation.testing.MainCoroutineRule +import com.mapbox.navigation.ui.shield.internal.RoadShieldDownloader import io.mockk.coEvery import io.mockk.mockkObject -import io.mockk.unmockkObject +import io.mockk.unmockkAll import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runBlockingTest import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before -import org.junit.Rule import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) -class ShieldSpritesCacheTest { +class ShieldSpritesDownloaderTest { - @get:Rule - var coroutineRule = MainCoroutineRule() - - private val cache = ShieldSpritesCache() + private lateinit var sut: ShieldSpritesDownloader @Before fun setup() { mockkObject(RoadShieldDownloader) + sut = ShieldSpritesDownloader() + } + + @After + fun tearDown() { + unmockkAll() } @Test - fun `download error`() = coroutineRule.runBlockingTest { + fun `download error`() = runBlockingTest { val argument = "url" coEvery { RoadShieldDownloader.download(argument) } returns ExpectedFactory.createError("error") - val result = cache.getOrRequest(argument) + val result = sut.load(argument) assertEquals("error", result.error) } @Test - fun `parsing error`() = coroutineRule.runBlockingTest { + fun `parsing error`() = runBlockingTest { val argument = "url" coEvery { RoadShieldDownloader.download(argument) } returns ExpectedFactory.createValue("wrong json".toByteArray()) - val result = cache.getOrRequest(argument) + val result = sut.load(argument) assertTrue( result.error!!.startsWith("Error parsing shield sprites:") @@ -55,7 +58,7 @@ class ShieldSpritesCacheTest { } @Test - fun `download success`() = coroutineRule.runBlockingTest { + fun `download success`() = runBlockingTest { val argument = "url" val expectedResult = ExpectedFactory.createValue( """ @@ -86,13 +89,8 @@ class ShieldSpritesCacheTest { expectedResult } - val result = cache.getOrRequest(argument) + val result = sut.load(argument) assertEquals(ShieldSprites.fromJson(String(expectedResult.value!!)), result.value) } - - @After - fun tearDown() { - unmockkObject(RoadShieldDownloader) - } } From b8a665570de82fec3c7ef1e8eaccacf4cdf5764a Mon Sep 17 00:00:00 2001 From: Tomasz Rybakiewicz Date: Tue, 17 Jan 2023 11:46:19 -0500 Subject: [PATCH 2/2] Renamed ResourceLoader -> Loader, ResourceDownloader -> Downloader. Updated return type to return Expected instead of Expected --- .../RoadShieldContentManagerContainer.kt | 6 +- .../ui/shield/RoadShieldContentManagerImpl.kt | 174 ++++++++++-------- .../shield/internal/RoadShieldDownloader.kt | 17 +- .../ui/shield/internal/loader/CachedLoader.kt | 26 +++ .../internal/loader/CachedResourceLoader.kt | 26 --- .../ui/shield/internal/loader/Downloader.kt | 43 +++++ .../ui/shield/internal/loader/Loader.kt | 7 + .../internal/loader/ResourceDownloader.kt | 43 ----- .../shield/internal/loader/ResourceLoader.kt | 7 - .../internal/loader/RoadShieldLoader.kt | 29 +-- .../loader/ShieldSpritesDownloader.kt | 17 +- .../RoadShieldContentManagerImplTest.kt | 26 +-- .../ui/shield/RoadShieldDownloaderTest.kt | 6 +- ...ourceLoaderTest.kt => CachedLoaderTest.kt} | 8 +- .../internal/loader/ResourceDownloaderTest.kt | 20 +- .../internal/loader/RoadShieldLoaderTest.kt | 20 +- .../loader/ShieldSpritesDownloaderTest.kt | 8 +- 17 files changed, 253 insertions(+), 230 deletions(-) create mode 100644 libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/CachedLoader.kt delete mode 100644 libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/CachedResourceLoader.kt create mode 100644 libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/Downloader.kt create mode 100644 libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/Loader.kt delete mode 100644 libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceDownloader.kt delete mode 100644 libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceLoader.kt rename libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/{CachedResourceLoaderTest.kt => CachedLoaderTest.kt} (85%) diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerContainer.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerContainer.kt index a2ea4d9bbce..856562dfe7f 100644 --- a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerContainer.kt +++ b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerContainer.kt @@ -1,7 +1,7 @@ package com.mapbox.navigation.ui.shield import com.mapbox.navigation.ui.shield.internal.RoadShieldDownloader -import com.mapbox.navigation.ui.shield.internal.loader.CachedResourceLoader +import com.mapbox.navigation.ui.shield.internal.loader.CachedLoader import com.mapbox.navigation.ui.shield.internal.loader.RoadShieldLoader import com.mapbox.navigation.ui.shield.internal.loader.ShieldSpritesDownloader import com.mapbox.navigation.ui.shield.internal.model.RouteShieldToDownload @@ -16,10 +16,10 @@ internal object RoadShieldContentManagerContainer : RoadShieldContentManager { private val contentManager: RoadShieldContentManager by lazy { RoadShieldContentManagerImpl( - shieldLoader = CachedResourceLoader( + shieldLoader = CachedLoader( IMAGES_CACHE_SIZE, RoadShieldLoader( - spritesLoader = CachedResourceLoader( + spritesLoader = CachedLoader( SPRITES_CACHE_SIZE, ShieldSpritesDownloader() ), diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerImpl.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerImpl.kt index 710b9b05f0a..5d23c226700 100644 --- a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerImpl.kt +++ b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/RoadShieldContentManagerImpl.kt @@ -2,8 +2,9 @@ package com.mapbox.navigation.ui.shield import com.mapbox.api.directions.v5.models.BannerComponents import com.mapbox.bindgen.Expected -import com.mapbox.bindgen.ExpectedFactory -import com.mapbox.navigation.ui.shield.internal.loader.ResourceLoader +import com.mapbox.bindgen.ExpectedFactory.createError +import com.mapbox.bindgen.ExpectedFactory.createValue +import com.mapbox.navigation.ui.shield.internal.loader.Loader import com.mapbox.navigation.ui.shield.internal.model.RouteShieldToDownload import com.mapbox.navigation.ui.shield.model.RouteShield import com.mapbox.navigation.ui.shield.model.RouteShieldError @@ -13,7 +14,7 @@ import com.mapbox.navigation.utils.internal.InternalJobControlFactory import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine -import java.util.UUID +import java.util.* import kotlin.coroutines.resume /** @@ -69,7 +70,7 @@ import kotlin.coroutines.resume * - If request fails: repeat step 1. */ internal class RoadShieldContentManagerImpl( - private val shieldLoader: ResourceLoader + private val shieldLoader: Loader ) : RoadShieldContentManager { internal companion object { internal const val CANCELED_MESSAGE = "canceled" @@ -97,7 +98,7 @@ internal class RoadShieldContentManagerImpl( } returnList.addAll( missingResults.map { - ExpectedFactory.createError( + createError( RouteShieldError(it.toDownload.url, CANCELED_MESSAGE) ) } @@ -118,90 +119,103 @@ internal class RoadShieldContentManagerImpl( mainJob.scope.launch { when (toDownload) { is RouteShieldToDownload.MapboxDesign -> { - val mapboxDesignShieldResult = shieldLoader.load(toDownload) - resultMap[request] = if (mapboxDesignShieldResult.isError) { - val legacyFallback = toDownload.legacyFallback - if (legacyFallback != null) { - shieldLoader.load(legacyFallback).fold( - { error -> - ExpectedFactory.createError( - RouteShieldError( - url = toDownload.url, - errorMessage = """ - |original request failed with: - |url: ${toDownload.url} - |error: ${mapboxDesignShieldResult.error} - | - |fallback request failed with: - |url: ${legacyFallback.url} - |error: $error - """.trimMargin() - ) - ) - }, - { legacyShield -> - ExpectedFactory.createValue( - RouteShieldResult( - legacyShield, - RouteShieldOrigin( - isFallback = true, - originalUrl = toDownload.url, - mapboxDesignShieldResult.error!! - ) - ) - ) - } - ) - } else { - ExpectedFactory.createError( - RouteShieldError( - url = toDownload.url, - errorMessage = mapboxDesignShieldResult.error!! - ) + resultMap[request] = loadDesignShield(toDownload) + } + is RouteShieldToDownload.MapboxLegacy -> { + resultMap[request] = loadLegacyShield(toDownload) + } + } + invalidate() + } + request + }.toSet() + } + + private suspend fun loadDesignShield( + toDownload: RouteShieldToDownload.MapboxDesign + ): Expected { + val mapboxDesignShieldResult = shieldLoader.load(toDownload) + return if (mapboxDesignShieldResult.isError) { + val legacyFallback = toDownload.legacyFallback + if (legacyFallback != null) { + shieldLoader.load(legacyFallback) + .fold( + { error -> + createError( + RouteShieldError( + toDownload.url, + """ + |original request failed with: + |url: ${toDownload.url} + |error: ${mapboxDesignShieldResult.error?.localizedMessage} + | + |fallback request failed with: + |url: ${legacyFallback.url} + |error: ${error.localizedMessage} + """.trimMargin() ) - } - } else { - ExpectedFactory.createValue( + ) + }, + { legacyShield -> + createValue( RouteShieldResult( - mapboxDesignShieldResult.value!!, + legacyShield, RouteShieldOrigin( - isFallback = false, - mapboxDesignShieldResult.value!!.url, - "" + true, + toDownload.url, + mapboxDesignShieldResult.error?.message ?: "" ) ) ) } - } - is RouteShieldToDownload.MapboxLegacy -> { - resultMap[request] = shieldLoader.load(toDownload).fold( - { error -> - ExpectedFactory.createError( - RouteShieldError( - url = toDownload.url, - errorMessage = error - ) - ) - }, - { legacyShield -> - ExpectedFactory.createValue( - RouteShieldResult( - legacyShield, - RouteShieldOrigin( - isFallback = false, - originalUrl = toDownload.url, - originalErrorMessage = "" - ) - ) - ) - } + ) + } else { + createError( + RouteShieldError( + toDownload.url, + mapboxDesignShieldResult.error?.message ?: "" + ) + ) + } + } else { + createValue( + RouteShieldResult( + mapboxDesignShieldResult.value!!, + RouteShieldOrigin( + false, + mapboxDesignShieldResult.value!!.url, + "" + ) + ) + ) + } + } + + private suspend fun loadLegacyShield( + toDownload: RouteShieldToDownload.MapboxLegacy + ): Expected { + return shieldLoader.load(toDownload).fold( + { error -> + createError( + RouteShieldError( + toDownload.url, + error.message ?: "" + ) + ) + }, + { legacyShield -> + createValue( + RouteShieldResult( + legacyShield, + RouteShieldOrigin( + false, + toDownload.url, + "" ) - } - } - invalidate() + ) + ) } - request - }.toSet() + ) } private fun invalidate() { diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/RoadShieldDownloader.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/RoadShieldDownloader.kt index 54f2226f111..64290d163d2 100644 --- a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/RoadShieldDownloader.kt +++ b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/RoadShieldDownloader.kt @@ -1,7 +1,8 @@ package com.mapbox.navigation.ui.shield.internal import com.mapbox.bindgen.Expected -import com.mapbox.bindgen.ExpectedFactory +import com.mapbox.bindgen.ExpectedFactory.createError +import com.mapbox.bindgen.ExpectedFactory.createValue import com.mapbox.common.ResourceLoadStatus import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoadRequest import com.mapbox.navigation.ui.utils.internal.resource.ResourceLoader @@ -12,7 +13,7 @@ internal object RoadShieldDownloader { private val resourceLoader get() = ResourceLoaderFactory.getInstance() - suspend fun download(url: String): Expected { + suspend fun download(url: String): Expected { val response = resourceLoader.load(url) return response.value?.let { responseData -> @@ -20,19 +21,19 @@ internal object RoadShieldDownloader { ResourceLoadStatus.AVAILABLE -> { val blob: ByteArray = responseData.data?.data ?: byteArrayOf() if (blob.isNotEmpty()) { - ExpectedFactory.createValue(blob) + createValue(blob) } else { - ExpectedFactory.createError("No data available.") + createError(Error("No data available.")) } } ResourceLoadStatus.UNAUTHORIZED -> - ExpectedFactory.createError("Your token cannot access this resource.") + createError(Error("Your token cannot access this resource.")) ResourceLoadStatus.NOT_FOUND -> - ExpectedFactory.createError("Resource is missing.") + createError(Error("Resource is missing.")) else -> - ExpectedFactory.createError("Unknown error (status: ${responseData.status}).") + createError(Error("Unknown error (status: ${responseData.status}).")) } - } ?: ExpectedFactory.createError(response.error?.message ?: "No data available.") + } ?: createError(Error(response.error?.message ?: "No data available.")) } private suspend fun ResourceLoader.load(url: String) = load(ResourceLoadRequest(url)) diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/CachedLoader.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/CachedLoader.kt new file mode 100644 index 00000000000..e2b311af5ed --- /dev/null +++ b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/CachedLoader.kt @@ -0,0 +1,26 @@ +package com.mapbox.navigation.ui.shield.internal.loader + +import android.util.LruCache +import com.mapbox.bindgen.Expected + +/** + * Loader backed by LruCache + */ +internal class CachedLoader( + cacheSize: Int, + private val loader: Loader +) : Loader { + + private val cache = LruCache>(cacheSize) + + override suspend fun load(input: Input): Expected { + var value = cache.get(input) + if (value != null) { + return value + } + + value = loader.load(input) + cache.put(input, value) + return value + } +} diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/CachedResourceLoader.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/CachedResourceLoader.kt deleted file mode 100644 index 1d8dffa946b..00000000000 --- a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/CachedResourceLoader.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.mapbox.navigation.ui.shield.internal.loader - -import android.util.LruCache -import com.mapbox.bindgen.Expected - -/** - * Resource Loader backed by LruCache - */ -internal class CachedResourceLoader( - cacheSize: Int, - private val loader: ResourceLoader -) : ResourceLoader { - - private val cache = LruCache>(cacheSize) - - override suspend fun load(argument: Argument): Expected { - var value = cache.get(argument) - if (value != null) { - return value - } - - value = loader.load(argument) - cache.put(argument, value) - return value - } -} diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/Downloader.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/Downloader.kt new file mode 100644 index 00000000000..2b84792b44e --- /dev/null +++ b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/Downloader.kt @@ -0,0 +1,43 @@ +package com.mapbox.navigation.ui.shield.internal.loader + +import com.mapbox.bindgen.Expected +import com.mapbox.bindgen.ExpectedFactory +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +private typealias Callback = (Expected) -> Unit + +internal abstract class Downloader : Loader { + internal companion object { + internal const val CANCELED_MESSAGE = "canceled" + } + + private val ongoingRequest = mutableMapOf>>() + + override suspend fun load(input: I): Expected { + return if (ongoingRequest.contains(input)) { + suspendCancellableCoroutine { continuation -> + val callback: Callback = { result -> + continuation.resume(result) + } + + ongoingRequest[input]?.add(callback) + continuation.invokeOnCancellation { + ongoingRequest[input]?.remove(callback) + } + } + } else { + ongoingRequest[input] = mutableListOf() + val result = try { + download(input) + } catch (ex: CancellationException) { + ExpectedFactory.createError(Error(CANCELED_MESSAGE, ex)) + } + ongoingRequest.remove(input)?.onEach { it(result) } + result + } + } + + protected abstract suspend fun download(input: I): Expected +} diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/Loader.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/Loader.kt new file mode 100644 index 00000000000..1dbd9c0281f --- /dev/null +++ b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/Loader.kt @@ -0,0 +1,7 @@ +package com.mapbox.navigation.ui.shield.internal.loader + +import com.mapbox.bindgen.Expected + +internal fun interface Loader { + suspend fun load(input: Input): Expected +} diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceDownloader.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceDownloader.kt deleted file mode 100644 index f3f337848b8..00000000000 --- a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceDownloader.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.mapbox.navigation.ui.shield.internal.loader - -import com.mapbox.bindgen.Expected -import com.mapbox.bindgen.ExpectedFactory -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlin.coroutines.resume - -private typealias LoaderCallback = (Expected) -> Unit - -internal abstract class ResourceDownloader : ResourceLoader { - internal companion object { - internal const val CANCELED_MESSAGE = "canceled" - } - - private val ongoingRequest = mutableMapOf>>() - - override suspend fun load(argument: I): Expected { - return if (ongoingRequest.contains(argument)) { - suspendCancellableCoroutine { continuation -> - val callback: LoaderCallback = { result -> - continuation.resume(result) - } - - ongoingRequest[argument]?.add(callback) - continuation.invokeOnCancellation { - ongoingRequest[argument]?.remove(callback) - } - } - } else { - ongoingRequest[argument] = mutableListOf() - val result = try { - download(argument) - } catch (ex: CancellationException) { - ExpectedFactory.createError(CANCELED_MESSAGE) - } - ongoingRequest.remove(argument)?.onEach { it(result) } - result - } - } - - protected abstract suspend fun download(argument: I): Expected -} diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceLoader.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceLoader.kt deleted file mode 100644 index 97209309892..00000000000 --- a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceLoader.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.mapbox.navigation.ui.shield.internal.loader - -import com.mapbox.bindgen.Expected - -internal fun interface ResourceLoader { - suspend fun load(argument: Argument): Expected -} diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/RoadShieldLoader.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/RoadShieldLoader.kt index 8e2a71b685f..7924b04478a 100644 --- a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/RoadShieldLoader.kt +++ b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/RoadShieldLoader.kt @@ -10,40 +10,40 @@ import com.mapbox.navigation.ui.shield.internal.model.getSpriteFrom import com.mapbox.navigation.ui.shield.model.RouteShield internal class RoadShieldLoader( - private val spritesLoader: ResourceLoader, - private val imageLoader: ResourceLoader -) : ResourceLoader { + private val spritesLoader: Loader, + private val imageLoader: Loader +) : Loader { - override suspend fun load(argument: RouteShieldToDownload): Expected { - return when (argument) { - is RouteShieldToDownload.MapboxDesign -> loadMapboxDesignShield(argument) - is RouteShieldToDownload.MapboxLegacy -> loadMapboxLegacyShield(argument) + override suspend fun load(input: RouteShieldToDownload): Expected { + return when (input) { + is RouteShieldToDownload.MapboxDesign -> loadMapboxDesignShield(input) + is RouteShieldToDownload.MapboxLegacy -> loadMapboxLegacyShield(input) } } private suspend fun loadMapboxDesignShield( toDownload: RouteShieldToDownload.MapboxDesign - ): Expected { + ): Expected { val spriteUrl = toDownload.generateSpriteSheetUrl() val shieldSpritesResult = spritesLoader.load(spriteUrl) val shieldSprites = if (shieldSpritesResult.isValue) { shieldSpritesResult.value!! } else { - return ExpectedFactory.createError( + return createError( """ Error when downloading image sprite. url: $spriteUrl - result: ${shieldSpritesResult.error!!} + result: ${shieldSpritesResult.error?.message} """.trimIndent() ) } val sprite = toDownload.getSpriteFrom(shieldSprites) - ?: return ExpectedFactory.createError( + ?: return createError( "Sprite not found for ${toDownload.mapboxShield.name()} in $shieldSprites." ) val placeholder = sprite.spriteAttributes().placeholder() if (placeholder.isNullOrEmpty()) { - return ExpectedFactory.createError( + return createError( """ Mapbox shield sprite placeholder was null or empty in: $sprite """.trimIndent() @@ -85,7 +85,7 @@ internal class RoadShieldLoader( private suspend fun loadMapboxLegacyShield( toDownload: RouteShieldToDownload.MapboxLegacy - ): Expected { + ): Expected { val shieldUrl = toDownload.url return imageLoader.load(shieldUrl).mapValue { byteArray -> RouteShield.MapboxLegacyShield( @@ -95,4 +95,7 @@ internal class RoadShieldLoader( ) } } + + private fun createError(errorMessage: String): Expected = + ExpectedFactory.createError(Error(errorMessage)) } diff --git a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ShieldSpritesDownloader.kt b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ShieldSpritesDownloader.kt index 10c9fbfb1e5..ec13aa1b5df 100644 --- a/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ShieldSpritesDownloader.kt +++ b/libnavui-shield/src/main/java/com/mapbox/navigation/ui/shield/internal/loader/ShieldSpritesDownloader.kt @@ -3,12 +3,12 @@ package com.mapbox.navigation.ui.shield.internal.loader import com.google.gson.JsonSyntaxException import com.mapbox.api.directions.v5.models.ShieldSprites import com.mapbox.bindgen.Expected -import com.mapbox.bindgen.ExpectedFactory +import com.mapbox.bindgen.ExpectedFactory.createError import com.mapbox.navigation.ui.shield.internal.RoadShieldDownloader -internal class ShieldSpritesDownloader : ResourceDownloader() { - override suspend fun download(argument: String): Expected { - val result = RoadShieldDownloader.download(argument) +internal class ShieldSpritesDownloader : Downloader() { + override suspend fun download(input: String): Expected { + val result = RoadShieldDownloader.download(input) return try { result.mapValue { data -> val spriteJson = String(data) @@ -17,12 +17,15 @@ internal class ShieldSpritesDownloader : ResourceDownloader + private lateinit var loader: Loader @Before fun setUp() { @@ -81,9 +81,11 @@ class RoadShieldContentManagerImplTest { } coAnswers { try { delay(1000L) - ExpectedFactory.createError("error") + ExpectedFactory.createError(Error("error")) } catch (ex: CancellationException) { - ExpectedFactory.createError(RoadShieldContentManagerImpl.CANCELED_MESSAGE) + ExpectedFactory.createError( + Error(RoadShieldContentManagerImpl.CANCELED_MESSAGE) + ) } } @@ -144,7 +146,7 @@ class RoadShieldContentManagerImplTest { loader.load(toDownloadDesign) } coAnswers { delay(1000L) - ExpectedFactory.createError("error") + ExpectedFactory.createError(Error("error")) } var result: List>? = null @@ -205,7 +207,7 @@ class RoadShieldContentManagerImplTest { loader.load(toDownloadDesign) } coAnswers { delay(500L) - ExpectedFactory.createError("error") + ExpectedFactory.createError(Error("error")) } var result: List>? = null @@ -253,7 +255,7 @@ class RoadShieldContentManagerImplTest { ) coEvery { loader.load(toDownloadDesign) - } returns ExpectedFactory.createError("error") + } returns ExpectedFactory.createError(Error("error")) val result = sut.getShields(listOf(toDownloadLegacy, toDownloadDesign)) @@ -288,7 +290,7 @@ class RoadShieldContentManagerImplTest { ) coEvery { loader.load(toDownload) - } returns ExpectedFactory.createError("error") + } returns ExpectedFactory.createError(Error("error")) coEvery { loader.load(legacyToDownload) } returns ExpectedFactory.createValue(expectedLegacyShield) @@ -322,10 +324,10 @@ class RoadShieldContentManagerImplTest { ) coEvery { loader.load(toDownload) - } returns ExpectedFactory.createError("error") + } returns ExpectedFactory.createError(Error("error")) coEvery { loader.load(legacyToDownload) - } returns ExpectedFactory.createError("error_legacy") + } returns ExpectedFactory.createError(Error("error_legacy")) val result = sut.getShields(listOf(toDownload)) @@ -346,7 +348,7 @@ class RoadShieldContentManagerImplTest { coEvery { loader.load(toDownload) - } returns ExpectedFactory.createError("error") + } returns ExpectedFactory.createError(Error("error")) val result = sut.getShields(listOf(toDownload)) @@ -424,7 +426,7 @@ class RoadShieldContentManagerImplTest { coEvery { loader.load(toDownload) - } returns ExpectedFactory.createError("error") + } returns ExpectedFactory.createError(Error("error")) val result = sut.getShields(listOf(toDownload)) diff --git a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/RoadShieldDownloaderTest.kt b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/RoadShieldDownloaderTest.kt index e1db39cb3a2..f3d427cf18f 100644 --- a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/RoadShieldDownloaderTest.kt +++ b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/RoadShieldDownloaderTest.kt @@ -101,7 +101,7 @@ internal class RoadShieldDownloaderTest { val result = sut.download(url) - assertEquals(expectedError, result.error) + assertEquals(expectedError, result.error?.message) } @Test @@ -120,7 +120,7 @@ internal class RoadShieldDownloaderTest { val result = sut.download(url) - assertEquals(expectedError, result.error) + assertEquals(expectedError, result.error?.message) } @Test @@ -138,7 +138,7 @@ internal class RoadShieldDownloaderTest { val result = sut.download(url) - assertEquals(expectedError, result.error) + assertEquals(expectedError, result.error?.message) } private fun givenResourceLoaderResponse( diff --git a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/CachedResourceLoaderTest.kt b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/CachedLoaderTest.kt similarity index 85% rename from libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/CachedResourceLoaderTest.kt rename to libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/CachedLoaderTest.kt index 2ad3b9263ee..96a3233f5e5 100644 --- a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/CachedResourceLoaderTest.kt +++ b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/CachedLoaderTest.kt @@ -14,10 +14,10 @@ import org.robolectric.RobolectricTestRunner @OptIn(ExperimentalCoroutinesApi::class) @RunWith(RobolectricTestRunner::class) -class CachedResourceLoaderTest { +class CachedLoaderTest { - private lateinit var loader: ResourceLoader - private lateinit var sut: CachedResourceLoader + private lateinit var loader: Loader + private lateinit var sut: CachedLoader @Before fun setUp() { @@ -26,7 +26,7 @@ class CachedResourceLoaderTest { createValue(firstArg().toString()) } } - sut = CachedResourceLoader(2, loader) + sut = CachedLoader(2, loader) } @Test diff --git a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceDownloaderTest.kt b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceDownloaderTest.kt index e00c9a0ee39..c90af569377 100644 --- a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceDownloaderTest.kt +++ b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/ResourceDownloaderTest.kt @@ -17,9 +17,9 @@ class ResourceDownloaderTest { @Test fun `load should call download method and return its value`() = runBlockingTest { - val sut = object : ResourceDownloader() { - override suspend fun download(argument: Int): Expected = - createValue("$argument") + val sut = object : Downloader() { + override suspend fun download(input: Int): Expected = + createValue("$input") } val r = sut.load(1234) @@ -31,10 +31,10 @@ class ResourceDownloaderTest { runBlockingTest { val stateFlow = MutableStateFlow(null) val downloadCalls = mutableListOf() - val sut = object : ResourceDownloader() { - override suspend fun download(argument: Int): Expected { - downloadCalls.add(argument) - val v = stateFlow.filter { it == "$argument" }.filterNotNull().first() + val sut = object : Downloader() { + override suspend fun download(input: Int): Expected { + downloadCalls.add(input) + val v = stateFlow.filter { it == "$input" }.filterNotNull().first() return createValue(v) } } @@ -61,9 +61,9 @@ class ResourceDownloaderTest { // resource 2: {-- loading 2 --} // val stateFlow = MutableStateFlow(null) - val sut = object : ResourceDownloader() { - override suspend fun download(argument: Int): Expected { - val v = stateFlow.filter { it == "$argument" }.filterNotNull().first() + val sut = object : Downloader() { + override suspend fun download(input: Int): Expected { + val v = stateFlow.filter { it == "$input" }.filterNotNull().first() return createValue(v) } } diff --git a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/RoadShieldLoaderTest.kt b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/RoadShieldLoaderTest.kt index f531e2d9cbf..7798d5a1dc5 100644 --- a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/RoadShieldLoaderTest.kt +++ b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/RoadShieldLoaderTest.kt @@ -27,8 +27,8 @@ class RoadShieldLoaderTest { private lateinit var sut: RoadShieldLoader - private lateinit var spritesLoader: ResourceLoader - private lateinit var imageLoader: ResourceLoader + private lateinit var spritesLoader: Loader + private lateinit var imageLoader: Loader @Before fun setup() { @@ -116,12 +116,12 @@ class RoadShieldLoaderTest { } returns ExpectedFactory.createValue(shieldSprites) coEvery { imageLoader.load(shieldUrl) - } returns ExpectedFactory.createError("error") + } returns ExpectedFactory.createError(Error("error")) val expected = "error" val result = sut.load(toDownload) - assertEquals(expected, result.error) + assertEquals(expected, result.error?.message) } @Test @@ -132,7 +132,7 @@ class RoadShieldLoaderTest { } coEvery { spritesLoader.load(spriteUrl) - } returns ExpectedFactory.createError("error") + } returns ExpectedFactory.createError(Error("error")) val expected = """ Error when downloading image sprite. @@ -141,7 +141,7 @@ class RoadShieldLoaderTest { """.trimIndent() val result = sut.load(toDownload) - assertEquals(expected, result.error) + assertEquals(expected, result.error?.message) } @Test @@ -162,7 +162,7 @@ class RoadShieldLoaderTest { val expected = "Sprite not found for ${toDownload.mapboxShield.name()} in $shieldSprites." val result = sut.load(toDownload) - assertEquals(expected, result.error) + assertEquals(expected, result.error?.message) } @Test @@ -185,7 +185,7 @@ class RoadShieldLoaderTest { val expected = "Mapbox shield sprite placeholder was null or empty in: $shieldSprite" val result = sut.load(toDownload) - assertEquals(expected, result.error) + assertEquals(expected, result.error?.message) } @Test @@ -218,11 +218,11 @@ class RoadShieldLoaderTest { } coEvery { imageLoader.load(shieldUrl) - } returns ExpectedFactory.createError("error") + } returns ExpectedFactory.createError(Error("error")) val expected = "error" val result = sut.load(toDownload) - assertEquals(expected, result.error) + assertEquals(expected, result.error?.message) } } diff --git a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/ShieldSpritesDownloaderTest.kt b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/ShieldSpritesDownloaderTest.kt index 7a7f5469643..104631d00e6 100644 --- a/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/ShieldSpritesDownloaderTest.kt +++ b/libnavui-shield/src/test/java/com/mapbox/navigation/ui/shield/internal/loader/ShieldSpritesDownloaderTest.kt @@ -36,11 +36,11 @@ class ShieldSpritesDownloaderTest { val argument = "url" coEvery { RoadShieldDownloader.download(argument) - } returns ExpectedFactory.createError("error") + } returns ExpectedFactory.createError(Error("error")) val result = sut.load(argument) - assertEquals("error", result.error) + assertEquals("error", result.error?.message) } @Test @@ -53,14 +53,14 @@ class ShieldSpritesDownloaderTest { val result = sut.load(argument) assertTrue( - result.error!!.startsWith("Error parsing shield sprites:") + result.error!!.message!!.startsWith("Error parsing shield sprites:") ) } @Test fun `download success`() = runBlockingTest { val argument = "url" - val expectedResult = ExpectedFactory.createValue( + val expectedResult = ExpectedFactory.createValue( """ { "turning-circle-outline": {