Skip to content

Commit 739129d

Browse files
Merge pull request #166 from backtrace-labs/anr-app-exit
ANR based on ApplicationExitInfo
2 parents e950372 + a137d5c commit 739129d

27 files changed

+2355
-18
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package backtraceio.library;
2+
3+
import java.io.InputStream;
4+
5+
public class TestUtils {
6+
7+
public static InputStream readFileAsStream(Object obj, String fileName) {
8+
ClassLoader classLoader = obj.getClass().getClassLoader();
9+
InputStream inputStream = classLoader.getResourceAsStream(fileName);
10+
11+
if (inputStream != null) {
12+
return inputStream;
13+
}
14+
return null;
15+
}
16+
}
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package backtraceio.library.anr;
2+
3+
import static org.junit.Assert.fail;
4+
import static org.mockito.ArgumentMatchers.anyLong;
5+
import static org.mockito.Mockito.doNothing;
6+
import static org.mockito.Mockito.mock;
7+
import static org.mockito.Mockito.when;
8+
9+
import android.app.ApplicationExitInfo;
10+
import android.content.Context;
11+
12+
import androidx.test.ext.junit.runners.AndroidJUnit4;
13+
import androidx.test.filters.SdkSuppress;
14+
import androidx.test.platform.app.InstrumentationRegistry;
15+
16+
import net.jodah.concurrentunit.Waiter;
17+
18+
import org.junit.Before;
19+
import org.junit.Test;
20+
import org.junit.runner.RunWith;
21+
import org.mockito.Mock;
22+
23+
import java.io.IOException;
24+
import java.io.InputStream;
25+
import java.util.ArrayList;
26+
import java.util.Collections;
27+
import java.util.List;
28+
import java.util.Map;
29+
import java.util.concurrent.TimeUnit;
30+
31+
import backtraceio.library.BacktraceClient;
32+
import backtraceio.library.BacktraceCredentials;
33+
import backtraceio.library.TestUtils;
34+
import backtraceio.library.events.RequestHandler;
35+
import backtraceio.library.models.BacktraceApiResult;
36+
import backtraceio.library.models.BacktraceData;
37+
import backtraceio.library.models.BacktraceResult;
38+
39+
@RunWith(AndroidJUnit4.class)
40+
public class BacktraceAppExitInfoSenderHandlerTest {
41+
@Mock
42+
private Context mockContext;
43+
44+
private final String PACKAGE_NAME = "backtrace.io.tests";
45+
46+
private final String ANR_APPEXIT_STACKTRACE_FILE = "anrAppExitInfoStacktrace.txt";
47+
48+
private final BacktraceCredentials credentials = new BacktraceCredentials("https://example-endpoint.com/", "");
49+
private BacktraceClient backtraceClient;
50+
51+
@Before
52+
public void setUp() throws Exception {
53+
this.mockContext = InstrumentationRegistry.getInstrumentation().getContext();
54+
this.backtraceClient = new BacktraceClient(this.mockContext, credentials);
55+
}
56+
57+
private ExitInfo mockApplicationExitInfo(String description, Long timestamp, int reason,
58+
int pid, int importance, long pss, long rss, InputStream stacktrace) throws IOException {
59+
ExitInfo mockExitInfo = mock(ExitInfo.class);
60+
when(mockExitInfo.getDescription()).thenReturn(description);
61+
when(mockExitInfo.getTimestamp()).thenReturn(timestamp);
62+
when(mockExitInfo.getReason()).thenReturn(reason);
63+
when(mockExitInfo.getPid()).thenReturn(pid);
64+
when(mockExitInfo.getImportance()).thenReturn(importance);
65+
when(mockExitInfo.getPss()).thenReturn(pss);
66+
when(mockExitInfo.getRss()).thenReturn(rss);
67+
when(mockExitInfo.getTraceInputStream()).thenReturn(stacktrace);
68+
return mockExitInfo;
69+
}
70+
71+
private ExitInfo mockApplicationExitInfo(String description, Long timestamp, int reason, InputStream stacktrace) throws IOException {
72+
return mockApplicationExitInfo(description, timestamp, reason, 0, 0, 0L, 0L, stacktrace);
73+
}
74+
75+
private ProcessExitInfoProvider mockActivityManagerExitInfoProvider() throws IOException {
76+
ActivityManagerExitInfoProvider mock = mock(ActivityManagerExitInfoProvider.class);
77+
final List<ExitInfo> exitInfoList = new ArrayList<>();
78+
exitInfoList.add(mockApplicationExitInfo("random-text", System.currentTimeMillis(), ApplicationExitInfo.REASON_CRASH_NATIVE, null));
79+
exitInfoList.add(mockApplicationExitInfo("anr", System.currentTimeMillis(), ApplicationExitInfo.REASON_ANR, TestUtils.readFileAsStream(this, ANR_APPEXIT_STACKTRACE_FILE)));
80+
exitInfoList.add(mockApplicationExitInfo("anr without stacktrace", System.currentTimeMillis(), ApplicationExitInfo.REASON_ANR, null));
81+
exitInfoList.add(mockApplicationExitInfo("random-description", System.currentTimeMillis(), ApplicationExitInfo.REASON_LOW_MEMORY, null));
82+
83+
when(mock.getHistoricalExitInfo(PACKAGE_NAME, 0, 0)).thenReturn(exitInfoList);
84+
when(mock.getSupportedTypesOfExitInfo()).thenReturn(Collections.singletonList(ApplicationExitInfo.REASON_ANR));
85+
return mock;
86+
}
87+
88+
private AnrExitInfoState mockAnrExitInfoState() {
89+
AnrExitInfoState mock = mock(AnrExitInfoState.class);
90+
doNothing().when(mock).saveTimestamp(anyLong());
91+
when(mock.getLastTimestamp()).thenReturn(0L);
92+
return mock;
93+
}
94+
95+
@Test
96+
@SdkSuppress(minSdkVersion = android.os.Build.VERSION_CODES.R)
97+
public void checkIfANRIsSentFromAppExitInfo() throws IOException {
98+
// GIVEN
99+
final ProcessExitInfoProvider mockProcessExitInfoProvider = mockActivityManagerExitInfoProvider();
100+
final AnrExitInfoState anrExitInfoState = mockAnrExitInfoState();
101+
final Waiter waiter = new Waiter();
102+
backtraceClient.setOnRequestHandler(new RequestHandler() {
103+
@Override
104+
public BacktraceResult onRequest(BacktraceData data) {
105+
106+
Map<String, String> attributes = data.getAttributes();
107+
Map<String, Object> annotations = data.getAnnotations();
108+
Map<String, Object> anrAnnotations = (Map<String, Object>) annotations.get("ANR annotations");
109+
110+
waiter.assertEquals(data.getReport().getException().getStackTrace().length, 33);
111+
112+
waiter.assertNotNull(anrAnnotations);
113+
waiter.assertNotNull(attributes);
114+
waiter.assertEquals("anr", anrAnnotations.get("description"));
115+
waiter.assertEquals(ApplicationExitInfo.REASON_ANR, anrAnnotations.get("reason-code"));
116+
waiter.assertEquals("anr", anrAnnotations.get("reason"));
117+
waiter.assertTrue(((Map<String, Object>)annotations.get("ANR parsed stacktrace")).size() > 0);
118+
waiter.assertEquals("backtraceio.library.anr.BacktraceANRExitInfoException", attributes.get("classifier"));
119+
waiter.assertEquals("Hang", attributes.get("error.type"));
120+
waiter.assertTrue(attributes.get("ANR stacktrace").length() > 0);
121+
waiter.resume();
122+
123+
return new BacktraceResult(new BacktraceApiResult("_", "ok"));
124+
}
125+
});
126+
// WHEN
127+
new BacktraceAppExitInfoSenderHandler(this.backtraceClient, PACKAGE_NAME, anrExitInfoState, mockProcessExitInfoProvider);
128+
129+
// THEN
130+
try {
131+
waiter.await(5, TimeUnit.SECONDS); // Check if anr is detected and event was emitted
132+
} catch (Exception ex) {
133+
fail(ex.getMessage());
134+
}
135+
}
136+
137+
@Test
138+
@SdkSuppress(maxSdkVersion = android.os.Build.VERSION_CODES.Q)
139+
public void checkIfANRIsNotSentOnOldSDK() throws IOException {
140+
// GIVEN
141+
final int THREAD_SLEEP_TIME_MS = 3000;
142+
final ProcessExitInfoProvider mockProcessExitInfoProvider = mockActivityManagerExitInfoProvider();
143+
final AnrExitInfoState anrExitInfoState = mockAnrExitInfoState();
144+
final Waiter waiter = new Waiter();
145+
backtraceClient.setOnRequestHandler(new RequestHandler() {
146+
@Override
147+
public BacktraceResult onRequest(BacktraceData data) {
148+
waiter.fail();
149+
return new BacktraceResult(new BacktraceApiResult("_", "ok"));
150+
}
151+
});
152+
// WHEN
153+
new BacktraceAppExitInfoSenderHandler(this.backtraceClient, PACKAGE_NAME, anrExitInfoState, mockProcessExitInfoProvider);
154+
155+
// THEN
156+
try {
157+
Thread.sleep(THREAD_SLEEP_TIME_MS);
158+
} catch (Exception ex) {
159+
fail(ex.getMessage());
160+
}
161+
System.out.println("wat");
162+
}
163+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package backtraceio.library.common;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.mockito.Mockito.anyInt;
5+
import static org.mockito.Mockito.anyLong;
6+
import static org.mockito.Mockito.anyString;
7+
import static org.mockito.Mockito.verify;
8+
import static org.mockito.Mockito.when;
9+
10+
import android.content.Context;
11+
import android.content.SharedPreferences;
12+
13+
import org.junit.Before;
14+
import org.junit.Test;
15+
import org.junit.runner.RunWith;
16+
import org.mockito.Mock;
17+
import org.mockito.junit.MockitoJUnitRunner;
18+
19+
@RunWith(MockitoJUnitRunner.class)
20+
public class SharedPreferencesManagerTest {
21+
22+
private SharedPreferencesManager sharedPreferencesManager;
23+
24+
@Mock
25+
private Context mockContext;
26+
27+
@Mock
28+
private SharedPreferences mockSharedPreferences;
29+
30+
@Mock
31+
private SharedPreferences.Editor mockEditor;
32+
33+
@Before
34+
public void setUp() {
35+
sharedPreferencesManager = new SharedPreferencesManager(mockContext);
36+
when(mockContext.getSharedPreferences(anyString(), anyInt())).thenReturn(mockSharedPreferences);
37+
when(mockSharedPreferences.edit()).thenReturn(mockEditor);
38+
when(mockEditor.putLong(anyString(), anyLong())).thenReturn(mockEditor);
39+
}
40+
41+
@Test
42+
public void testSaveLongToSharedPreferences() {
43+
// GIVEN
44+
String prefName = "test_prefs";
45+
String key = "test_key";
46+
long value = 12345L;
47+
48+
// WHEN
49+
sharedPreferencesManager.saveLongToSharedPreferences(prefName, key, value);
50+
51+
// THEN
52+
verify(mockContext).getSharedPreferences(prefName, Context.MODE_PRIVATE);
53+
verify(mockSharedPreferences).edit();
54+
verify(mockEditor).putLong(key, value);
55+
verify(mockEditor).commit();
56+
}
57+
58+
@Test
59+
public void testReadLongFromSharedPreferences() {
60+
// GIVEN
61+
String prefName = "test_prefs";
62+
String key = "test_key";
63+
long defaultValue = 0L;
64+
Long expectedValue = 12345L;
65+
66+
// WHEN
67+
when(mockSharedPreferences.getLong(key, defaultValue)).thenReturn(expectedValue);
68+
69+
// THEN
70+
Long result = sharedPreferencesManager.readLongFromSharedPreferences(prefName, key, defaultValue);
71+
72+
verify(mockContext).getSharedPreferences(prefName, Context.MODE_PRIVATE);
73+
verify(mockSharedPreferences).getLong(key, defaultValue);
74+
assertEquals(expectedValue, result);
75+
}
76+
}

backtrace-library/src/androidTest/java/backtraceio/library/watchdog/BacktraceAnrTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public void setUp() {
4040
public void checkIfANRIsDetectedCorrectly() {
4141
// GIVEN
4242
final Waiter waiter = new Waiter();
43-
BacktraceANRWatchdog watchdog = new BacktraceANRWatchdog(this.backtraceClient, 500);
43+
BacktraceANRHandlerWatchdog watchdog = new BacktraceANRHandlerWatchdog(this.backtraceClient, 500);
4444
watchdog.setOnApplicationNotRespondingEvent(new OnApplicationNotRespondingEvent() {
4545
@Override
4646
public void onEvent(BacktraceWatchdogTimeoutException exception) {
@@ -96,7 +96,7 @@ public void checkIfANRIsNotDetected() {
9696
// GIVEN
9797
final int numberOfIterations = 5;
9898
final Waiter waiter = new Waiter();
99-
BacktraceANRWatchdog watchdog = new BacktraceANRWatchdog(this.backtraceClient, 5000);
99+
BacktraceANRHandlerWatchdog watchdog = new BacktraceANRHandlerWatchdog(this.backtraceClient, 5000);
100100
watchdog.setOnApplicationNotRespondingEvent(new OnApplicationNotRespondingEvent() {
101101
@Override
102102
public void onEvent(BacktraceWatchdogTimeoutException exception) {
@@ -145,4 +145,4 @@ public void onEvent(BacktraceWatchdogTimeoutException exception) {
145145
fail(e.getMessage());
146146
}
147147
}
148-
}
148+
}

0 commit comments

Comments
 (0)