Skip to content

Commit e2858ec

Browse files
committed
test: add e2e tests for Crashlytics
1 parent 866e6bd commit e2858ec

35 files changed

+1937
-0
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
**/google-services.json
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Firebase Crashlytics Test App
2+
3+
## Setup
4+
5+
Download the `google-services.json` file
6+
from [Firebase Console](https://console.firebase.google.com/) (for whatever Firebase project you
7+
have or want to integrate the `test-app`) and store it under the current directory.
8+
9+
Note: The [Package name](https://firebase.google.com/docs/android/setup#register-app) for your app
10+
created on the Firebase Console (for which the `google-services.json` is downloaded) must match
11+
the [applicationId](https://developer.android.com/studio/build/application-id.html) declared in
12+
the `test-app/test-app.gradle.kts` for the app to link to Firebase.
13+
14+
## Running
15+
16+
Run the test app directly from Android Studio by selecting and running
17+
the `firebase-crashlytics.test-app` run configuration.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-keep class com.google.firebase.** { *; }
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?xml version="1.0" encoding="utf-8"?><!--
2+
~ Copyright 2023 Google LLC
3+
~
4+
~ Licensed under the Apache License, Version 2.0 (the "License");
5+
~ you may not use this file except in compliance with the License.
6+
~ You may obtain a copy of the License at
7+
~
8+
~ http://www.apache.org/licenses/LICENSE-2.0
9+
~
10+
~ Unless required by applicable law or agreed to in writing, software
11+
~ distributed under the License is distributed on an "AS IS" BASIS,
12+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
~ See the License for the specific language governing permissions and
14+
~ limitations under the License.
15+
-->
16+
17+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
18+
xmlns:tools="http://schemas.android.com/tools"
19+
android:versionCode="1"
20+
android:versionName="1.0.0">
21+
22+
<application>
23+
24+
</application>
25+
26+
<uses-sdk tools:overrideLibrary="android_libs.ub_uiautomator" />
27+
28+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
/*
2+
* Copyright 2023 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.testing.sessions
18+
19+
import android.content.Context
20+
import android.content.Intent
21+
import androidx.test.core.app.ApplicationProvider
22+
import androidx.test.ext.junit.runners.AndroidJUnit4
23+
import androidx.test.platform.app.InstrumentationRegistry
24+
import androidx.test.uiautomator.By
25+
import androidx.test.uiautomator.UiDevice
26+
import androidx.test.uiautomator.UiObject2
27+
import androidx.test.uiautomator.Until
28+
import com.google.common.truth.Truth.assertThat
29+
import java.util.regex.Pattern
30+
import org.junit.After
31+
import org.junit.Assert.fail
32+
import org.junit.Before
33+
import org.junit.Test
34+
import org.junit.runner.RunWith
35+
36+
@RunWith(AndroidJUnit4::class)
37+
class FirebaseSessionsIntegrationTest {
38+
39+
private lateinit var device: UiDevice
40+
41+
@Before
42+
fun setup() {
43+
// Initialize UiDevice instance
44+
device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation())
45+
}
46+
47+
@After
48+
fun cleanup() {
49+
// Make sure all processes are killed
50+
Runtime.getRuntime().exec(arrayOf("am", "force-stop", TEST_APP_PACKAGE))
51+
}
52+
53+
@Test
54+
fun sameSessionIdBetweenActivitiesOnDifferentProcesses() {
55+
launchApp()
56+
57+
val sessionId1 = getCurrentSessionId()
58+
navigateToSecondActivity()
59+
Thread.sleep(TIME_TO_PROPAGATE_SESSION)
60+
val sessionId2 = getCurrentSessionId()
61+
62+
assertThat(sessionId1).isEqualTo(sessionId2)
63+
}
64+
65+
@Test
66+
fun sameSessionIdAfterQuickForegroundBackground() {
67+
launchApp()
68+
69+
val sessionId1 = getCurrentSessionId()
70+
background()
71+
foreground()
72+
val sessionId2 = getCurrentSessionId()
73+
74+
assertThat(sessionId1).isEqualTo(sessionId2)
75+
}
76+
77+
@Test
78+
fun newSessionIdAfterLongBackground() {
79+
launchApp()
80+
81+
val sessionId1 = getCurrentSessionId()
82+
background()
83+
// Test app overrides the background time from 30m, to 5s.
84+
Thread.sleep(6_000)
85+
foreground()
86+
device.waitForIdle()
87+
Thread.sleep(TIME_TO_PROPAGATE_SESSION)
88+
val sessionId2 = getCurrentSessionId()
89+
90+
assertThat(sessionId1).isNotEqualTo(sessionId2)
91+
}
92+
93+
@Test
94+
fun newSessionFollowingCrash() {
95+
if (!BuildConfig.SHOULD_CRASH_APP) return
96+
97+
launchApp()
98+
val origSession = getCurrentSessionId()
99+
getButton("CRASH!").click()
100+
dismissPossibleErrorDialog()
101+
102+
launchApp()
103+
104+
Thread.sleep(TIME_TO_PROPAGATE_SESSION)
105+
val newSession = getCurrentSessionId()
106+
assertThat(newSession).isNotEqualTo(origSession)
107+
}
108+
109+
@Test
110+
fun nonFatalMainActivity() {
111+
launchApp()
112+
val origSession = getCurrentSessionId()
113+
114+
getButton("NON FATAL").click()
115+
device.waitForIdle()
116+
117+
Thread.sleep(TIME_TO_PROPAGATE_SESSION)
118+
val newSession = getCurrentSessionId()
119+
assertThat(origSession).isEqualTo(newSession)
120+
}
121+
122+
@Test
123+
fun anrMainActivity() {
124+
if (!BuildConfig.SHOULD_CRASH_APP) return
125+
launchApp()
126+
val origSession = getCurrentSessionId()
127+
128+
getButton("ANR").click()
129+
device.waitForIdle()
130+
dismissPossibleAnrDialog()
131+
132+
launchApp()
133+
134+
Thread.sleep(TIME_TO_PROPAGATE_SESSION)
135+
val newSession = getCurrentSessionId()
136+
assertThat(origSession).isNotEqualTo(newSession)
137+
}
138+
139+
@Test
140+
fun crashSecondaryProcess() {
141+
if (!BuildConfig.SHOULD_CRASH_APP) return
142+
launchApp()
143+
navigateToSecondActivity()
144+
val origSession = getCurrentSessionId()
145+
146+
getButton("CRASH!").click()
147+
dismissPossibleErrorDialog()
148+
149+
launchApp()
150+
151+
Thread.sleep(TIME_TO_PROPAGATE_SESSION)
152+
val newSession = getCurrentSessionId()
153+
assertThat(newSession).isNotEqualTo(origSession)
154+
}
155+
156+
private fun launchApp() {
157+
// Start from the home screen
158+
device.pressHome()
159+
160+
// Wait for launcher
161+
device.wait(Until.hasObject(By.pkg(device.launcherPackageName).depth(0)), LAUNCH_TIMEOUT)
162+
163+
// Launch the app
164+
val context = ApplicationProvider.getApplicationContext<Context>()
165+
val intent =
166+
context.packageManager.getLaunchIntentForPackage(TEST_APP_PACKAGE)?.apply {
167+
// Clear out any previous instances
168+
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
169+
}
170+
context.startActivity(intent)
171+
172+
// Wait for the app to appear
173+
device.wait(Until.hasObject(By.pkg(TEST_APP_PACKAGE).depth(0)), LAUNCH_TIMEOUT)
174+
device.waitForIdle()
175+
}
176+
177+
private fun navigateToSecondActivity() {
178+
device.wait(Until.hasObject(By.text("NEXT ACTIVITY").depth(0)), TRANSITION_TIMEOUT)
179+
val nextActivityButton =
180+
device.findObject(By.text("NEXT ACTIVITY").clazz("android.widget.Button"))
181+
nextActivityButton?.click()
182+
device.wait(Until.hasObject(By.pkg(TEST_APP_PACKAGE).depth(0)), TRANSITION_TIMEOUT)
183+
}
184+
185+
private fun getButton(text: String): UiObject2 {
186+
device.wait(Until.hasObject(By.text(text).depth(0)), TRANSITION_TIMEOUT)
187+
val button = device.findObject(By.text(text).clazz("android.widget.Button"))
188+
if (button == null) {
189+
fail("Could not locate button with text $text")
190+
}
191+
return button
192+
}
193+
194+
private fun dismissPossibleAnrDialog() {
195+
device.wait(
196+
Until.hasObject(By.clazz("com.android.server.am.AppNotRespondingDialog")),
197+
TRANSITION_TIMEOUT
198+
)
199+
device.findObject(By.text("Close app").clazz("android.widget.Button"))?.click()
200+
}
201+
202+
private fun dismissPossibleErrorDialog() {
203+
device.wait(
204+
Until.hasObject(By.clazz("com.android.server.am.AppErrorDialog")),
205+
TRANSITION_TIMEOUT
206+
)
207+
device.findObject(By.text("Close app").clazz("android.widget.Button"))?.click()
208+
}
209+
210+
private fun background() {
211+
device.pressHome()
212+
device.wait(Until.hasObject(By.pkg(device.launcherPackageName).depth(0)), TRANSITION_TIMEOUT)
213+
}
214+
215+
private fun foreground() {
216+
device.pressRecentApps()
217+
Thread.sleep(1_000L)
218+
device.click(device.displayWidth / 2, device.displayHeight / 2)
219+
device.wait(Until.hasObject(By.pkg(TEST_APP_PACKAGE).depth(0)), TRANSITION_TIMEOUT)
220+
device.waitForIdle()
221+
}
222+
223+
private fun getCurrentSessionId(): String? {
224+
device.wait(
225+
Until.hasObject(By.res(Pattern.compile(".*session_id_(fragment|second)_text")).depth(0)),
226+
TRANSITION_TIMEOUT
227+
)
228+
return device.findObject(By.res(Pattern.compile(".*session_id_(fragment|second)_text")))?.text
229+
}
230+
231+
companion object {
232+
private const val TEST_APP_PACKAGE = "com.google.firebase.testing.sessions"
233+
private const val LAUNCH_TIMEOUT = 5_000L
234+
private const val TRANSITION_TIMEOUT = 1_000L
235+
private const val TIME_TO_PROPAGATE_SESSION = 5_000L
236+
}
237+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?xml version="1.0" encoding="utf-8"?><!--
2+
~ Copyright 2023 Google LLC
3+
~
4+
~ Licensed under the Apache License, Version 2.0 (the "License");
5+
~ you may not use this file except in compliance with the License.
6+
~ You may obtain a copy of the License at
7+
~
8+
~ http://www.apache.org/licenses/LICENSE-2.0
9+
~
10+
~ Unless required by applicable law or agreed to in writing, software
11+
~ distributed under the License is distributed on an "AS IS" BASIS,
12+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
~ See the License for the specific language governing permissions and
14+
~ limitations under the License.
15+
-->
16+
17+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
18+
xmlns:tools="http://schemas.android.com/tools">
19+
20+
<application
21+
android:icon="@mipmap/ic_launcher"
22+
android:label="@string/app_name"
23+
android:name=".TestApplication"
24+
android:supportsRtl="true"
25+
android:theme="@style/Theme.Widget_test_app"
26+
tools:targetApi="31">
27+
<activity
28+
android:exported="true"
29+
android:label="@string/app_name"
30+
android:name=".MainActivity"
31+
android:process=":main"
32+
android:theme="@style/Theme.Widget_test_app.NoActionBar">
33+
<intent-filter>
34+
<action android:name="android.intent.action.MAIN" />
35+
<category android:name="android.intent.category.LAUNCHER" />
36+
</intent-filter>
37+
</activity>
38+
<!-- Override the background timeout for the test app to be 5s instead of 30m -->
39+
<activity
40+
android:exported="true"
41+
android:label="@string/app_name"
42+
android:name=".SecondActivity"
43+
android:process=":second"
44+
android:theme="@style/Theme.Widget_test_app.NoActionBar" />
45+
46+
<meta-data
47+
android:name="firebase_performance_logcat_enabled"
48+
android:value="true" />
49+
50+
<meta-data
51+
android:name="firebase_sessions_sessions_restart_timeout"
52+
android:value="5" />
53+
54+
<receiver
55+
android:exported="false"
56+
android:name="CrashWidgetProvider"
57+
android:process=":widgetProcess">
58+
<intent-filter>
59+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
60+
</intent-filter>
61+
<meta-data
62+
android:name="android.appwidget.provider"
63+
android:resource="@xml/homescreen_widget" />
64+
</receiver>
65+
66+
<receiver android:name=".CrashBroadcastReceiver" />
67+
68+
<service
69+
android:enabled="true"
70+
android:exported="false"
71+
android:foregroundServiceType="shortService"
72+
android:name=".ForegroundService"
73+
android:process=":foregroundServiceProcess" />
74+
75+
</application>
76+
77+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
78+
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
79+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
80+
</manifest>

0 commit comments

Comments
 (0)