Skip to content

Commit 9ddd157

Browse files
committed
Deflake firebase_common HeartBeat tests.
The tests relied on `TestOnCompleteListener` that was not safe to call more than once since it was based on a count down latch. So reusing it multiple times would cause await() to return immediately. This change makes it so that a new latch is created for every await() call, making all await() calls work. Fixes: http://b/245956774
1 parent e6226b7 commit 9ddd157

File tree

4 files changed

+99
-145
lines changed

4 files changed

+99
-145
lines changed

firebase-common/src/main/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatController.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public Task<Void> registerHeartBeat() {
6767
return Tasks.call(
6868
backgroundExecutor,
6969
() -> {
70+
System.out.println();
7071
synchronized (DefaultHeartBeatController.this) {
7172
this.storageProvider
7273
.get()

firebase-common/src/test/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatControllerTest.java

Lines changed: 42 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package com.google.firebase.heartbeatinfo;
1616

1717
import static com.google.common.truth.Truth.assertThat;
18+
import static com.google.firebase.heartbeatinfo.TaskWaiter.await;
1819
import static java.nio.charset.StandardCharsets.UTF_8;
1920
import static org.mockito.ArgumentMatchers.anyLong;
2021
import static org.mockito.ArgumentMatchers.anyString;
@@ -26,7 +27,7 @@
2627
import android.content.Context;
2728
import android.content.SharedPreferences;
2829
import androidx.test.core.app.ApplicationProvider;
29-
import androidx.test.runner.AndroidJUnit4;
30+
import androidx.test.ext.junit.runners.AndroidJUnit4;
3031
import com.google.firebase.platforminfo.UserAgentPublisher;
3132
import java.io.ByteArrayOutputStream;
3233
import java.io.IOException;
@@ -35,27 +36,23 @@
3536
import java.util.Collections;
3637
import java.util.HashSet;
3738
import java.util.Set;
38-
import java.util.concurrent.ExecutionException;
39-
import java.util.concurrent.ExecutorService;
40-
import java.util.concurrent.LinkedBlockingQueue;
41-
import java.util.concurrent.ThreadPoolExecutor;
42-
import java.util.concurrent.TimeUnit;
39+
import java.util.concurrent.Executor;
40+
import java.util.concurrent.Executors;
41+
import java.util.concurrent.TimeoutException;
4342
import java.util.zip.GZIPOutputStream;
44-
import org.json.JSONException;
4543
import org.junit.Before;
4644
import org.junit.Test;
4745
import org.junit.runner.RunWith;
4846
import org.robolectric.annotation.Config;
4947

5048
@RunWith(AndroidJUnit4.class)
5149
public class DefaultHeartBeatControllerTest {
52-
private ExecutorService executor;
53-
private TestOnCompleteListener<Void> storeOnCompleteListener;
54-
private TestOnCompleteListener<String> getOnCompleteListener;
55-
private final String DEFAULT_USER_AGENT = "agent1";
56-
private HeartBeatInfoStorage storage = mock(HeartBeatInfoStorage.class);
57-
private UserAgentPublisher publisher = mock(UserAgentPublisher.class);
58-
private static Context applicationContext = ApplicationProvider.getApplicationContext();
50+
private static final String DEFAULT_USER_AGENT = "agent1";
51+
private final Executor executor = Executors.newSingleThreadExecutor();
52+
53+
private final HeartBeatInfoStorage storage = mock(HeartBeatInfoStorage.class);
54+
private final UserAgentPublisher publisher = mock(UserAgentPublisher.class);
55+
private final Context applicationContext = ApplicationProvider.getApplicationContext();
5956
private final Set<HeartBeatConsumer> logSources =
6057
new HashSet<HeartBeatConsumer>() {
6158
{
@@ -66,22 +63,18 @@ public class DefaultHeartBeatControllerTest {
6663

6764
@Before
6865
public void setUp() {
69-
executor = new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
7066
when(publisher.getUserAgent()).thenReturn(DEFAULT_USER_AGENT);
71-
storeOnCompleteListener = new TestOnCompleteListener<>();
72-
getOnCompleteListener = new TestOnCompleteListener<>();
7367
heartBeatController =
7468
new DefaultHeartBeatController(
7569
() -> storage, logSources, executor, () -> publisher, applicationContext);
7670
}
7771

7872
@Test
79-
public void whenNoSource_dontStoreHeartBeat() throws ExecutionException, InterruptedException {
73+
public void whenNoSource_dontStoreHeartBeat() throws InterruptedException, TimeoutException {
8074
DefaultHeartBeatController controller =
8175
new DefaultHeartBeatController(
8276
() -> storage, new HashSet<>(), executor, () -> publisher, applicationContext);
83-
controller.registerHeartBeat().addOnCompleteListener(executor, storeOnCompleteListener);
84-
storeOnCompleteListener.await();
77+
await(controller.registerHeartBeat());
8578
verify(storage, times(0)).storeHeartBeat(anyLong(), anyString());
8679
}
8780

@@ -99,31 +92,24 @@ public void getHeartBeatCode_noHeartBeat() {
9992

10093
@Config(sdk = 29)
10194
@Test
102-
public void generateHeartBeat_oneHeartBeat()
103-
throws ExecutionException, InterruptedException, JSONException, IOException {
95+
public void generateHeartBeat_oneHeartBeat() throws InterruptedException, TimeoutException {
10496
ArrayList<HeartBeatResult> returnResults = new ArrayList<>();
10597
returnResults.add(
106-
HeartBeatResult.create(
107-
"test-agent", new ArrayList<String>(Collections.singleton("2015-02-03"))));
98+
HeartBeatResult.create("test-agent", Collections.singletonList("2015-02-03")));
10899
when(storage.getAllHeartBeats()).thenReturn(returnResults);
109-
heartBeatController
110-
.registerHeartBeat()
111-
.addOnCompleteListener(executor, storeOnCompleteListener);
112-
storeOnCompleteListener.await();
100+
await(heartBeatController.registerHeartBeat());
113101
verify(storage, times(1)).storeHeartBeat(anyLong(), anyString());
114-
heartBeatController
115-
.getHeartBeatsHeader()
116-
.addOnCompleteListener(executor, getOnCompleteListener);
117102
String str =
118103
"{\"heartbeats\":[{\"agent\":\"test-agent\",\"dates\":[\"2015-02-03\"]}],\"version\":\"2\"}";
119104
String expected = compress(str);
120-
assertThat(getOnCompleteListener.await().replace("\n", "")).isEqualTo(expected);
105+
assertThat(await(heartBeatController.getHeartBeatsHeader()).replace("\n", ""))
106+
.isEqualTo(expected);
121107
}
122108

123109
@Config(sdk = 29)
124110
@Test
125111
public void firstNewThenOld_synchronizedCorrectly()
126-
throws ExecutionException, InterruptedException {
112+
throws InterruptedException, TimeoutException {
127113
Context context = ApplicationProvider.getApplicationContext();
128114
SharedPreferences heartBeatSharedPreferences =
129115
context.getSharedPreferences("testHeartBeat", Context.MODE_PRIVATE);
@@ -136,10 +122,8 @@ public void firstNewThenOld_synchronizedCorrectly()
136122
Base64.getUrlEncoder()
137123
.withoutPadding()
138124
.encodeToString("{\"heartbeats\":[],\"version\":\"2\"}".getBytes());
139-
controller.registerHeartBeat().addOnCompleteListener(executor, storeOnCompleteListener);
140-
storeOnCompleteListener.await();
141-
controller.getHeartBeatsHeader().addOnCompleteListener(executor, getOnCompleteListener);
142-
String output = getOnCompleteListener.await();
125+
await(controller.registerHeartBeat());
126+
String output = await(controller.getHeartBeatsHeader());
143127
assertThat(output.replace("\n", "")).isNotEqualTo(emptyString);
144128
int heartBeatCode = controller.getHeartBeatCode("test").getCode();
145129
assertThat(heartBeatCode).isEqualTo(0);
@@ -148,7 +132,7 @@ public void firstNewThenOld_synchronizedCorrectly()
148132
@Config(sdk = 29)
149133
@Test
150134
public void firstOldThenNew_synchronizedCorrectly()
151-
throws ExecutionException, InterruptedException, IOException {
135+
throws InterruptedException, TimeoutException {
152136
Context context = ApplicationProvider.getApplicationContext();
153137
SharedPreferences heartBeatSharedPreferences =
154138
context.getSharedPreferences("testHeartBeat", Context.MODE_PRIVATE);
@@ -158,46 +142,36 @@ public void firstOldThenNew_synchronizedCorrectly()
158142
new DefaultHeartBeatController(
159143
() -> heartBeatInfoStorage, logSources, executor, () -> publisher, context);
160144
String emptyString = compress("{\"heartbeats\":[],\"version\":\"2\"}");
161-
controller.registerHeartBeat().addOnCompleteListener(executor, storeOnCompleteListener);
162-
storeOnCompleteListener.await();
145+
await(controller.registerHeartBeat());
163146
int heartBeatCode = controller.getHeartBeatCode("test").getCode();
164147
assertThat(heartBeatCode).isEqualTo(2);
165-
controller.getHeartBeatsHeader().addOnCompleteListener(executor, getOnCompleteListener);
166-
String output = getOnCompleteListener.await();
148+
String output = await(controller.getHeartBeatsHeader());
167149
assertThat(output.replace("\n", "")).isEqualTo(emptyString);
168-
controller.registerHeartBeat().addOnCompleteListener(executor, storeOnCompleteListener);
169-
storeOnCompleteListener.await();
170-
controller.getHeartBeatsHeader().addOnCompleteListener(executor, getOnCompleteListener);
171-
output = getOnCompleteListener.await();
150+
151+
await(controller.registerHeartBeat());
152+
await(controller.getHeartBeatsHeader());
172153
assertThat(output.replace("\n", "")).isEqualTo(emptyString);
173154
}
174155

175156
@Config(sdk = 29)
176157
@Test
177158
public void generateHeartBeat_twoHeartBeatsSameUserAgent()
178-
throws ExecutionException, InterruptedException, JSONException, IOException {
159+
throws InterruptedException, TimeoutException {
179160
ArrayList<HeartBeatResult> returnResults = new ArrayList<>();
180161
ArrayList<String> dateList = new ArrayList<>();
181162
dateList.add("2015-03-02");
182163
dateList.add("2015-03-01");
183164
returnResults.add(HeartBeatResult.create("test-agent", dateList));
184165
when(storage.getAllHeartBeats()).thenReturn(returnResults);
185-
heartBeatController
186-
.registerHeartBeat()
187-
.addOnCompleteListener(executor, storeOnCompleteListener);
188-
storeOnCompleteListener.await();
189-
heartBeatController
190-
.registerHeartBeat()
191-
.addOnCompleteListener(executor, storeOnCompleteListener);
192-
storeOnCompleteListener.await();
166+
await(heartBeatController.registerHeartBeat());
167+
await(heartBeatController.registerHeartBeat());
193168
verify(storage, times(2)).storeHeartBeat(anyLong(), anyString());
194-
heartBeatController
195-
.getHeartBeatsHeader()
196-
.addOnCompleteListener(executor, getOnCompleteListener);
169+
197170
String str =
198171
"{\"heartbeats\":[{\"agent\":\"test-agent\",\"dates\":[\"2015-03-02\",\"2015-03-01\"]}],\"version\":\"2\"}";
199172
String expected = compress(str);
200-
assertThat(getOnCompleteListener.await().replace("\n", "")).isEqualTo(expected);
173+
assertThat(await(heartBeatController.getHeartBeatsHeader()).replace("\n", ""))
174+
.isEqualTo(expected);
201175
}
202176

203177
private static String base64Encode(byte[] input) {
@@ -218,38 +192,28 @@ private static byte[] gzip(String input) {
218192
}
219193
}
220194

221-
private String compress(String str) throws IOException {
195+
private String compress(String str) {
222196
return base64Encode(gzip(str));
223197
}
224198

225199
@Config(sdk = 29)
226200
@Test
227201
public void generateHeartBeat_twoHeartBeatstwoUserAgents()
228-
throws ExecutionException, InterruptedException, JSONException, IOException {
202+
throws InterruptedException, TimeoutException {
229203
ArrayList<HeartBeatResult> returnResults = new ArrayList<>();
230204
returnResults.add(
231-
HeartBeatResult.create(
232-
"test-agent", new ArrayList<String>(Collections.singleton("2015-03-02"))));
205+
HeartBeatResult.create("test-agent", Collections.singletonList("2015-03-02")));
233206
returnResults.add(
234-
HeartBeatResult.create(
235-
"test-agent-1", new ArrayList<String>(Collections.singleton("2015-03-03"))));
207+
HeartBeatResult.create("test-agent-1", Collections.singletonList("2015-03-03")));
236208
when(storage.getAllHeartBeats()).thenReturn(returnResults);
237-
heartBeatController
238-
.registerHeartBeat()
239-
.addOnCompleteListener(executor, storeOnCompleteListener);
240-
storeOnCompleteListener.await();
241-
heartBeatController
242-
.registerHeartBeat()
243-
.addOnCompleteListener(executor, storeOnCompleteListener);
244-
storeOnCompleteListener.await();
245-
Thread.sleep(1000);
209+
await(heartBeatController.registerHeartBeat());
210+
await(heartBeatController.registerHeartBeat());
211+
246212
verify(storage, times(2)).storeHeartBeat(anyLong(), anyString());
247-
heartBeatController
248-
.getHeartBeatsHeader()
249-
.addOnCompleteListener(executor, getOnCompleteListener);
250213
String str =
251214
"{\"heartbeats\":[{\"agent\":\"test-agent\",\"dates\":[\"2015-03-02\"]},{\"agent\":\"test-agent-1\",\"dates\":[\"2015-03-03\"]}],\"version\":\"2\"}";
252215
String expected = compress(str);
253-
assertThat(getOnCompleteListener.await().replace("\n", "")).isEqualTo(expected);
216+
assertThat(await(heartBeatController.getHeartBeatsHeader()).replace("\n", ""))
217+
.isEqualTo(expected);
254218
}
255219
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright 2020 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.heartbeatinfo;
16+
17+
import androidx.annotation.NonNull;
18+
import com.google.android.gms.tasks.OnCompleteListener;
19+
import com.google.android.gms.tasks.Task;
20+
import com.google.errorprone.annotations.CanIgnoreReturnValue;
21+
import java.util.concurrent.CountDownLatch;
22+
import java.util.concurrent.TimeUnit;
23+
import java.util.concurrent.TimeoutException;
24+
25+
/**
26+
* Helper listener that works around a limitation of the Tasks API where await() cannot be called on
27+
* the main thread.
28+
*/
29+
public class TaskWaiter<TResult> implements OnCompleteListener<TResult> {
30+
private static final long TIMEOUT_MS = 500000;
31+
private final CountDownLatch latch = new CountDownLatch(1);
32+
private final Task<TResult> task;
33+
34+
private TaskWaiter(Task<TResult> task) {
35+
this.task = task;
36+
task.addOnCompleteListener(Runnable::run, this);
37+
}
38+
39+
@Override
40+
public void onComplete(@NonNull Task<TResult> task) {
41+
latch.countDown();
42+
}
43+
44+
public TResult await() throws InterruptedException, TimeoutException {
45+
if (!task.isComplete() && !latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
46+
throw new TimeoutException("timed out waiting for result");
47+
}
48+
return task.getResult();
49+
}
50+
51+
@CanIgnoreReturnValue
52+
public static <TResult> TResult await(Task<TResult> task)
53+
throws InterruptedException, TimeoutException {
54+
return new TaskWaiter<>(task).await();
55+
}
56+
}

firebase-common/src/test/java/com/google/firebase/heartbeatinfo/TestOnCompleteListener.java

Lines changed: 0 additions & 67 deletions
This file was deleted.

0 commit comments

Comments
 (0)