diff --git a/CHANGELOG.md b/CHANGELOG.md index d885e51810..554291747f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - Customizable fragment lifecycle breadcrumbs ([#2299](https://github.com/getsentry/sentry-java/pull/2299)) - Provide hook for Jetpack Compose navigation instrumentation ([#2320](https://github.com/getsentry/sentry-java/pull/2320)) +- Populate `event.modules` with dependencies metadata ([#2324](https://github.com/getsentry/sentry-java/pull/2324)) ### Dependencies diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java index 2167964d17..0ecbbb9067 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AndroidOptionsInitializer.java @@ -12,6 +12,7 @@ import io.sentry.SendFireAndForgetOutboxSender; import io.sentry.SentryLevel; import io.sentry.android.core.cache.AndroidEnvelopeCache; +import io.sentry.android.core.internal.modules.AssetsModulesLoader; import io.sentry.android.fragment.FragmentLifecycleIntegration; import io.sentry.android.timber.SentryTimberIntegration; import io.sentry.util.Objects; @@ -155,6 +156,7 @@ static void init( options.setTransportGate(new AndroidTransportGate(context, options.getLogger())); options.setTransactionProfiler( new AndroidTransactionProfiler(context, options, buildInfoProvider)); + options.setModulesLoader(new AssetsModulesLoader(context, options.getLogger())); } private static void installDefaultIntegrations( diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/internal/modules/AssetsModulesLoader.java b/sentry-android-core/src/main/java/io/sentry/android/core/internal/modules/AssetsModulesLoader.java new file mode 100644 index 0000000000..c381bacf0a --- /dev/null +++ b/sentry-android-core/src/main/java/io/sentry/android/core/internal/modules/AssetsModulesLoader.java @@ -0,0 +1,39 @@ +package io.sentry.android.core.internal.modules; + +import android.content.Context; +import io.sentry.ILogger; +import io.sentry.SentryLevel; +import io.sentry.internal.modules.ModulesLoader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.TreeMap; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class AssetsModulesLoader extends ModulesLoader { + + private final @NotNull Context context; + + public AssetsModulesLoader(final @NotNull Context context, final @NotNull ILogger logger) { + super(logger); + this.context = context; + } + + @Override + protected Map loadModules() { + final Map modules = new TreeMap<>(); + + try { + final InputStream stream = context.getAssets().open(EXTERNAL_MODULES_FILENAME); + return parseStream(stream); + } catch (FileNotFoundException e) { + logger.log(SentryLevel.INFO, "%s file was not found.", EXTERNAL_MODULES_FILENAME); + } catch (IOException e) { + logger.log(SentryLevel.ERROR, "Error extracting modules.", e); + } + return modules; + } +} diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt index 5403d19223..72ec316fc9 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AndroidOptionsInitializerTest.kt @@ -11,6 +11,7 @@ import io.sentry.ILogger import io.sentry.MainEventProcessor import io.sentry.SentryOptions import io.sentry.android.core.cache.AndroidEnvelopeCache +import io.sentry.android.core.internal.modules.AssetsModulesLoader import io.sentry.android.fragment.FragmentLifecycleIntegration import io.sentry.android.timber.SentryTimberIntegration import org.junit.runner.RunWith @@ -414,4 +415,11 @@ class AndroidOptionsInitializerTest { (activityLifeCycleIntegration as ActivityLifecycleIntegration).activityFramesTracker.isFrameMetricsAggregatorAvailable ) } + + @Test + fun `AssetsModulesLoader is set to options`() { + fixture.initSut() + + assertTrue { fixture.sentryOptions.modulesLoader is AssetsModulesLoader } + } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/internal/modules/AssetsModulesLoaderTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/internal/modules/AssetsModulesLoaderTest.kt new file mode 100644 index 0000000000..04006ca2bf --- /dev/null +++ b/sentry-android-core/src/test/java/io/sentry/android/core/internal/modules/AssetsModulesLoaderTest.kt @@ -0,0 +1,104 @@ +package io.sentry.android.core.internal.modules + +import android.content.Context +import android.content.res.AssetManager +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import io.sentry.ILogger +import java.io.FileNotFoundException +import java.nio.charset.Charset +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class AssetsModulesLoaderTest { + + class Fixture { + val context = mock() + val assets = mock() + val logger = mock() + + fun getSut( + fileName: String = "sentry-external-modules.txt", + content: String? = null, + throws: Boolean = false + ): AssetsModulesLoader { + if (content != null) { + whenever(assets.open(fileName)).thenReturn( + content.byteInputStream(Charset.defaultCharset()) + ) + } + if (throws) { + whenever(assets.open(fileName)).thenThrow(FileNotFoundException()) + } + whenever(context.assets).thenReturn(assets) + return AssetsModulesLoader(context, logger) + } + } + + private val fixture = Fixture() + + @Test + fun `reads modules from assets into map`() { + val sut = fixture.getSut( + content = + """ + com.squareup.okhttp3:okhttp:3.14.9 + com.squareup.okio:okio:1.17.2 + """.trimIndent() + ) + + assertEquals( + mapOf( + "com.squareup.okhttp3:okhttp" to "3.14.9", + "com.squareup.okio:okio" to "1.17.2" + ), + sut.orLoadModules + ) + } + + @Test + fun `caches modules after first read`() { + val sut = fixture.getSut( + content = + """ + com.squareup.okhttp3:okhttp:3.14.9 + com.squareup.okio:okio:1.17.2 + """.trimIndent() + ) + + // first, call method to get modules cached + sut.orLoadModules + + // then call it second time + assertEquals( + mapOf( + "com.squareup.okhttp3:okhttp" to "3.14.9", + "com.squareup.okio:okio" to "1.17.2" + ), + sut.orLoadModules + ) + // the context only called once when there's no in-memory cache + verify(fixture.context).assets + } + + @Test + fun `when file does not exist, swallows exception and returns empty map`() { + val sut = fixture.getSut(throws = true) + + assertTrue(sut.orLoadModules!!.isEmpty()) + } + + @Test + fun `when content is malformed, swallows exception and returns empty map`() { + val sut = fixture.getSut( + content = + """ + com.squareup.okhttp3;3.14.9 + """.trimIndent() + ) + + assertTrue(sut.orLoadModules!!.isEmpty()) + } +} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 8e398c641a..4d5c9ee87f 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1379,6 +1379,7 @@ public class io/sentry/SentryOptions { public fun getMaxRequestBodySize ()Lio/sentry/SentryOptions$RequestSize; public fun getMaxSpans ()I public fun getMaxTraceFileSize ()J + public fun getModulesLoader ()Lio/sentry/internal/modules/IModulesLoader; public fun getOutboxPath ()Ljava/lang/String; public fun getProfilesSampleRate ()Ljava/lang/Double; public fun getProfilesSampler ()Lio/sentry/SentryOptions$ProfilesSamplerCallback; @@ -1457,6 +1458,7 @@ public class io/sentry/SentryOptions { public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V public fun setMaxSpans (I)V public fun setMaxTraceFileSize (J)V + public fun setModulesLoader (Lio/sentry/internal/modules/IModulesLoader;)V public fun setPrintUncaughtStackTrace (Z)V public fun setProfilesSampleRate (Ljava/lang/Double;)V public fun setProfilesSampler (Lio/sentry/SentryOptions$ProfilesSamplerCallback;)V @@ -2190,6 +2192,28 @@ public final class io/sentry/instrumentation/file/SentryFileWriter : java/io/Out public fun (Ljava/lang/String;Z)V } +public abstract interface class io/sentry/internal/modules/IModulesLoader { + public abstract fun getOrLoadModules ()Ljava/util/Map; +} + +public abstract class io/sentry/internal/modules/ModulesLoader : io/sentry/internal/modules/IModulesLoader { + public static final field EXTERNAL_MODULES_FILENAME Ljava/lang/String; + protected final field logger Lio/sentry/ILogger; + public fun (Lio/sentry/ILogger;)V + public fun getOrLoadModules ()Ljava/util/Map; + protected abstract fun loadModules ()Ljava/util/Map; + protected fun parseStream (Ljava/io/InputStream;)Ljava/util/Map; +} + +public final class io/sentry/internal/modules/NoOpModulesLoader : io/sentry/internal/modules/IModulesLoader { + public static fun getInstance ()Lio/sentry/internal/modules/NoOpModulesLoader; + public fun getOrLoadModules ()Ljava/util/Map; +} + +public final class io/sentry/internal/modules/ResourcesModulesLoader : io/sentry/internal/modules/ModulesLoader { + public fun (Lio/sentry/ILogger;)V +} + public final class io/sentry/protocol/App : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public static final field TYPE Ljava/lang/String; public fun ()V diff --git a/sentry/src/main/java/io/sentry/MainEventProcessor.java b/sentry/src/main/java/io/sentry/MainEventProcessor.java index 5e559b64f9..26d21bfa6d 100644 --- a/sentry/src/main/java/io/sentry/MainEventProcessor.java +++ b/sentry/src/main/java/io/sentry/MainEventProcessor.java @@ -60,6 +60,7 @@ public MainEventProcessor(final @NotNull SentryOptions options) { setCommons(event); setExceptions(event); setDebugMeta(event); + setModules(event); if (shouldApplyScopeData(event, hint)) { processNonCachedEvent(event); @@ -90,6 +91,20 @@ private void setDebugMeta(final @NotNull SentryEvent event) { } } + private void setModules(final @NotNull SentryEvent event) { + final Map modules = options.getModulesLoader().getOrLoadModules(); + if (modules == null) { + return; + } + + final Map eventModules = event.getModules(); + if (eventModules == null) { + event.setModules(modules); + } else { + eventModules.putAll(modules); + } + } + private boolean shouldApplyScopeData( final @NotNull SentryBaseEvent event, final @NotNull Hint hint) { if (HintUtils.shouldApplyScopeData(hint)) { diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index d616aa03b5..2f43ea60ae 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -3,6 +3,9 @@ import io.sentry.cache.EnvelopeCache; import io.sentry.cache.IEnvelopeCache; import io.sentry.config.PropertiesProviderFactory; +import io.sentry.internal.modules.IModulesLoader; +import io.sentry.internal.modules.NoOpModulesLoader; +import io.sentry.internal.modules.ResourcesModulesLoader; import io.sentry.protocol.SentryId; import io.sentry.protocol.User; import io.sentry.transport.NoOpEnvelopeCache; @@ -270,6 +273,12 @@ private static boolean initConfigurations(final @NotNull SentryOptions options) }); } + final IModulesLoader modulesLoader = options.getModulesLoader(); + // only override the ModulesLoader if it's not already set by Android + if (modulesLoader instanceof NoOpModulesLoader) { + options.setModulesLoader(new ResourcesModulesLoader(options.getLogger())); + } + return true; } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 7867b5bf85..e7c0980c2b 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -5,7 +5,10 @@ import io.sentry.clientreport.ClientReportRecorder; import io.sentry.clientreport.IClientReportRecorder; import io.sentry.clientreport.NoOpClientReportRecorder; +import io.sentry.internal.modules.IModulesLoader; +import io.sentry.internal.modules.NoOpModulesLoader; import io.sentry.protocol.SdkVersion; +import io.sentry.transport.ITransport; import io.sentry.transport.ITransportGate; import io.sentry.transport.NoOpEnvelopeCache; import io.sentry.transport.NoOpTransportGate; @@ -181,8 +184,8 @@ public class SentryOptions { private final @NotNull List inAppIncludes = new CopyOnWriteArrayList<>(); /** - * The transport factory creates instances of {@link io.sentry.transport.ITransport} - internal - * construct of the client that abstracts away the event sending. + * The transport factory creates instances of {@link ITransport} - internal construct of the + * client that abstracts away the event sending. */ private @NotNull ITransportFactory transportFactory = NoOpTransportFactory.getInstance(); @@ -355,6 +358,9 @@ public class SentryOptions { /** ClientReportRecorder to track count of lost events / transactions / ... * */ @NotNull IClientReportRecorder clientReportRecorder = new ClientReportRecorder(this); + /** Modules (dependencies, packages) that will be send along with each event. */ + private @NotNull IModulesLoader modulesLoader = NoOpModulesLoader.getInstance(); + /** * Adds an event processor * @@ -1745,6 +1751,21 @@ public void setSendClientReports(boolean sendClientReports) { return clientReportRecorder; } + /** + * Returns a ModulesLoader to load external modules (dependencies/packages) of the program. + * + * @return a modules loader or no-op + */ + @ApiStatus.Internal + public @NotNull IModulesLoader getModulesLoader() { + return modulesLoader; + } + + @ApiStatus.Internal + public void setModulesLoader(final @Nullable IModulesLoader modulesLoader) { + this.modulesLoader = modulesLoader != null ? modulesLoader : NoOpModulesLoader.getInstance(); + } + /** The BeforeSend callback */ public interface BeforeSendCallback { diff --git a/sentry/src/main/java/io/sentry/internal/modules/IModulesLoader.java b/sentry/src/main/java/io/sentry/internal/modules/IModulesLoader.java new file mode 100644 index 0000000000..f8679363b7 --- /dev/null +++ b/sentry/src/main/java/io/sentry/internal/modules/IModulesLoader.java @@ -0,0 +1,11 @@ +package io.sentry.internal.modules; + +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public interface IModulesLoader { + @Nullable + Map getOrLoadModules(); +} diff --git a/sentry/src/main/java/io/sentry/internal/modules/ModulesLoader.java b/sentry/src/main/java/io/sentry/internal/modules/ModulesLoader.java new file mode 100644 index 0000000000..553b9279ba --- /dev/null +++ b/sentry/src/main/java/io/sentry/internal/modules/ModulesLoader.java @@ -0,0 +1,59 @@ +package io.sentry.internal.modules; + +import io.sentry.ILogger; +import io.sentry.SentryLevel; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.Charset; +import java.util.Map; +import java.util.TreeMap; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public abstract class ModulesLoader implements IModulesLoader { + + public static final String EXTERNAL_MODULES_FILENAME = "sentry-external-modules.txt"; + protected final @NotNull ILogger logger; + private @Nullable Map cachedModules = null; + + public ModulesLoader(final @NotNull ILogger logger) { + this.logger = logger; + } + + @Override + public @Nullable Map getOrLoadModules() { + if (cachedModules != null) { + return cachedModules; + } + cachedModules = loadModules(); + return cachedModules; + } + + protected abstract Map loadModules(); + + @SuppressWarnings("CharsetObjectCanBeUsed") + protected Map parseStream(final @NotNull InputStream stream) { + final Map modules = new TreeMap<>(); + try (final BufferedReader reader = + new BufferedReader(new InputStreamReader(stream, Charset.forName("UTF-8")))) { + String module = reader.readLine(); + while (module != null) { + int sep = module.lastIndexOf(':'); + final String group = module.substring(0, sep); + final String version = module.substring(sep + 1); + modules.put(group, version); + module = reader.readLine(); + } + logger.log(SentryLevel.DEBUG, "Extracted %d modules from resources.", modules.size()); + } catch (IOException e) { + logger.log(SentryLevel.ERROR, "Error extracting modules.", e); + } catch (RuntimeException e) { + logger.log(SentryLevel.ERROR, e, "%s file is malformed.", EXTERNAL_MODULES_FILENAME); + } + return modules; + } +} diff --git a/sentry/src/main/java/io/sentry/internal/modules/NoOpModulesLoader.java b/sentry/src/main/java/io/sentry/internal/modules/NoOpModulesLoader.java new file mode 100644 index 0000000000..c0dd1e56f7 --- /dev/null +++ b/sentry/src/main/java/io/sentry/internal/modules/NoOpModulesLoader.java @@ -0,0 +1,20 @@ +package io.sentry.internal.modules; + +import java.util.Map; +import org.jetbrains.annotations.Nullable; + +public final class NoOpModulesLoader implements IModulesLoader { + + private static final NoOpModulesLoader instance = new NoOpModulesLoader(); + + public static NoOpModulesLoader getInstance() { + return instance; + } + + private NoOpModulesLoader() {} + + @Override + public @Nullable Map getOrLoadModules() { + return null; + } +} diff --git a/sentry/src/main/java/io/sentry/internal/modules/ResourcesModulesLoader.java b/sentry/src/main/java/io/sentry/internal/modules/ResourcesModulesLoader.java new file mode 100644 index 0000000000..491678118a --- /dev/null +++ b/sentry/src/main/java/io/sentry/internal/modules/ResourcesModulesLoader.java @@ -0,0 +1,43 @@ +package io.sentry.internal.modules; + +import io.sentry.ILogger; +import io.sentry.SentryLevel; +import java.io.InputStream; +import java.util.Map; +import java.util.TreeMap; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@ApiStatus.Internal +public final class ResourcesModulesLoader extends ModulesLoader { + + private final @NotNull ClassLoader classLoader; + + public ResourcesModulesLoader(final @NotNull ILogger logger) { + this(logger, ResourcesModulesLoader.class.getClassLoader()); + } + + ResourcesModulesLoader(final @NotNull ILogger logger, final @NotNull ClassLoader classLoader) { + super(logger); + this.classLoader = classLoader; + } + + @Override + protected Map loadModules() { + final Map modules = new TreeMap<>(); + try { + final InputStream resourcesStream = + classLoader.getResourceAsStream(EXTERNAL_MODULES_FILENAME); + + if (resourcesStream == null) { + logger.log(SentryLevel.INFO, "%s file was not found.", EXTERNAL_MODULES_FILENAME); + return modules; + } + + return parseStream(resourcesStream); + } catch (SecurityException e) { + logger.log(SentryLevel.INFO, "Access to resources denied.", e); + } + return modules; + } +} diff --git a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt index 3f5e40dba4..baa58ada73 100644 --- a/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt +++ b/sentry/src/test/java/io/sentry/MainEventProcessorTest.kt @@ -13,7 +13,6 @@ import io.sentry.protocol.User import io.sentry.util.HintUtils import org.awaitility.kotlin.await import org.mockito.Mockito -import java.lang.RuntimeException import java.net.InetAddress import kotlin.test.AfterTest import kotlin.test.Test @@ -37,12 +36,25 @@ class MainEventProcessorTest { lateinit var sentryTracer: SentryTracer private val hostnameCacheMock = Mockito.mockStatic(HostnameCache::class.java) - fun getSut(attachThreads: Boolean = true, attachStackTrace: Boolean = true, environment: String? = "environment", tags: Map = emptyMap(), sendDefaultPii: Boolean? = null, serverName: String? = "server", host: String? = null, resolveHostDelay: Long? = null, hostnameCacheDuration: Long = 10, proguardUuid: String? = null): MainEventProcessor { + fun getSut( + attachThreads: Boolean = true, + attachStackTrace: Boolean = true, + environment: String? = "environment", + tags: Map = emptyMap(), + sendDefaultPii: Boolean? = null, + serverName: String? = "server", + host: String? = null, + resolveHostDelay: Long? = null, + hostnameCacheDuration: Long = 10, + proguardUuid: String? = null, + modules: Map? = null + ): MainEventProcessor { sentryOptions.isAttachThreads = attachThreads sentryOptions.isAttachStacktrace = attachStackTrace sentryOptions.isAttachServerName = true sentryOptions.environment = environment sentryOptions.serverName = serverName + sentryOptions.setModulesLoader { modules } if (sendDefaultPii != null) { sentryOptions.isSendDefaultPii = sendDefaultPii } @@ -348,7 +360,8 @@ class MainEventProcessorTest { @Test fun `uses cache to retrieve servername for subsequent events`() { - val processor = fixture.getSut(serverName = null, host = "aHost", hostnameCacheDuration = 1000) + val processor = + fixture.getSut(serverName = null, host = "aHost", hostnameCacheDuration = 1000) val firstEvent = SentryEvent() processor.process(firstEvent, Hint()) assertEquals("aHost", firstEvent.serverName) @@ -477,9 +490,38 @@ class MainEventProcessorTest { } } - private fun generateCrashedEvent(crashedThread: Thread = Thread.currentThread()) = SentryEvent().apply { - val mockThrowable = mock() - val actualThrowable = UncaughtExceptionHandlerIntegration.getUnhandledThrowable(crashedThread, mockThrowable) - throwable = actualThrowable + @Test + fun `when event has modules, appends to them`() { + val sut = fixture.getSut(modules = mapOf("group1:artifact1" to "2.0.0")) + + var event = SentryEvent().apply { + modules = mapOf("group:artifact" to "1.0.0") + } + event = sut.process(event, Hint()) + + assertEquals(2, event.modules!!.size) + assertEquals("1.0.0", event.modules!!["group:artifact"]) + assertEquals("2.0.0", event.modules!!["group1:artifact1"]) + } + + @Test + fun `sets event modules`() { + val sut = fixture.getSut(modules = mapOf("group1:artifact1" to "2.0.0")) + + var event = SentryEvent() + event = sut.process(event, Hint()) + + assertEquals(1, event.modules!!.size) + assertEquals("2.0.0", event.modules!!["group1:artifact1"]) } + + private fun generateCrashedEvent(crashedThread: Thread = Thread.currentThread()) = + SentryEvent().apply { + val mockThrowable = mock() + val actualThrowable = UncaughtExceptionHandlerIntegration.getUnhandledThrowable( + crashedThread, + mockThrowable + ) + throwable = actualThrowable + } } diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index 75658d06e6..9a34f12acf 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -7,6 +7,8 @@ import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import io.sentry.cache.EnvelopeCache import io.sentry.cache.IEnvelopeCache +import io.sentry.internal.modules.IModulesLoader +import io.sentry.internal.modules.ResourcesModulesLoader import io.sentry.protocol.SentryId import org.junit.rules.TemporaryFolder import java.io.File @@ -19,6 +21,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertNotNull import kotlin.test.assertTrue + class SentryTest { private val dsn = "http://key@localhost/proj" @@ -295,6 +298,35 @@ class SentryTest { assertTrue { sentryOptions!!.envelopeDiskCache is CustomEnvelopCache } } + @Test + fun `overrides modules loader if it's not set`() { + var sentryOptions: SentryOptions? = null + + Sentry.init { + it.dsn = dsn + sentryOptions = it + } + + assertTrue { sentryOptions!!.modulesLoader is ResourcesModulesLoader } + } + + @Test + fun `does not override modules loader if it's already set`() { + var sentryOptions: SentryOptions? = null + + Sentry.init { + it.dsn = dsn + it.setModulesLoader(CustomModulesLoader()) + sentryOptions = it + } + + assertTrue { sentryOptions!!.modulesLoader is CustomModulesLoader } + } + + private class CustomModulesLoader : IModulesLoader { + override fun getOrLoadModules(): MutableMap? = null + } + private class CustomEnvelopCache : IEnvelopeCache { override fun iterator(): MutableIterator = TODO() override fun store(envelope: SentryEnvelope, hint: Hint) = Unit diff --git a/sentry/src/test/java/io/sentry/internal/modules/ResourcesModulesLoaderTest.kt b/sentry/src/test/java/io/sentry/internal/modules/ResourcesModulesLoaderTest.kt new file mode 100644 index 0000000000..34605984cd --- /dev/null +++ b/sentry/src/test/java/io/sentry/internal/modules/ResourcesModulesLoaderTest.kt @@ -0,0 +1,96 @@ +package io.sentry.internal.modules + +import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import io.sentry.ILogger +import java.nio.charset.Charset +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +class ResourcesModulesLoaderTest { + + class Fixture { + val logger = mock() + val classLoader = mock() + + fun getSut( + fileName: String = "sentry-external-modules.txt", + content: String? = null + ): ResourcesModulesLoader { + if (content != null) { + whenever(classLoader.getResourceAsStream(fileName)).thenReturn( + content.byteInputStream(Charset.defaultCharset()) + ) + } + return ResourcesModulesLoader(logger, classLoader) + } + } + + private val fixture = Fixture() + + @Test + fun `reads modules from resources into map`() { + val sut = fixture.getSut( + content = + """ + com.squareup.okhttp3:okhttp:3.14.9 + com.squareup.okio:okio:1.17.2 + """.trimIndent() + ) + + assertEquals( + mapOf( + "com.squareup.okhttp3:okhttp" to "3.14.9", + "com.squareup.okio:okio" to "1.17.2" + ), + sut.orLoadModules + ) + } + + @Test + fun `caches modules after first read`() { + val sut = fixture.getSut( + content = + """ + com.squareup.okhttp3:okhttp:3.14.9 + com.squareup.okio:okio:1.17.2 + """.trimIndent() + ) + + // first, call method to get modules cached + sut.orLoadModules + + // then call it second time + assertEquals( + mapOf( + "com.squareup.okhttp3:okhttp" to "3.14.9", + "com.squareup.okio:okio" to "1.17.2" + ), + sut.orLoadModules + ) + // the classloader only called once when there's no in-memory cache + verify(fixture.classLoader).getResourceAsStream(any()) + } + + @Test + fun `when file does not exist, returns empty map`() { + val sut = fixture.getSut() + + assertTrue(sut.orLoadModules!!.isEmpty()) + } + + @Test + fun `when content is malformed, swallows exception and returns empty map`() { + val sut = fixture.getSut( + content = + """ + com.squareup.okhttp3;3.14.9 + """.trimIndent() + ) + + assertTrue(sut.orLoadModules!!.isEmpty()) + } +}