From 43dc2d9f3d86bd0669f85997709c1a5d0f6ab30a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 29 Jul 2025 10:13:00 +0000 Subject: [PATCH 1/8] Fix dropCommonAddresses to handle edge cases and improve index tracking Co-authored-by: giancarlo.buenaflor --- .../kotlin/multiplatform/nsexception/Throwable.kt | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt b/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt index 8fec98f7..ba95a646 100644 --- a/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt +++ b/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt @@ -75,9 +75,16 @@ internal fun List.dropInitAddresses( internal fun List.dropCommonAddresses( commonAddresses: List ): List { - var i = commonAddresses.size - if (i == 0) return this - return dropLastWhile { - i-- >= 0 && commonAddresses[i] == it + if (commonAddresses.isEmpty() || this.isEmpty()) return this + + var commonIndex = commonAddresses.size - 1 + return dropLastWhile { address -> + if (commonIndex < 0 || commonIndex >= commonAddresses.size) { + false + } else { + val matches = commonAddresses[commonIndex] == address + if (matches) commonIndex-- + matches + } } } From 412bcbe037fe4dd5ecf847bacc19b43e7ffd8676 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 29 Jul 2025 12:53:08 +0000 Subject: [PATCH 2/8] Add comprehensive tests for stack trace filtering and address dropping Co-authored-by: giancarlo.buenaflor --- .../nsexception/CommonAddressesTests.kt | 191 ++++++++++++++ .../StackTraceFilteringIntegrationTests.kt | 249 ++++++++++++++++++ 2 files changed, 440 insertions(+) create mode 100644 sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/StackTraceFilteringIntegrationTests.kt diff --git a/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/CommonAddressesTests.kt b/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/CommonAddressesTests.kt index c157eb30..b7ddecd8 100644 --- a/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/CommonAddressesTests.kt +++ b/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/CommonAddressesTests.kt @@ -17,9 +17,12 @@ package io.sentry.kotlin.multiplatform.nsexception import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertSame +import kotlin.test.assertTrue class CommonAddressesTests { + // MARK: - Basic Functionality Tests (Positive Cases) + @Test fun testDropCommon() { val commonAddresses = listOf(5, 4, 3, 2, 1, 0) @@ -28,6 +31,40 @@ class CommonAddressesTests { assertEquals(listOf(8, 7, 6), withoutCommonAddresses) } + @Test + fun testDropCommonPartialMatch() { + val commonAddresses = listOf(3, 2, 1) + val addresses = listOf(9, 8, 7, 2, 1) + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertEquals(listOf(9, 8, 7), withoutCommonAddresses) + } + + @Test + fun testDropCommonNoMatch() { + val commonAddresses = listOf(5, 4, 3) + val addresses = listOf(9, 8, 7, 6) + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertSame(addresses, withoutCommonAddresses) + } + + @Test + fun testDropCommonSingleElementMatch() { + val commonAddresses = listOf(1) + val addresses = listOf(5, 4, 3, 1) + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertEquals(listOf(5, 4, 3), withoutCommonAddresses) + } + + @Test + fun testDropCommonSingleElementNoMatch() { + val commonAddresses = listOf(2) + val addresses = listOf(5, 4, 3, 1) + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertSame(addresses, withoutCommonAddresses) + } + + // MARK: - Edge Cases (Boundary Conditions) + @Test fun testDropCommonEmptyCommon() { val addresses = listOf(0, 1, 2) @@ -35,10 +72,164 @@ class CommonAddressesTests { assertSame(addresses, withoutCommonAddresses) } + @Test + fun testDropCommonEmptyAddresses() { + val commonAddresses = listOf(1, 2, 3) + val addresses = emptyList() + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertSame(addresses, withoutCommonAddresses) + } + + @Test + fun testDropCommonBothEmpty() { + val addresses = emptyList() + val commonAddresses = emptyList() + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertSame(addresses, withoutCommonAddresses) + } + @Test fun testDropCommonSameAddresses() { val addresses = listOf(0, 1, 2) val withoutCommonAddresses = addresses.dropCommonAddresses(addresses) assertEquals(emptyList(), withoutCommonAddresses) } + + @Test + fun testDropCommonLargerCommonList() { + val commonAddresses = listOf(5, 4, 3, 2, 1, 0) + val addresses = listOf(2, 1, 0) + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertEquals(emptyList(), withoutCommonAddresses) + } + + @Test + fun testDropCommonLargerAddressList() { + val commonAddresses = listOf(2, 1, 0) + val addresses = listOf(9, 8, 7, 6, 5, 4, 2, 1, 0) + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertEquals(listOf(9, 8, 7, 6, 5, 4), withoutCommonAddresses) + } + + // MARK: - Regression Tests (Prevent IndexOutOfBoundsException) + + @Test + fun testDropCommonNoIndexOutOfBounds_LargeCommonList() { + // This test specifically targets the original bug where i-- could become -1 + val commonAddresses = (0L..26L).toList().reversed() // 27 elements: [26, 25, ..., 1, 0] + val addresses = listOf(30, 29, 28, 2, 1, 0) + + // This should not throw IndexOutOfBoundsException + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertEquals(listOf(30, 29, 28), withoutCommonAddresses) + } + + @Test + fun testDropCommonNoIndexOutOfBounds_ExactSizeMatch() { + // Test when commonAddresses.size equals the number of matching elements + val commonAddresses = listOf(4, 3, 2, 1, 0) + val addresses = listOf(9, 8, 7, 4, 3, 2, 1, 0) + + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertEquals(listOf(9, 8, 7), withoutCommonAddresses) + } + + @Test + fun testDropCommonNoIndexOutOfBounds_OffByOneScenario() { + // Test the exact scenario that caused the original crash + val commonAddresses = (1L..27L).toList() // size: 27 + val addresses = listOf(100L, 99L, 98L) + commonAddresses.takeLast(3) + + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertEquals(listOf(100L, 99L, 98L), withoutCommonAddresses) + } + + @Test + fun testDropCommonNoIndexOutOfBounds_SingleElementCommon() { + val commonAddresses = listOf(0) + val addresses = listOf(5, 4, 3, 2, 1, 0) + + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertEquals(listOf(5, 4, 3, 2, 1), withoutCommonAddresses) + } + + // MARK: - Performance and Stress Tests + + @Test + fun testDropCommonLargeList() { + val commonAddresses = (0L..999L).toList().reversed() + val addresses = (500L..1499L).toList() + + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertTrue("Result should contain elements not in common", withoutCommonAddresses.isNotEmpty()) + assertEquals(500, withoutCommonAddresses.size) // Should keep 1000-1499 + } + + @Test + fun testDropCommonRepeatedElements() { + val commonAddresses = listOf(1, 1, 1, 0, 0) + val addresses = listOf(5, 4, 1, 1, 0) + + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertEquals(listOf(5, 4, 1, 1), withoutCommonAddresses) + } + + // MARK: - Sequence and Order Tests + + @Test + fun testDropCommonMaintainsOrder() { + val commonAddresses = listOf(3, 2, 1) + val addresses = listOf(9, 8, 7, 6, 5, 3, 2, 1) + + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertEquals(listOf(9, 8, 7, 6, 5), withoutCommonAddresses) + } + + @Test + fun testDropCommonOutOfSequenceMatch() { + // Common addresses are in different order than in the target list + val commonAddresses = listOf(1, 3, 2) // Note: 3 and 2 are swapped + val addresses = listOf(9, 8, 7, 6, 2) + + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertEquals(listOf(9, 8, 7, 6), withoutCommonAddresses) // Should drop 2 + } + + @Test + fun testDropCommonPartialSequenceMatch() { + val commonAddresses = listOf(4, 3, 2, 1) + val addresses = listOf(9, 8, 3, 2, 1) // Missing 4 in sequence + + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertEquals(listOf(9, 8), withoutCommonAddresses) // Should drop 3,2,1 but keep others + } + + // MARK: - Negative Test Cases + + @Test + fun testDropCommonWithNegativeNumbers() { + val commonAddresses = listOf(-1, -2, -3) + val addresses = listOf(1, 0, -1, -2, -3) + + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertEquals(listOf(1, 0), withoutCommonAddresses) + } + + @Test + fun testDropCommonMixedPositiveNegative() { + val commonAddresses = listOf(1, 0, -1) + val addresses = listOf(5, 4, 3, 1, 0, -1) + + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertEquals(listOf(5, 4, 3), withoutCommonAddresses) + } + + @Test + fun testDropCommonLargeNumbers() { + val commonAddresses = listOf(Long.MAX_VALUE - 1, Long.MAX_VALUE - 2) + val addresses = listOf(Long.MAX_VALUE, Long.MAX_VALUE - 1, Long.MAX_VALUE - 2) + + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) + assertEquals(listOf(Long.MAX_VALUE), withoutCommonAddresses) + } } diff --git a/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/StackTraceFilteringIntegrationTests.kt b/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/StackTraceFilteringIntegrationTests.kt new file mode 100644 index 00000000..3584fc7b --- /dev/null +++ b/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/StackTraceFilteringIntegrationTests.kt @@ -0,0 +1,249 @@ +// Integration tests for the complete stack trace filtering pipeline +// +// Copyright (c) 2024 Sentry +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +package io.sentry.kotlin.multiplatform.nsexception + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.assertNotNull + +class StackTraceFilteringIntegrationTests { + + // MARK: - Mock Implementation for Testing + + // Mock extension function to simulate getStackTraceAddresses for testing + private fun createMockThrowable( + addresses: List, + stackTrace: Array, + className: String = "TestException" + ): TestThrowable { + return TestThrowable(addresses, stackTrace, className) + } + + private class TestThrowable( + private val mockAddresses: List, + private val mockStackTrace: Array, + private val mockClassName: String + ) : Throwable() { + + fun getStackTraceAddresses(): List = mockAddresses + + override fun getStackTrace(): Array { + return mockStackTrace.map { + StackTraceElement("", "", "", 0) + }.toTypedArray() + } + + fun getStackTrace(): Array = mockStackTrace + + fun getFilteredStackTraceAddresses( + keepLastInit: Boolean = false, + commonAddresses: List = emptyList() + ): List = getStackTraceAddresses().dropInitAddresses( + qualifiedClassName = mockClassName, + stackTrace = getStackTrace(), + keepLast = keepLastInit + ).dropCommonAddresses(commonAddresses) + } + + // MARK: - Integration Tests + + @Test + fun testCompleteFilteringPipeline_NormalCase() { + val addresses = listOf(10, 11, 12, 13, 14, 15, 16, 17, 18, 19) + val stackTrace = arrayOf( + "kfun:kotlin.Throwable#(kotlin.String?){} + 24", + "kfun:kotlin.Exception#(kotlin.String?){} + 5", + "kfun:TestException#(kotlin.String?){} + 10", // addresses[2] + "kfun:TestException#(){} + 12", // addresses[3] + "kfun:my.app.class#function1(){} + 50", // addresses[4] - should be kept + "kfun:my.app.class#function2(){} + 60", // addresses[5] - should be kept + "kfun:my.app.class#function3(){} + 70", // addresses[6] - should be kept + "kfun:my.app.class#function4(){} + 80", // addresses[7] - should be kept + "kfun:my.app.class#function5(){} + 90", // addresses[8] - common + "kfun:my.app.class#function6(){} + 100" // addresses[9] - common + ) + val commonAddresses = listOf(18, 19) // Last two addresses + + val throwable = createMockThrowable(addresses, stackTrace, "TestException") + val filtered = throwable.getFilteredStackTraceAddresses( + keepLastInit = false, + commonAddresses = commonAddresses + ) + + // Should drop addresses[0,1,2,3] (init) and addresses[8,9] (common) + assertEquals(listOf(14, 15, 16, 17), filtered) + } + + @Test + fun testCompleteFilteringPipeline_WithIndexOutOfBoundsRegression() { + // This test recreates the exact scenario that caused the original crash + val addresses = (100L..126L).toList() // 27 addresses + val stackTrace = arrayOf( + "kfun:kotlin.Throwable#(kotlin.String?){} + 24", + "kfun:LargeException#(kotlin.String?){} + 10", + "kfun:my.app.class#function1(){} + 50" + ) + (3..26).map { "kfun:my.app.class#function$it(){} + ${it * 10}" }.toTypedArray() + + // Create a large common addresses list (27 elements) that would trigger the bug + val commonAddresses = (0L..26L).toList() // This caused the original IndexOutOfBoundsException + + val throwable = createMockThrowable(addresses, stackTrace, "LargeException") + + // This should NOT throw IndexOutOfBoundsException + val filtered = throwable.getFilteredStackTraceAddresses( + keepLastInit = false, + commonAddresses = commonAddresses + ) + + // Should process without crashing + assertNotNull(filtered) + assertTrue("Filtered addresses should be a valid list", filtered is List) + } + + @Test + fun testCompleteFilteringPipeline_KeepLastInit() { + val addresses = listOf(20, 21, 22, 23, 24, 25, 26) + val stackTrace = arrayOf( + "kfun:kotlin.Throwable#(kotlin.String?){} + 24", + "kfun:CustomException#(kotlin.String?){} + 10", // addresses[1] + "kfun:CustomException#(){} + 12", // addresses[2] - last init, should be kept + "kfun:my.app.class#function1(){} + 50", // addresses[3] + "kfun:my.app.class#function2(){} + 60", // addresses[4] + "kfun:my.app.class#function3(){} + 70", // addresses[5] - common + "kfun:my.app.class#function4(){} + 80" // addresses[6] - common + ) + val commonAddresses = listOf(25, 26) // Last two addresses + + val throwable = createMockThrowable(addresses, stackTrace, "CustomException") + val filtered = throwable.getFilteredStackTraceAddresses( + keepLastInit = true, + commonAddresses = commonAddresses + ) + + // Should drop addresses[0,1] (init except last), and addresses[5,6] (common) + assertEquals(listOf(22, 23, 24), filtered) + } + + @Test + fun testCompleteFilteringPipeline_EmptyAfterFiltering() { + val addresses = listOf(30, 31, 32) + val stackTrace = arrayOf( + "kfun:kotlin.Throwable#(kotlin.String?){} + 24", + "kfun:SmallException#(kotlin.String?){} + 10", + "kfun:my.app.class#function1(){} + 50" + ) + val commonAddresses = listOf(32) // The only remaining address after init filtering + + val throwable = createMockThrowable(addresses, stackTrace, "SmallException") + val filtered = throwable.getFilteredStackTraceAddresses( + keepLastInit = false, + commonAddresses = commonAddresses + ) + + // Should result in empty list after all filtering + assertEquals(emptyList(), filtered) + } + + @Test + fun testCompleteFilteringPipeline_LargeStackTrace() { + // Test with a large stack trace to ensure performance and correctness + val addresses = (1000L..1099L).toList() // 100 addresses + val stackTrace = arrayOf( + "kfun:kotlin.Throwable#(kotlin.String?){} + 24", + "kfun:LargeStackException#(kotlin.String?){} + 10" + ) + (2..99).map { "kfun:my.app.class#function$it(){} + ${it * 10}" }.toTypedArray() + + val commonAddresses = (1080L..1099L).toList() // Last 20 addresses + + val throwable = createMockThrowable(addresses, stackTrace, "LargeStackException") + val filtered = throwable.getFilteredStackTraceAddresses( + keepLastInit = false, + commonAddresses = commonAddresses + ) + + // Should have 78 addresses remaining (100 - 2 init - 20 common) + assertEquals(78, filtered.size) + assertEquals(1002L, filtered.first()) // First non-init address + assertEquals(1079L, filtered.last()) // Last non-common address + } + + @Test + fun testCompleteFilteringPipeline_NoInitAddresses() { + val addresses = listOf(40, 41, 42, 43, 44) + val stackTrace = arrayOf( + "kfun:my.app.class#function1(){} + 50", + "kfun:my.app.class#function2(){} + 60", + "kfun:my.app.class#function3(){} + 70", + "kfun:my.app.class#function4(){} + 80", + "kfun:my.app.class#function5(){} + 90" + ) + val commonAddresses = listOf(43, 44) // Last two + + val throwable = createMockThrowable(addresses, stackTrace, "NoInitException") + val filtered = throwable.getFilteredStackTraceAddresses( + keepLastInit = false, + commonAddresses = commonAddresses + ) + + // Should only drop common addresses, no init addresses to drop + assertEquals(listOf(40, 41, 42), filtered) + } + + @Test + fun testCompleteFilteringPipeline_NoCommonAddresses() { + val addresses = listOf(50, 51, 52, 53, 54) + val stackTrace = arrayOf( + "kfun:kotlin.Throwable#(kotlin.String?){} + 24", + "kfun:NoCommonException#(kotlin.String?){} + 10", + "kfun:my.app.class#function1(){} + 50", + "kfun:my.app.class#function2(){} + 60", + "kfun:my.app.class#function3(){} + 70" + ) + val commonAddresses = emptyList() // No common addresses + + val throwable = createMockThrowable(addresses, stackTrace, "NoCommonException") + val filtered = throwable.getFilteredStackTraceAddresses( + keepLastInit = false, + commonAddresses = commonAddresses + ) + + // Should only drop init addresses, no common addresses to drop + assertEquals(listOf(52, 53, 54), filtered) + } + + @Test + fun testCompleteFilteringPipeline_EdgeCaseSequentialIndices() { + // Test the specific edge case where sequential processing could cause issues + val addresses = listOf(60, 61, 62, 63, 64, 65) + val stackTrace = arrayOf( + "kfun:EdgeCaseException#(kotlin.String?){} + 10", + "kfun:my.app.class#function1(){} + 50", + "kfun:my.app.class#function2(){} + 60", + "kfun:my.app.class#function3(){} + 70", + "kfun:my.app.class#function4(){} + 80", + "kfun:my.app.class#function5(){} + 90" + ) + val commonAddresses = listOf(64, 65) // Sequential at the end + + val throwable = createMockThrowable(addresses, stackTrace, "EdgeCaseException") + val filtered = throwable.getFilteredStackTraceAddresses( + keepLastInit = false, + commonAddresses = commonAddresses + ) + + assertEquals(listOf(61, 62, 63), filtered) + } +} \ No newline at end of file From a2e95556005b6156188e298b3886b17e8ade0ded Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 30 Jul 2025 15:12:58 +0200 Subject: [PATCH 3/8] Update --- .../nsexception/CommonAddressesTests.kt | 15 +- .../StackTraceFilteringIntegrationTests.kt | 249 ------------------ 2 files changed, 1 insertion(+), 263 deletions(-) delete mode 100644 sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/StackTraceFilteringIntegrationTests.kt diff --git a/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/CommonAddressesTests.kt b/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/CommonAddressesTests.kt index b7ddecd8..1bb53bef 100644 --- a/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/CommonAddressesTests.kt +++ b/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/CommonAddressesTests.kt @@ -17,12 +17,8 @@ package io.sentry.kotlin.multiplatform.nsexception import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertSame -import kotlin.test.assertTrue class CommonAddressesTests { - - // MARK: - Basic Functionality Tests (Positive Cases) - @Test fun testDropCommon() { val commonAddresses = listOf(5, 4, 3, 2, 1, 0) @@ -63,8 +59,6 @@ class CommonAddressesTests { assertSame(addresses, withoutCommonAddresses) } - // MARK: - Edge Cases (Boundary Conditions) - @Test fun testDropCommonEmptyCommon() { val addresses = listOf(0, 1, 2) @@ -111,8 +105,6 @@ class CommonAddressesTests { assertEquals(listOf(9, 8, 7, 6, 5, 4), withoutCommonAddresses) } - // MARK: - Regression Tests (Prevent IndexOutOfBoundsException) - @Test fun testDropCommonNoIndexOutOfBounds_LargeCommonList() { // This test specifically targets the original bug where i-- could become -1 @@ -153,15 +145,12 @@ class CommonAddressesTests { assertEquals(listOf(5, 4, 3, 2, 1), withoutCommonAddresses) } - // MARK: - Performance and Stress Tests - @Test fun testDropCommonLargeList() { val commonAddresses = (0L..999L).toList().reversed() val addresses = (500L..1499L).toList() val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) - assertTrue("Result should contain elements not in common", withoutCommonAddresses.isNotEmpty()) assertEquals(500, withoutCommonAddresses.size) // Should keep 1000-1499 } @@ -169,7 +158,7 @@ class CommonAddressesTests { fun testDropCommonRepeatedElements() { val commonAddresses = listOf(1, 1, 1, 0, 0) val addresses = listOf(5, 4, 1, 1, 0) - + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) assertEquals(listOf(5, 4, 1, 1), withoutCommonAddresses) } @@ -204,8 +193,6 @@ class CommonAddressesTests { assertEquals(listOf(9, 8), withoutCommonAddresses) // Should drop 3,2,1 but keep others } - // MARK: - Negative Test Cases - @Test fun testDropCommonWithNegativeNumbers() { val commonAddresses = listOf(-1, -2, -3) diff --git a/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/StackTraceFilteringIntegrationTests.kt b/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/StackTraceFilteringIntegrationTests.kt deleted file mode 100644 index 3584fc7b..00000000 --- a/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/StackTraceFilteringIntegrationTests.kt +++ /dev/null @@ -1,249 +0,0 @@ -// Integration tests for the complete stack trace filtering pipeline -// -// Copyright (c) 2024 Sentry -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -package io.sentry.kotlin.multiplatform.nsexception - -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertTrue -import kotlin.test.assertNotNull - -class StackTraceFilteringIntegrationTests { - - // MARK: - Mock Implementation for Testing - - // Mock extension function to simulate getStackTraceAddresses for testing - private fun createMockThrowable( - addresses: List, - stackTrace: Array, - className: String = "TestException" - ): TestThrowable { - return TestThrowable(addresses, stackTrace, className) - } - - private class TestThrowable( - private val mockAddresses: List, - private val mockStackTrace: Array, - private val mockClassName: String - ) : Throwable() { - - fun getStackTraceAddresses(): List = mockAddresses - - override fun getStackTrace(): Array { - return mockStackTrace.map { - StackTraceElement("", "", "", 0) - }.toTypedArray() - } - - fun getStackTrace(): Array = mockStackTrace - - fun getFilteredStackTraceAddresses( - keepLastInit: Boolean = false, - commonAddresses: List = emptyList() - ): List = getStackTraceAddresses().dropInitAddresses( - qualifiedClassName = mockClassName, - stackTrace = getStackTrace(), - keepLast = keepLastInit - ).dropCommonAddresses(commonAddresses) - } - - // MARK: - Integration Tests - - @Test - fun testCompleteFilteringPipeline_NormalCase() { - val addresses = listOf(10, 11, 12, 13, 14, 15, 16, 17, 18, 19) - val stackTrace = arrayOf( - "kfun:kotlin.Throwable#(kotlin.String?){} + 24", - "kfun:kotlin.Exception#(kotlin.String?){} + 5", - "kfun:TestException#(kotlin.String?){} + 10", // addresses[2] - "kfun:TestException#(){} + 12", // addresses[3] - "kfun:my.app.class#function1(){} + 50", // addresses[4] - should be kept - "kfun:my.app.class#function2(){} + 60", // addresses[5] - should be kept - "kfun:my.app.class#function3(){} + 70", // addresses[6] - should be kept - "kfun:my.app.class#function4(){} + 80", // addresses[7] - should be kept - "kfun:my.app.class#function5(){} + 90", // addresses[8] - common - "kfun:my.app.class#function6(){} + 100" // addresses[9] - common - ) - val commonAddresses = listOf(18, 19) // Last two addresses - - val throwable = createMockThrowable(addresses, stackTrace, "TestException") - val filtered = throwable.getFilteredStackTraceAddresses( - keepLastInit = false, - commonAddresses = commonAddresses - ) - - // Should drop addresses[0,1,2,3] (init) and addresses[8,9] (common) - assertEquals(listOf(14, 15, 16, 17), filtered) - } - - @Test - fun testCompleteFilteringPipeline_WithIndexOutOfBoundsRegression() { - // This test recreates the exact scenario that caused the original crash - val addresses = (100L..126L).toList() // 27 addresses - val stackTrace = arrayOf( - "kfun:kotlin.Throwable#(kotlin.String?){} + 24", - "kfun:LargeException#(kotlin.String?){} + 10", - "kfun:my.app.class#function1(){} + 50" - ) + (3..26).map { "kfun:my.app.class#function$it(){} + ${it * 10}" }.toTypedArray() - - // Create a large common addresses list (27 elements) that would trigger the bug - val commonAddresses = (0L..26L).toList() // This caused the original IndexOutOfBoundsException - - val throwable = createMockThrowable(addresses, stackTrace, "LargeException") - - // This should NOT throw IndexOutOfBoundsException - val filtered = throwable.getFilteredStackTraceAddresses( - keepLastInit = false, - commonAddresses = commonAddresses - ) - - // Should process without crashing - assertNotNull(filtered) - assertTrue("Filtered addresses should be a valid list", filtered is List) - } - - @Test - fun testCompleteFilteringPipeline_KeepLastInit() { - val addresses = listOf(20, 21, 22, 23, 24, 25, 26) - val stackTrace = arrayOf( - "kfun:kotlin.Throwable#(kotlin.String?){} + 24", - "kfun:CustomException#(kotlin.String?){} + 10", // addresses[1] - "kfun:CustomException#(){} + 12", // addresses[2] - last init, should be kept - "kfun:my.app.class#function1(){} + 50", // addresses[3] - "kfun:my.app.class#function2(){} + 60", // addresses[4] - "kfun:my.app.class#function3(){} + 70", // addresses[5] - common - "kfun:my.app.class#function4(){} + 80" // addresses[6] - common - ) - val commonAddresses = listOf(25, 26) // Last two addresses - - val throwable = createMockThrowable(addresses, stackTrace, "CustomException") - val filtered = throwable.getFilteredStackTraceAddresses( - keepLastInit = true, - commonAddresses = commonAddresses - ) - - // Should drop addresses[0,1] (init except last), and addresses[5,6] (common) - assertEquals(listOf(22, 23, 24), filtered) - } - - @Test - fun testCompleteFilteringPipeline_EmptyAfterFiltering() { - val addresses = listOf(30, 31, 32) - val stackTrace = arrayOf( - "kfun:kotlin.Throwable#(kotlin.String?){} + 24", - "kfun:SmallException#(kotlin.String?){} + 10", - "kfun:my.app.class#function1(){} + 50" - ) - val commonAddresses = listOf(32) // The only remaining address after init filtering - - val throwable = createMockThrowable(addresses, stackTrace, "SmallException") - val filtered = throwable.getFilteredStackTraceAddresses( - keepLastInit = false, - commonAddresses = commonAddresses - ) - - // Should result in empty list after all filtering - assertEquals(emptyList(), filtered) - } - - @Test - fun testCompleteFilteringPipeline_LargeStackTrace() { - // Test with a large stack trace to ensure performance and correctness - val addresses = (1000L..1099L).toList() // 100 addresses - val stackTrace = arrayOf( - "kfun:kotlin.Throwable#(kotlin.String?){} + 24", - "kfun:LargeStackException#(kotlin.String?){} + 10" - ) + (2..99).map { "kfun:my.app.class#function$it(){} + ${it * 10}" }.toTypedArray() - - val commonAddresses = (1080L..1099L).toList() // Last 20 addresses - - val throwable = createMockThrowable(addresses, stackTrace, "LargeStackException") - val filtered = throwable.getFilteredStackTraceAddresses( - keepLastInit = false, - commonAddresses = commonAddresses - ) - - // Should have 78 addresses remaining (100 - 2 init - 20 common) - assertEquals(78, filtered.size) - assertEquals(1002L, filtered.first()) // First non-init address - assertEquals(1079L, filtered.last()) // Last non-common address - } - - @Test - fun testCompleteFilteringPipeline_NoInitAddresses() { - val addresses = listOf(40, 41, 42, 43, 44) - val stackTrace = arrayOf( - "kfun:my.app.class#function1(){} + 50", - "kfun:my.app.class#function2(){} + 60", - "kfun:my.app.class#function3(){} + 70", - "kfun:my.app.class#function4(){} + 80", - "kfun:my.app.class#function5(){} + 90" - ) - val commonAddresses = listOf(43, 44) // Last two - - val throwable = createMockThrowable(addresses, stackTrace, "NoInitException") - val filtered = throwable.getFilteredStackTraceAddresses( - keepLastInit = false, - commonAddresses = commonAddresses - ) - - // Should only drop common addresses, no init addresses to drop - assertEquals(listOf(40, 41, 42), filtered) - } - - @Test - fun testCompleteFilteringPipeline_NoCommonAddresses() { - val addresses = listOf(50, 51, 52, 53, 54) - val stackTrace = arrayOf( - "kfun:kotlin.Throwable#(kotlin.String?){} + 24", - "kfun:NoCommonException#(kotlin.String?){} + 10", - "kfun:my.app.class#function1(){} + 50", - "kfun:my.app.class#function2(){} + 60", - "kfun:my.app.class#function3(){} + 70" - ) - val commonAddresses = emptyList() // No common addresses - - val throwable = createMockThrowable(addresses, stackTrace, "NoCommonException") - val filtered = throwable.getFilteredStackTraceAddresses( - keepLastInit = false, - commonAddresses = commonAddresses - ) - - // Should only drop init addresses, no common addresses to drop - assertEquals(listOf(52, 53, 54), filtered) - } - - @Test - fun testCompleteFilteringPipeline_EdgeCaseSequentialIndices() { - // Test the specific edge case where sequential processing could cause issues - val addresses = listOf(60, 61, 62, 63, 64, 65) - val stackTrace = arrayOf( - "kfun:EdgeCaseException#(kotlin.String?){} + 10", - "kfun:my.app.class#function1(){} + 50", - "kfun:my.app.class#function2(){} + 60", - "kfun:my.app.class#function3(){} + 70", - "kfun:my.app.class#function4(){} + 80", - "kfun:my.app.class#function5(){} + 90" - ) - val commonAddresses = listOf(64, 65) // Sequential at the end - - val throwable = createMockThrowable(addresses, stackTrace, "EdgeCaseException") - val filtered = throwable.getFilteredStackTraceAddresses( - keepLastInit = false, - commonAddresses = commonAddresses - ) - - assertEquals(listOf(61, 62, 63), filtered) - } -} \ No newline at end of file From fd8649b483fb8cc1eb2c9327d8a468658477ffb0 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 30 Jul 2025 15:23:48 +0200 Subject: [PATCH 4/8] Clean up tests --- .../multiplatform/nsexception/Throwable.kt | 2 +- .../nsexception/CommonAddressesTests.kt | 76 +++---------------- 2 files changed, 12 insertions(+), 66 deletions(-) diff --git a/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt b/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt index ba95a646..00e441c6 100644 --- a/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt +++ b/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt @@ -76,7 +76,7 @@ internal fun List.dropCommonAddresses( commonAddresses: List ): List { if (commonAddresses.isEmpty() || this.isEmpty()) return this - + var commonIndex = commonAddresses.size - 1 return dropLastWhile { address -> if (commonIndex < 0 || commonIndex >= commonAddresses.size) { diff --git a/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/CommonAddressesTests.kt b/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/CommonAddressesTests.kt index 1bb53bef..9fb7813e 100644 --- a/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/CommonAddressesTests.kt +++ b/sentry-kotlin-multiplatform/src/appleTest/kotlin/io/sentry/kotlin/multiplatform/nsexception/CommonAddressesTests.kt @@ -40,7 +40,7 @@ class CommonAddressesTests { val commonAddresses = listOf(5, 4, 3) val addresses = listOf(9, 8, 7, 6) val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) - assertSame(addresses, withoutCommonAddresses) + assertEquals(addresses, withoutCommonAddresses) } @Test @@ -51,14 +51,6 @@ class CommonAddressesTests { assertEquals(listOf(5, 4, 3), withoutCommonAddresses) } - @Test - fun testDropCommonSingleElementNoMatch() { - val commonAddresses = listOf(2) - val addresses = listOf(5, 4, 3, 1) - val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) - assertSame(addresses, withoutCommonAddresses) - } - @Test fun testDropCommonEmptyCommon() { val addresses = listOf(0, 1, 2) @@ -74,14 +66,6 @@ class CommonAddressesTests { assertSame(addresses, withoutCommonAddresses) } - @Test - fun testDropCommonBothEmpty() { - val addresses = emptyList() - val commonAddresses = emptyList() - val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) - assertSame(addresses, withoutCommonAddresses) - } - @Test fun testDropCommonSameAddresses() { val addresses = listOf(0, 1, 2) @@ -110,7 +94,7 @@ class CommonAddressesTests { // This test specifically targets the original bug where i-- could become -1 val commonAddresses = (0L..26L).toList().reversed() // 27 elements: [26, 25, ..., 1, 0] val addresses = listOf(30, 29, 28, 2, 1, 0) - + // This should not throw IndexOutOfBoundsException val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) assertEquals(listOf(30, 29, 28), withoutCommonAddresses) @@ -121,7 +105,7 @@ class CommonAddressesTests { // Test when commonAddresses.size equals the number of matching elements val commonAddresses = listOf(4, 3, 2, 1, 0) val addresses = listOf(9, 8, 7, 4, 3, 2, 1, 0) - + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) assertEquals(listOf(9, 8, 7), withoutCommonAddresses) } @@ -131,27 +115,9 @@ class CommonAddressesTests { // Test the exact scenario that caused the original crash val commonAddresses = (1L..27L).toList() // size: 27 val addresses = listOf(100L, 99L, 98L) + commonAddresses.takeLast(3) - - val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) - assertEquals(listOf(100L, 99L, 98L), withoutCommonAddresses) - } - @Test - fun testDropCommonNoIndexOutOfBounds_SingleElementCommon() { - val commonAddresses = listOf(0) - val addresses = listOf(5, 4, 3, 2, 1, 0) - val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) - assertEquals(listOf(5, 4, 3, 2, 1), withoutCommonAddresses) - } - - @Test - fun testDropCommonLargeList() { - val commonAddresses = (0L..999L).toList().reversed() - val addresses = (500L..1499L).toList() - - val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) - assertEquals(500, withoutCommonAddresses.size) // Should keep 1000-1499 + assertEquals(listOf(100L, 99L, 98L), withoutCommonAddresses) } @Test @@ -163,13 +129,11 @@ class CommonAddressesTests { assertEquals(listOf(5, 4, 1, 1), withoutCommonAddresses) } - // MARK: - Sequence and Order Tests - @Test fun testDropCommonMaintainsOrder() { val commonAddresses = listOf(3, 2, 1) val addresses = listOf(9, 8, 7, 6, 5, 3, 2, 1) - + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) assertEquals(listOf(9, 8, 7, 6, 5), withoutCommonAddresses) } @@ -179,44 +143,26 @@ class CommonAddressesTests { // Common addresses are in different order than in the target list val commonAddresses = listOf(1, 3, 2) // Note: 3 and 2 are swapped val addresses = listOf(9, 8, 7, 6, 2) - - val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) - assertEquals(listOf(9, 8, 7, 6), withoutCommonAddresses) // Should drop 2 - } - @Test - fun testDropCommonPartialSequenceMatch() { - val commonAddresses = listOf(4, 3, 2, 1) - val addresses = listOf(9, 8, 3, 2, 1) // Missing 4 in sequence - val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) - assertEquals(listOf(9, 8), withoutCommonAddresses) // Should drop 3,2,1 but keep others - } - - @Test - fun testDropCommonWithNegativeNumbers() { - val commonAddresses = listOf(-1, -2, -3) - val addresses = listOf(1, 0, -1, -2, -3) - - val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) - assertEquals(listOf(1, 0), withoutCommonAddresses) + assertEquals(listOf(9, 8, 7, 6), withoutCommonAddresses) // Should drop 2 } @Test fun testDropCommonMixedPositiveNegative() { val commonAddresses = listOf(1, 0, -1) val addresses = listOf(5, 4, 3, 1, 0, -1) - + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) assertEquals(listOf(5, 4, 3), withoutCommonAddresses) } @Test fun testDropCommonLargeNumbers() { - val commonAddresses = listOf(Long.MAX_VALUE - 1, Long.MAX_VALUE - 2) - val addresses = listOf(Long.MAX_VALUE, Long.MAX_VALUE - 1, Long.MAX_VALUE - 2) - + val commonAddresses = listOf(Long.MAX_VALUE - 1, Long.MAX_VALUE - 2) + val addresses = listOf(Long.MAX_VALUE, Long.MAX_VALUE - 1, Long.MAX_VALUE - 2) + val withoutCommonAddresses = addresses.dropCommonAddresses(commonAddresses) - assertEquals(listOf(Long.MAX_VALUE), withoutCommonAddresses) + assertEquals(listOf(Long.MAX_VALUE), withoutCommonAddresses) } } From 2e9a17b11708af3b931149640462a625e4c23d84 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 30 Jul 2025 15:28:58 +0200 Subject: [PATCH 5/8] Update CHANGELOG --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 831e06b8..62cd9e0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Fixes + +- Fix stack trace crash on Apple targets ([#434](https://github.com/getsentry/sentry-kotlin-multiplatform/pull/434)) + ## 0.17.1 ### Fixes From 2755b088670271b2ca68d6c43e37c1050ea42b11 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 30 Jul 2025 18:07:12 +0200 Subject: [PATCH 6/8] Simplify impl --- .../kotlin/multiplatform/nsexception/Throwable.kt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt b/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt index 00e441c6..30e35d97 100644 --- a/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt +++ b/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt @@ -75,16 +75,15 @@ internal fun List.dropInitAddresses( internal fun List.dropCommonAddresses( commonAddresses: List ): List { - if (commonAddresses.isEmpty() || this.isEmpty()) return this + var i = commonAddresses.size - 1 + if (i < 0) return this - var commonIndex = commonAddresses.size - 1 - return dropLastWhile { address -> - if (commonIndex < 0 || commonIndex >= commonAddresses.size) { - false + return dropLastWhile { it -> + if (i >= 0 && commonAddresses[i] == it) { + i-- + true } else { - val matches = commonAddresses[commonIndex] == address - if (matches) commonIndex-- - matches + false } } } From d8c9dacd0e7eb7e1d683eaeec8160072175abaf5 Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 30 Jul 2025 18:27:27 +0200 Subject: [PATCH 7/8] Simplify impl even more --- .../sentry/kotlin/multiplatform/nsexception/Throwable.kt | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt b/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt index 30e35d97..c9299f12 100644 --- a/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt +++ b/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt @@ -79,11 +79,6 @@ internal fun List.dropCommonAddresses( if (i < 0) return this return dropLastWhile { it -> - if (i >= 0 && commonAddresses[i] == it) { - i-- - true - } else { - false - } + i-- > 0 && commonAddresses[i] == it } } From 84f403c0926661a93950c8ced34611e32c40a37b Mon Sep 17 00:00:00 2001 From: Giancarlo Buenaflor Date: Wed, 30 Jul 2025 18:28:21 +0200 Subject: [PATCH 8/8] Simplify impl even more --- .../io/sentry/kotlin/multiplatform/nsexception/Throwable.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt b/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt index c9299f12..27d8f8ec 100644 --- a/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt +++ b/sentry-kotlin-multiplatform/src/appleMain/kotlin/io/sentry/kotlin/multiplatform/nsexception/Throwable.kt @@ -75,10 +75,10 @@ internal fun List.dropInitAddresses( internal fun List.dropCommonAddresses( commonAddresses: List ): List { - var i = commonAddresses.size - 1 - if (i < 0) return this + var i = commonAddresses.size + if (i == 0) return this - return dropLastWhile { it -> + return dropLastWhile { i-- > 0 && commonAddresses[i] == it } }