Skip to content

Commit bd78546

Browse files
committed
Generalize the CrashlyticsWorker
1 parent 1b49171 commit bd78546

File tree

4 files changed

+470
-0
lines changed

4 files changed

+470
-0
lines changed

firebase-crashlytics/firebase-crashlytics.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,6 @@ dependencies {
111111
androidTestImplementation(libs.androidx.test.junit)
112112
androidTestImplementation(libs.androidx.test.runner)
113113
androidTestImplementation(libs.truth)
114+
androidTestImplementation(libs.playservices.tasks)
115+
androidTestImplementation(project(":integ-testing"))
114116
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
/*
2+
* Copyright 2024 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.crashlytics.internal;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
import static org.junit.Assert.assertThrows;
21+
22+
import com.google.android.gms.tasks.Task;
23+
import com.google.android.gms.tasks.Tasks;
24+
import com.google.firebase.concurrent.TestOnlyExecutors;
25+
import java.util.ArrayList;
26+
import java.util.HashSet;
27+
import java.util.List;
28+
import java.util.Set;
29+
import java.util.concurrent.CancellationException;
30+
import java.util.concurrent.ExecutionException;
31+
import java.util.concurrent.Executors;
32+
import java.util.concurrent.ThreadPoolExecutor;
33+
import java.util.concurrent.TimeUnit;
34+
import java.util.concurrent.TimeoutException;
35+
import java.util.concurrent.atomic.AtomicBoolean;
36+
import org.junit.After;
37+
import org.junit.Before;
38+
import org.junit.Test;
39+
40+
public class CrashlyticsWorkerTest {
41+
private CrashlyticsWorker crashlyticsWorker;
42+
43+
@Before
44+
public void setUp() {
45+
crashlyticsWorker = new CrashlyticsWorker(TestOnlyExecutors.background());
46+
}
47+
48+
@After
49+
public void tearDown() throws Exception {
50+
// Drain the worker, just in case any test cases would fail but didn't await.
51+
crashlyticsWorker.await();
52+
}
53+
54+
@Test
55+
public void executesTasksOnThreadPool() throws Exception {
56+
Set<String> threads = new HashSet<>();
57+
58+
// Find thread names by adding the names we touch to the set.
59+
for (int i = 0; i < 100; i++) {
60+
crashlyticsWorker.submit(() -> threads.add(Thread.currentThread().getName()));
61+
}
62+
63+
crashlyticsWorker.await();
64+
65+
// Verify that we touched at lease some of the expected background threads.
66+
assertThat(threads)
67+
.containsAnyOf(
68+
"Firebase Background Thread #0",
69+
"Firebase Background Thread #1",
70+
"Firebase Background Thread #2",
71+
"Firebase Background Thread #3");
72+
}
73+
74+
@Test
75+
public void executesTasksInOrder() throws Exception {
76+
List<Integer> list = new ArrayList<>();
77+
78+
// Add sequential numbers to the list to validate tasks execute in order.
79+
for (int i = 0; i < 100; i++) {
80+
int sequential = i;
81+
crashlyticsWorker.submit(() -> list.add(sequential));
82+
}
83+
84+
crashlyticsWorker.await();
85+
86+
// Verify that the tasks executed in order.
87+
assertThat(list).isInOrder();
88+
}
89+
90+
@Test
91+
public void executesTasksSequentially() throws Exception {
92+
List<Integer> list = new ArrayList<>();
93+
AtomicBoolean reentrant = new AtomicBoolean(false);
94+
95+
for (int i = 0; i < 100; i++) {
96+
int sequential = i;
97+
crashlyticsWorker.submit(
98+
() -> {
99+
if (reentrant.get()) {
100+
// Return early if two runnables ran at the same time.
101+
return;
102+
}
103+
104+
reentrant.set(true);
105+
// Sleep a bit to simulate some work.
106+
sleep(5);
107+
list.add(sequential);
108+
reentrant.set(false);
109+
});
110+
}
111+
112+
crashlyticsWorker.await();
113+
114+
// Verify that all the runnable tasks executed, one at a time, and in order.
115+
assertThat(list).hasSize(100);
116+
assertThat(list).isInOrder();
117+
}
118+
119+
@Test
120+
public void submitCallableThatReturns() throws Exception {
121+
String ender = "Remember, the enemy's gate is down.";
122+
Task<String> task = crashlyticsWorker.submit(() -> ender);
123+
124+
String result = Tasks.await(task);
125+
126+
assertThat(result).isEqualTo(ender);
127+
}
128+
129+
@Test
130+
public void submitCallableThatReturnsNull() throws Exception {
131+
Task<String> task = crashlyticsWorker.submit(() -> null);
132+
133+
String result = Tasks.await(task);
134+
135+
assertThat(result).isNull();
136+
}
137+
138+
@Test
139+
public void submitCallableThatThrows() {
140+
Task<Void> task =
141+
crashlyticsWorker.submit(
142+
() -> {
143+
throw new Exception("I threw in the callable");
144+
});
145+
146+
ExecutionException thrown = assertThrows(ExecutionException.class, () -> Tasks.await(task));
147+
148+
assertThat(thrown).hasCauseThat().hasMessageThat().isEqualTo("I threw in the callable");
149+
}
150+
151+
@Test
152+
public void submitRunnable() throws Exception {
153+
Task<Void> task = crashlyticsWorker.submit(() -> {});
154+
155+
Void result = Tasks.await(task);
156+
157+
// A Runnable does not return, so the task evaluates to null.
158+
assertThat(result).isNull();
159+
}
160+
161+
@Test
162+
public void submitRunnableThatThrows() {
163+
Task<Void> task =
164+
crashlyticsWorker.submit(
165+
(Runnable)
166+
() -> {
167+
throw new RuntimeException("I threw in the runnable");
168+
});
169+
170+
ExecutionException thrown = assertThrows(ExecutionException.class, () -> Tasks.await(task));
171+
172+
assertThat(thrown).hasCauseThat().hasMessageThat().isEqualTo("I threw in the runnable");
173+
}
174+
175+
@Test
176+
public void submitTaskThatReturns() throws Exception {
177+
String skippy = "Think of the problem as an enemy, and defeat them in detail.";
178+
Task<String> task = crashlyticsWorker.submitTask(() -> Tasks.forResult(skippy));
179+
180+
String result = Tasks.await(task);
181+
182+
assertThat(result).isEqualTo(skippy);
183+
}
184+
185+
@Test
186+
public void submitTaskThatReturnsNull() throws Exception {
187+
Task<String> task = crashlyticsWorker.submitTask(() -> Tasks.forResult(null));
188+
189+
String result = Tasks.await(task);
190+
191+
assertThat(result).isNull();
192+
}
193+
194+
@Test
195+
public void submitTaskThatThrows() {
196+
Task<String> task =
197+
crashlyticsWorker.submitTask(
198+
() -> Tasks.forException(new Exception("Thrown from a task.")));
199+
200+
ExecutionException thrown = assertThrows(ExecutionException.class, () -> Tasks.await(task));
201+
202+
assertThat(thrown).hasCauseThat().hasMessageThat().isEqualTo("Thrown from a task.");
203+
}
204+
205+
@Test
206+
public void submitTaskThatThrowsThenReturns() throws Exception {
207+
crashlyticsWorker.submitTask(() -> Tasks.forException(new IllegalStateException()));
208+
Task<String> task = crashlyticsWorker.submitTask(() -> Tasks.forResult("The Hail Mary"));
209+
210+
String result = Tasks.await(task);
211+
212+
assertThat(result).isEqualTo("The Hail Mary");
213+
}
214+
215+
@Test
216+
public void submitTaskThatCancels() {
217+
Task<Void> task = crashlyticsWorker.submitTask(Tasks::forCanceled);
218+
219+
CancellationException thrown =
220+
assertThrows(CancellationException.class, () -> Tasks.await(task));
221+
222+
assertThat(task.isCanceled()).isTrue();
223+
assertThat(thrown).hasMessageThat().contains("Task is already canceled");
224+
}
225+
226+
@Test
227+
public void submitTaskThatCancelsThenReturns() throws Exception {
228+
crashlyticsWorker.submitTask(Tasks::forCanceled);
229+
Task<String> task = crashlyticsWorker.submitTask(() -> Tasks.forResult("Flying Dutchman"));
230+
231+
String result = Tasks.await(task);
232+
233+
assertThat(task.isCanceled()).isFalse();
234+
assertThat(result).isEqualTo("Flying Dutchman");
235+
}
236+
237+
@Test
238+
public void submitTaskThatCancelsThenAwaitsThenReturns() throws Exception {
239+
Task<?> cancelled = crashlyticsWorker.submitTask(Tasks::forCanceled);
240+
241+
// Await on the cancelled task to force the exception to propagate.
242+
assertThrows(CancellationException.class, () -> Tasks.await(cancelled));
243+
244+
// Submit another task.
245+
Task<String> task = crashlyticsWorker.submitTask(() -> Tasks.forResult("Valkyrie"));
246+
247+
String result = Tasks.await(task);
248+
249+
assertThat(cancelled.isCanceled()).isTrue();
250+
assertThat(task.isCanceled()).isFalse();
251+
assertThat(result).isEqualTo("Valkyrie");
252+
}
253+
254+
@Test
255+
public void submitTaskThatCancelsThenAwaitsThenRunnable() throws Exception {
256+
Task<?> cancelled = crashlyticsWorker.submitTask(Tasks::forCanceled);
257+
258+
// Await on the cancelled task to force the exception to propagate.
259+
assertThrows(CancellationException.class, () -> Tasks.await(cancelled));
260+
261+
// Submit an empty runnable.
262+
Task<Void> task = crashlyticsWorker.submit(() -> {});
263+
264+
Void result = Tasks.await(task);
265+
266+
assertThat(cancelled.isCanceled()).isTrue();
267+
assertThat(task.isCanceled()).isFalse();
268+
assertThat(result).isNull();
269+
}
270+
271+
@Test
272+
public void submitTaskFromAnotherWorker() throws Exception {
273+
Task<String> otherTask =
274+
new CrashlyticsWorker(TestOnlyExecutors.blocking())
275+
.submit(() -> "Dog's fine. Just sleeping.");
276+
277+
// This will not use a background thread while waiting for the task on blocking thread.
278+
Task<String> task = crashlyticsWorker.submitTask(() -> otherTask);
279+
280+
String result = Tasks.await(task);
281+
assertThat(result).isEqualTo("Dog's fine. Just sleeping.");
282+
}
283+
284+
@Test
285+
public void submitTaskFromAnotherWorkerDoesNotUseLocalThreads() throws Exception {
286+
// Setup a "local" worker.
287+
ThreadPoolExecutor localExecutor = (ThreadPoolExecutor) Executors.newFixedThreadPool(4);
288+
CrashlyticsWorker localWorker = new CrashlyticsWorker(localExecutor);
289+
290+
// Use a task off crashlyticsWorker to represent an other task.
291+
Task<Integer> otherTask =
292+
crashlyticsWorker.submit(
293+
() -> {
294+
sleep(30);
295+
return localExecutor.getActiveCount();
296+
});
297+
298+
// No active threads yet.
299+
assertThat(localExecutor.getActiveCount()).isEqualTo(0);
300+
301+
// 1 active thread when doing a local task.
302+
assertThat(Tasks.await(localWorker.submit(localExecutor::getActiveCount))).isEqualTo(1);
303+
304+
// 0 active local threads when waiting for other task.
305+
// Waiting for a task from another worker does not block a local thread.
306+
assertThat(Tasks.await(localWorker.submitTask(() -> otherTask))).isEqualTo(0);
307+
308+
// 1 active thread when doing a task.
309+
assertThat(Tasks.await(localWorker.submit(localExecutor::getActiveCount))).isEqualTo(1);
310+
311+
// No active threads after.
312+
assertThat(localExecutor.getActiveCount()).isEqualTo(0);
313+
}
314+
315+
@Test
316+
public void submitTaskWhenThreadPoolFull() {
317+
// Fill the backing executor thread pool.
318+
for (int i = 0; i < 10; i++) {
319+
crashlyticsWorker.getExecutor().execute(() -> sleep(1_000));
320+
}
321+
322+
Task<Integer> task = crashlyticsWorker.submitTask(() -> Tasks.forResult(42));
323+
324+
assertThrows(TimeoutException.class, () -> Tasks.await(task, 300, TimeUnit.MILLISECONDS));
325+
}
326+
327+
private static void sleep(long millis) {
328+
try {
329+
Thread.sleep(millis);
330+
} catch (InterruptedException ex) {
331+
Thread.currentThread().interrupt();
332+
}
333+
}
334+
}

0 commit comments

Comments
 (0)