diff --git a/CHANGELOG.md b/CHANGELOG.md index cc138006073..3f28915bca1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # vNext +* Fix: Mark stacktrace as snapshot if captured at arbitrary moment #1231 * Enchancement: Improve EventProcessor nullability annotations (#1229). # 4.1.0 diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ANRWatchDog.java b/sentry-android-core/src/main/java/io/sentry/android/core/ANRWatchDog.java index b16087a0564..27ce3664b8f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ANRWatchDog.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ANRWatchDog.java @@ -136,6 +136,6 @@ public interface ANRListener { * * @param error The error describing the ANR. */ - void onAppNotResponding(ApplicationNotResponding error); + void onAppNotResponding(@NotNull ApplicationNotResponding error); } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java index d82689fd5e9..f4d4f893975 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/AnrIntegration.java @@ -82,10 +82,10 @@ void reportANR( final @NotNull ApplicationNotResponding error) { logger.log(SentryLevel.INFO, "ANR triggered with message: %s", error.getMessage()); - Mechanism mechanism = new Mechanism(); + final Mechanism mechanism = new Mechanism(); mechanism.setType("ANR"); - ExceptionMechanismException throwable = - new ExceptionMechanismException(mechanism, error, error.getThread()); + final ExceptionMechanismException throwable = + new ExceptionMechanismException(mechanism, error, error.getThread(), true); hub.captureException(throwable); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationNotResponding.java b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationNotResponding.java index 7299cd29de6..f4998240f81 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationNotResponding.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/ApplicationNotResponding.java @@ -4,6 +4,7 @@ import io.sentry.util.Objects; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * Error thrown by ANRWatchDog when an ANR is detected. Contains the stack trace of the frozen UI @@ -16,15 +17,15 @@ final class ApplicationNotResponding extends RuntimeException { private static final long serialVersionUID = 252541144579117016L; - private final Thread thread; + private final @NotNull Thread thread; - ApplicationNotResponding(@NotNull String message, @NotNull Thread thread) { + ApplicationNotResponding(final @Nullable String message, final @NotNull Thread thread) { super(message); this.thread = Objects.requireNonNull(thread, "Thread must be provided."); setStackTrace(this.thread.getStackTrace()); } - public Thread getThread() { + public @NotNull Thread getThread() { return thread; } } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/IHandler.java b/sentry-android-core/src/main/java/io/sentry/android/core/IHandler.java index 5cc79d33217..6c6b5f997af 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/IHandler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/IHandler.java @@ -1,10 +1,12 @@ package io.sentry.android.core; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.TestOnly; @TestOnly interface IHandler { - void post(Runnable runnable); + void post(@NotNull Runnable runnable); + @NotNull Thread getThread(); } diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/MainLooperHandler.java b/sentry-android-core/src/main/java/io/sentry/android/core/MainLooperHandler.java index 2ba9865164b..713a541fc42 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/MainLooperHandler.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/MainLooperHandler.java @@ -2,21 +2,26 @@ import android.os.Handler; import android.os.Looper; +import org.jetbrains.annotations.NotNull; final class MainLooperHandler implements IHandler { - private final Handler handler; + private final @NotNull Handler handler; MainLooperHandler() { - handler = new Handler(Looper.getMainLooper()); + this(Looper.getMainLooper()); + } + + MainLooperHandler(final @NotNull Looper looper) { + handler = new Handler(looper); } @Override - public void post(Runnable runnable) { + public void post(final @NotNull Runnable runnable) { handler.post(runnable); } @Override - public Thread getThread() { + public @NotNull Thread getThread() { return handler.getLooper().getThread(); } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/AnrIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/AnrIntegrationTest.kt index 14d55987a72..2b1a9371627 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/AnrIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/AnrIntegrationTest.kt @@ -1,9 +1,12 @@ package io.sentry.android.core +import android.content.Context import com.nhaarman.mockitokotlin2.any +import com.nhaarman.mockitokotlin2.check import com.nhaarman.mockitokotlin2.mock import com.nhaarman.mockitokotlin2.verify import io.sentry.IHub +import io.sentry.exception.ExceptionMechanismException import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertNotNull @@ -12,49 +15,80 @@ import kotlin.test.assertTrue class AnrIntegrationTest { - private val integration = AnrIntegration(mock()) + private class Fixture { + val context = mock() + val hub = mock() + val options = SentryAndroidOptions() + + fun getSut(): AnrIntegration { + return AnrIntegration(context) + } + } + + private val fixture = Fixture() @BeforeTest fun `before each test`() { + val sut = fixture.getSut() // watch dog is static and has shared state - integration.close() + sut.close() } @Test fun `When ANR is enabled, ANR watch dog should be started`() { - val options = SentryAndroidOptions() - val hub = mock() - integration.register(hub, options) - assertNotNull(integration.anrWatchDog) - assertTrue((integration.anrWatchDog as ANRWatchDog).isAlive) + val sut = fixture.getSut() + + sut.register(fixture.hub, fixture.options) + + assertNotNull(sut.anrWatchDog) + assertTrue((sut.anrWatchDog as ANRWatchDog).isAlive) } @Test fun `When ANR is disabled, ANR should not be started`() { - val options = SentryAndroidOptions() - options.isAnrEnabled = false - val hub = mock() - val integration = AnrIntegration(mock()) - integration.register(hub, options) - assertNull(integration.anrWatchDog) + val sut = fixture.getSut() + fixture.options.isAnrEnabled = false + + sut.register(fixture.hub, fixture.options) + + assertNull(sut.anrWatchDog) } @Test fun `When ANR watch dog is triggered, it should capture exception`() { - val hub = mock() - val integration = AnrIntegration(mock()) - integration.reportANR(hub, mock(), mock()) - verify(hub).captureException(any()) + val sut = fixture.getSut() + + sut.reportANR(fixture.hub, mock(), getApplicationNotResponding()) + + verify(fixture.hub).captureException(any()) } @Test fun `When ANR integration is closed, watch dog should stop`() { - val options = SentryAndroidOptions() - val hub = mock() - val integration = AnrIntegration(mock()) - integration.register(hub, options) - assertNotNull(integration.anrWatchDog) - integration.close() - assertNull(integration.anrWatchDog) + val sut = fixture.getSut() + + sut.register(fixture.hub, fixture.options) + + assertNotNull(sut.anrWatchDog) + + sut.close() + + assertNull(sut.anrWatchDog) + } + + @Test + fun `When ANR watch dog is triggered, snapshot flag should be true`() { + val sut = fixture.getSut() + + sut.reportANR(fixture.hub, mock(), getApplicationNotResponding()) + + verify(fixture.hub).captureException(check { + val ex = it as ExceptionMechanismException + assertTrue(ex.isSnapshot) + }) + } + + private fun getApplicationNotResponding(): ApplicationNotResponding { + return ApplicationNotResponding("ApplicationNotResponding", Thread.currentThread()) } } diff --git a/sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java b/sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java index 62a0d0eb57e..9e0c9e2a6d2 100644 --- a/sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java +++ b/sentry-jul/src/main/java/io/sentry/jul/SentryHandler.java @@ -43,7 +43,11 @@ public SentryHandler() { this(new SentryOptions(), true); } - /** Creates an instance of SentryHandler. */ + /** + * Creates an instance of SentryHandler. + * + * @param options the SentryOptions + */ public SentryHandler(final @NotNull SentryOptions options) { this(options, true); } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 3695d304b3e..bfa8c150b2f 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -1145,9 +1145,11 @@ public final class io/sentry/config/PropertiesProviderFactory { public final class io/sentry/exception/ExceptionMechanismException : java/lang/RuntimeException { public fun (Lio/sentry/protocol/Mechanism;Ljava/lang/Throwable;Ljava/lang/Thread;)V + public fun (Lio/sentry/protocol/Mechanism;Ljava/lang/Throwable;Ljava/lang/Thread;Z)V public fun getExceptionMechanism ()Lio/sentry/protocol/Mechanism; public fun getThread ()Ljava/lang/Thread; public fun getThrowable ()Ljava/lang/Throwable; + public fun isSnapshot ()Z } public final class io/sentry/exception/InvalidDsnException : java/lang/RuntimeException { @@ -1597,8 +1599,10 @@ public final class io/sentry/protocol/SentryStackTrace : io/sentry/IUnknownPrope public fun acceptUnknownProperties (Ljava/util/Map;)V public fun getFrames ()Ljava/util/List; public fun getRegisters ()Ljava/util/Map; + public fun getSnapshot ()Ljava/lang/Boolean; public fun setFrames (Ljava/util/List;)V public fun setRegisters (Ljava/util/Map;)V + public fun setSnapshot (Ljava/lang/Boolean;)V } public final class io/sentry/protocol/SentryThread : io/sentry/IUnknownPropertiesConsumer { diff --git a/sentry/src/main/java/io/sentry/SentryExceptionFactory.java b/sentry/src/main/java/io/sentry/SentryExceptionFactory.java index 40c25004f9c..84755dcb263 100644 --- a/sentry/src/main/java/io/sentry/SentryExceptionFactory.java +++ b/sentry/src/main/java/io/sentry/SentryExceptionFactory.java @@ -62,11 +62,14 @@ List getSentryExceptions(final @NotNull Throwable throwable) { * @param exceptionMechanism The optional {@link Mechanism} of the {@code throwable}. Or null if * none exist. * @param thread The optional {@link Thread} which the exception originated. Or null if not known. + * @param snapshot if the captured {@link java.lang.Thread}'s stacktrace is a snapshot, See {@link + * SentryStackTrace#getSnapshot()} */ private @NotNull SentryException getSentryException( @NotNull final Throwable throwable, @Nullable final Mechanism exceptionMechanism, - @Nullable final Thread thread) { + @Nullable final Thread thread, + final boolean snapshot) { final Package exceptionPackage = throwable.getClass().getPackage(); final String fullClassName = throwable.getClass().getName(); @@ -86,7 +89,11 @@ List getSentryExceptions(final @NotNull Throwable throwable) { final List frames = sentryStackTraceFactory.getStackFrames(throwable.getStackTrace()); if (frames != null && !frames.isEmpty()) { - exception.setStacktrace(new SentryStackTrace(frames)); + final SentryStackTrace sentryStackTrace = new SentryStackTrace(frames); + if (snapshot) { + sentryStackTrace.setSnapshot(true); + } + exception.setStacktrace(sentryStackTrace); } if (thread != null) { @@ -120,6 +127,7 @@ Deque extractExceptionQueue(final @NotNull Throwable throwable) // Stack the exceptions to send them in the reverse order while (currentThrowable != null && circularityDetector.add(currentThrowable)) { + boolean snapshot = false; if (currentThrowable instanceof ExceptionMechanismException) { // this is for ANR I believe ExceptionMechanismException exceptionMechanismThrowable = @@ -127,12 +135,14 @@ Deque extractExceptionQueue(final @NotNull Throwable throwable) exceptionMechanism = exceptionMechanismThrowable.getExceptionMechanism(); currentThrowable = exceptionMechanismThrowable.getThrowable(); thread = exceptionMechanismThrowable.getThread(); + snapshot = exceptionMechanismThrowable.isSnapshot(); } else { exceptionMechanism = null; thread = Thread.currentThread(); } - SentryException exception = getSentryException(currentThrowable, exceptionMechanism, thread); + SentryException exception = + getSentryException(currentThrowable, exceptionMechanism, thread, snapshot); exceptions.addFirst(exception); currentThrowable = currentThrowable.getCause(); } diff --git a/sentry/src/main/java/io/sentry/SentryThreadFactory.java b/sentry/src/main/java/io/sentry/SentryThreadFactory.java index d03d94d6765..f6fd34079d0 100644 --- a/sentry/src/main/java/io/sentry/SentryThreadFactory.java +++ b/sentry/src/main/java/io/sentry/SentryThreadFactory.java @@ -126,7 +126,11 @@ List getCurrentThreads( sentryStackTraceFactory.getStackFrames(stackFramesElements); if (options.isAttachStacktrace() && frames != null && !frames.isEmpty()) { - sentryThread.setStacktrace(new SentryStackTrace(frames)); + final SentryStackTrace sentryStackTrace = new SentryStackTrace(frames); + // threads are always gotten either via Thread.currentThread() or Thread.getAllStackTraces() + sentryStackTrace.setSnapshot(true); + + sentryThread.setStacktrace(sentryStackTrace); } return sentryThread; diff --git a/sentry/src/main/java/io/sentry/exception/ExceptionMechanismException.java b/sentry/src/main/java/io/sentry/exception/ExceptionMechanismException.java index 5d7914c98b6..5cfeb747dd2 100644 --- a/sentry/src/main/java/io/sentry/exception/ExceptionMechanismException.java +++ b/sentry/src/main/java/io/sentry/exception/ExceptionMechanismException.java @@ -1,8 +1,9 @@ package io.sentry.exception; import io.sentry.protocol.Mechanism; +import io.sentry.util.Objects; import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.NotNull; /** * A throwable decorator that holds an {@link io.sentry.protocol.Mechanism} related to the decorated @@ -12,9 +13,10 @@ public final class ExceptionMechanismException extends RuntimeException { private static final long serialVersionUID = 142345454265713915L; - private final Mechanism exceptionMechanism; - private final Throwable throwable; - private final Thread thread; + private final @NotNull Mechanism exceptionMechanism; + private final @NotNull Throwable throwable; + private final @NotNull Thread thread; + private final boolean snapshot; /** * A {@link Throwable} that decorates another with a Sentry {@link Mechanism}. @@ -22,23 +24,66 @@ public final class ExceptionMechanismException extends RuntimeException { * @param mechanism The {@link Mechanism}. * @param throwable The {@link java.lang.Throwable}. * @param thread The {@link java.lang.Thread}. + * @param snapshot if the captured {@link java.lang.Thread}'s stacktrace is a snapshot. */ public ExceptionMechanismException( - @Nullable Mechanism mechanism, @Nullable Throwable throwable, @Nullable Thread thread) { - this.exceptionMechanism = mechanism; - this.throwable = throwable; - this.thread = thread; + final @NotNull Mechanism mechanism, + final @NotNull Throwable throwable, + final @NotNull Thread thread, + final boolean snapshot) { + exceptionMechanism = Objects.requireNonNull(mechanism, "Mechanism is required."); + this.throwable = Objects.requireNonNull(throwable, "Throwable is required."); + this.thread = Objects.requireNonNull(thread, "Thread is required."); + this.snapshot = snapshot; } - public Mechanism getExceptionMechanism() { + /** + * A {@link Throwable} that decorates another with a Sentry {@link Mechanism}. + * + * @param mechanism The {@link Mechanism}. + * @param throwable The {@link java.lang.Throwable}. + * @param thread The {@link java.lang.Thread}. + */ + public ExceptionMechanismException( + final @NotNull Mechanism mechanism, + final @NotNull Throwable throwable, + final @NotNull Thread thread) { + this(mechanism, throwable, thread, false); + } + + /** + * Returns the encapsulated Mechanism + * + * @return the Mechanism + */ + public @NotNull Mechanism getExceptionMechanism() { return exceptionMechanism; } - public Throwable getThrowable() { + /** + * Returns the encapsulated Throwable + * + * @return the Throwable + */ + public @NotNull Throwable getThrowable() { return throwable; } - public Thread getThread() { + /** + * Returns the encapsulated Thread + * + * @return the Thread + */ + public @NotNull Thread getThread() { return thread; } + + /** + * Returns true if its a snapshot or false otherwise + * + * @return true or false + */ + public boolean isSnapshot() { + return snapshot; + } } diff --git a/sentry/src/main/java/io/sentry/protocol/SentryStackTrace.java b/sentry/src/main/java/io/sentry/protocol/SentryStackTrace.java index e246ccd742d..f2a1b7b2974 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentryStackTrace.java +++ b/sentry/src/main/java/io/sentry/protocol/SentryStackTrace.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Map; import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; /** * A stack trace of a single thread. @@ -40,21 +41,27 @@ public final class SentryStackTrace implements IUnknownPropertiesConsumer { * Required. A non-empty list of stack frames. The list is ordered from caller to callee, or * oldest to youngest. The last frame is the one creating the exception. */ - private List frames; + private @Nullable List frames; /** * Register values of the thread (top frame). * *

A map of register names and their values. The values should contain the actual register * values of the thread, thus mapping to the last frame in the list. */ - private Map registers; + private @Nullable Map registers; + + /** + * This flag indicates that this stack trace is captured at an arbitrary moment in time and this + * would affect the quality of grouping, Sentry will special case if this is set to true. + */ + private @Nullable Boolean snapshot; @SuppressWarnings("unused") - private Map unknown; + private @Nullable Map unknown; public SentryStackTrace() {} - public SentryStackTrace(List frames) { + public SentryStackTrace(final @Nullable List frames) { this.frames = frames; } @@ -63,7 +70,7 @@ public SentryStackTrace(List frames) { * * @return the frames. */ - public List getFrames() { + public @Nullable List getFrames() { return frames; } @@ -72,21 +79,29 @@ public List getFrames() { * * @param frames the frames. */ - public void setFrames(List frames) { + public void setFrames(final @Nullable List frames) { this.frames = frames; } @ApiStatus.Internal @Override - public void acceptUnknownProperties(Map unknown) { + public void acceptUnknownProperties(final @Nullable Map unknown) { this.unknown = unknown; } - public Map getRegisters() { + public @Nullable Map getRegisters() { return registers; } - public void setRegisters(Map registers) { + public void setRegisters(final @Nullable Map registers) { this.registers = registers; } + + public @Nullable Boolean getSnapshot() { + return snapshot; + } + + public void setSnapshot(final @Nullable Boolean snapshot) { + this.snapshot = snapshot; + } } diff --git a/sentry/src/test/java/io/sentry/DuplicateEventDetectionEventProcessorTest.kt b/sentry/src/test/java/io/sentry/DuplicateEventDetectionEventProcessorTest.kt index b2e7ec0efb0..578b14bda5a 100644 --- a/sentry/src/test/java/io/sentry/DuplicateEventDetectionEventProcessorTest.kt +++ b/sentry/src/test/java/io/sentry/DuplicateEventDetectionEventProcessorTest.kt @@ -34,13 +34,13 @@ class DuplicateEventDetectionEventProcessorTest { val event = SentryEvent(RuntimeException()) processor.process(event, null) - val result = processor.process(SentryEvent(ExceptionMechanismException(Mechanism(), event.throwable, null)), null) + val result = processor.process(SentryEvent(ExceptionMechanismException(Mechanism(), event.throwable!!, Thread.currentThread())), null) assertNull(result) } @Test fun `drops event with exception that has already been processed with event with mechanism exception`() { - val sentryEvent = SentryEvent(ExceptionMechanismException(Mechanism(), RuntimeException(), null)) + val sentryEvent = SentryEvent(ExceptionMechanismException(Mechanism(), RuntimeException(), Thread.currentThread())) processor.process(sentryEvent, null) val result = processor.process(SentryEvent((sentryEvent.throwable as ExceptionMechanismException).throwable), null) diff --git a/sentry/src/test/java/io/sentry/SentryEventTest.kt b/sentry/src/test/java/io/sentry/SentryEventTest.kt index 878be32c7f2..5c17c5966c8 100644 --- a/sentry/src/test/java/io/sentry/SentryEventTest.kt +++ b/sentry/src/test/java/io/sentry/SentryEventTest.kt @@ -78,7 +78,7 @@ class SentryEventTest { fun `when throwable is a ExceptionMechanismException, getOriginThrowable unwraps original throwable`() { val event = SentryEvent() val ex = RuntimeException() - event.throwable = ExceptionMechanismException(null, ex, null) + event.throwable = ExceptionMechanismException(Mechanism(), ex, Thread.currentThread()) assertEquals(ex, event.originThrowable) } diff --git a/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt b/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt index 7393b6c3c06..94dc52efc11 100644 --- a/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryExceptionFactoryTest.kt @@ -7,6 +7,7 @@ import io.sentry.exception.ExceptionMechanismException import io.sentry.protocol.Mechanism import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue @@ -33,7 +34,7 @@ class SentryExceptionFactoryTest { assertEquals("Exception", sentryExceptions[0].type) assertEquals("Exception", sentryExceptions[0].value) assertEquals("java.lang", sentryExceptions[0].module) - assertTrue(sentryExceptions[0].stacktrace.frames.isNotEmpty()) + assertTrue(sentryExceptions[0].stacktrace.frames!!.isNotEmpty()) } @Test @@ -70,11 +71,22 @@ class SentryExceptionFactoryTest { val error = Exception("Exception") - val throwable = ExceptionMechanismException(mechanism, error, null) + val throwable = ExceptionMechanismException(mechanism, error, Thread.currentThread()) val sentryExceptions = fixture.getSut().getSentryExceptions(throwable) assertEquals("anr", sentryExceptions[0].mechanism.type) - assertEquals(false, sentryExceptions[0].mechanism.isHandled) + assertFalse(sentryExceptions[0].mechanism.isHandled) + assertNull(sentryExceptions[0].stacktrace?.snapshot) + } + + @Test + fun `When ExceptionMechanismException has threads snapshot, stack trace should set snapshot flag`() { + val error = Exception("Exception") + + val throwable = ExceptionMechanismException(Mechanism(), error, Thread.currentThread(), true) + val sentryExceptions = fixture.getSut().getSentryExceptions(throwable) + + assertTrue(sentryExceptions[0].stacktrace?.snapshot!!) } @Test diff --git a/sentry/src/test/java/io/sentry/SentryThreadFactoryTest.kt b/sentry/src/test/java/io/sentry/SentryThreadFactoryTest.kt index 1730fa4c7d9..77fdc0ea380 100644 --- a/sentry/src/test/java/io/sentry/SentryThreadFactoryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryThreadFactoryTest.kt @@ -41,13 +41,19 @@ class SentryThreadFactoryTest { @Test fun `when currentThreads is called, some thread stack frames are captured`() { val sut = fixture.getSut() - assertTrue(sut.getCurrentThreads(null)!!.filter { it.stacktrace != null }.any { it.stacktrace.frames.count() > 0 }) + assertTrue(sut.getCurrentThreads(null)!!.filter { it.stacktrace != null }.any { it.stacktrace.frames!!.count() > 0 }) + } + + @Test + fun `when currentThreads is called, stack traces are snapshot`() { + val sut = fixture.getSut() + assertTrue(sut.getCurrentThreads(null)!!.filter { it.stacktrace != null }.any { it.stacktrace.snapshot == true }) } @Test fun `when currentThreads and attachStacktrace is disabled, stack frames are not captured`() { val sut = fixture.getSut(false) - assertFalse(sut.getCurrentThreads(null)!!.filter { it.stacktrace != null }.any { it.stacktrace.frames.count() > 0 }) + assertFalse(sut.getCurrentThreads(null)!!.filter { it.stacktrace != null }.any { it.stacktrace.frames!!.count() > 0 }) } @Test