diff --git a/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatControllerTest.java b/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatControllerTest.java index 63a61a749ad..a39be1fe512 100644 --- a/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatControllerTest.java +++ b/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/DefaultHeartBeatControllerTest.java @@ -15,6 +15,7 @@ package com.google.firebase.heartbeatinfo; import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.heartbeatinfo.TaskWaiter.await; import static java.nio.charset.StandardCharsets.UTF_8; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; @@ -26,7 +27,7 @@ import android.content.Context; import android.content.SharedPreferences; import androidx.test.core.app.ApplicationProvider; -import androidx.test.runner.AndroidJUnit4; +import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.firebase.platforminfo.UserAgentPublisher; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -35,13 +36,10 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeoutException; import java.util.zip.GZIPOutputStream; -import org.json.JSONException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -49,13 +47,12 @@ @RunWith(AndroidJUnit4.class) public class DefaultHeartBeatControllerTest { - private ExecutorService executor; - private TestOnCompleteListener storeOnCompleteListener; - private TestOnCompleteListener getOnCompleteListener; - private final String DEFAULT_USER_AGENT = "agent1"; - private HeartBeatInfoStorage storage = mock(HeartBeatInfoStorage.class); - private UserAgentPublisher publisher = mock(UserAgentPublisher.class); - private static Context applicationContext = ApplicationProvider.getApplicationContext(); + private static final String DEFAULT_USER_AGENT = "agent1"; + private final Executor executor = Executors.newSingleThreadExecutor(); + + private final HeartBeatInfoStorage storage = mock(HeartBeatInfoStorage.class); + private final UserAgentPublisher publisher = mock(UserAgentPublisher.class); + private final Context applicationContext = ApplicationProvider.getApplicationContext(); private final Set logSources = new HashSet() { { @@ -66,22 +63,18 @@ public class DefaultHeartBeatControllerTest { @Before public void setUp() { - executor = new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); when(publisher.getUserAgent()).thenReturn(DEFAULT_USER_AGENT); - storeOnCompleteListener = new TestOnCompleteListener<>(); - getOnCompleteListener = new TestOnCompleteListener<>(); heartBeatController = new DefaultHeartBeatController( () -> storage, logSources, executor, () -> publisher, applicationContext); } @Test - public void whenNoSource_dontStoreHeartBeat() throws ExecutionException, InterruptedException { + public void whenNoSource_dontStoreHeartBeat() throws InterruptedException, TimeoutException { DefaultHeartBeatController controller = new DefaultHeartBeatController( () -> storage, new HashSet<>(), executor, () -> publisher, applicationContext); - controller.registerHeartBeat().addOnCompleteListener(executor, storeOnCompleteListener); - storeOnCompleteListener.await(); + await(controller.registerHeartBeat()); verify(storage, times(0)).storeHeartBeat(anyLong(), anyString()); } @@ -99,31 +92,24 @@ public void getHeartBeatCode_noHeartBeat() { @Config(sdk = 29) @Test - public void generateHeartBeat_oneHeartBeat() - throws ExecutionException, InterruptedException, JSONException, IOException { + public void generateHeartBeat_oneHeartBeat() throws InterruptedException, TimeoutException { ArrayList returnResults = new ArrayList<>(); returnResults.add( - HeartBeatResult.create( - "test-agent", new ArrayList(Collections.singleton("2015-02-03")))); + HeartBeatResult.create("test-agent", Collections.singletonList("2015-02-03"))); when(storage.getAllHeartBeats()).thenReturn(returnResults); - heartBeatController - .registerHeartBeat() - .addOnCompleteListener(executor, storeOnCompleteListener); - storeOnCompleteListener.await(); + await(heartBeatController.registerHeartBeat()); verify(storage, times(1)).storeHeartBeat(anyLong(), anyString()); - heartBeatController - .getHeartBeatsHeader() - .addOnCompleteListener(executor, getOnCompleteListener); String str = "{\"heartbeats\":[{\"agent\":\"test-agent\",\"dates\":[\"2015-02-03\"]}],\"version\":\"2\"}"; String expected = compress(str); - assertThat(getOnCompleteListener.await().replace("\n", "")).isEqualTo(expected); + assertThat(await(heartBeatController.getHeartBeatsHeader()).replace("\n", "")) + .isEqualTo(expected); } @Config(sdk = 29) @Test public void firstNewThenOld_synchronizedCorrectly() - throws ExecutionException, InterruptedException { + throws InterruptedException, TimeoutException { Context context = ApplicationProvider.getApplicationContext(); SharedPreferences heartBeatSharedPreferences = context.getSharedPreferences("testHeartBeat", Context.MODE_PRIVATE); @@ -136,10 +122,8 @@ public void firstNewThenOld_synchronizedCorrectly() Base64.getUrlEncoder() .withoutPadding() .encodeToString("{\"heartbeats\":[],\"version\":\"2\"}".getBytes()); - controller.registerHeartBeat().addOnCompleteListener(executor, storeOnCompleteListener); - storeOnCompleteListener.await(); - controller.getHeartBeatsHeader().addOnCompleteListener(executor, getOnCompleteListener); - String output = getOnCompleteListener.await(); + await(controller.registerHeartBeat()); + String output = await(controller.getHeartBeatsHeader()); assertThat(output.replace("\n", "")).isNotEqualTo(emptyString); int heartBeatCode = controller.getHeartBeatCode("test").getCode(); assertThat(heartBeatCode).isEqualTo(0); @@ -148,7 +132,7 @@ public void firstNewThenOld_synchronizedCorrectly() @Config(sdk = 29) @Test public void firstOldThenNew_synchronizedCorrectly() - throws ExecutionException, InterruptedException, IOException { + throws InterruptedException, TimeoutException { Context context = ApplicationProvider.getApplicationContext(); SharedPreferences heartBeatSharedPreferences = context.getSharedPreferences("testHeartBeat", Context.MODE_PRIVATE); @@ -158,46 +142,36 @@ public void firstOldThenNew_synchronizedCorrectly() new DefaultHeartBeatController( () -> heartBeatInfoStorage, logSources, executor, () -> publisher, context); String emptyString = compress("{\"heartbeats\":[],\"version\":\"2\"}"); - controller.registerHeartBeat().addOnCompleteListener(executor, storeOnCompleteListener); - storeOnCompleteListener.await(); + await(controller.registerHeartBeat()); int heartBeatCode = controller.getHeartBeatCode("test").getCode(); assertThat(heartBeatCode).isEqualTo(2); - controller.getHeartBeatsHeader().addOnCompleteListener(executor, getOnCompleteListener); - String output = getOnCompleteListener.await(); + String output = await(controller.getHeartBeatsHeader()); assertThat(output.replace("\n", "")).isEqualTo(emptyString); - controller.registerHeartBeat().addOnCompleteListener(executor, storeOnCompleteListener); - storeOnCompleteListener.await(); - controller.getHeartBeatsHeader().addOnCompleteListener(executor, getOnCompleteListener); - output = getOnCompleteListener.await(); + + await(controller.registerHeartBeat()); + await(controller.getHeartBeatsHeader()); assertThat(output.replace("\n", "")).isEqualTo(emptyString); } @Config(sdk = 29) @Test public void generateHeartBeat_twoHeartBeatsSameUserAgent() - throws ExecutionException, InterruptedException, JSONException, IOException { + throws InterruptedException, TimeoutException { ArrayList returnResults = new ArrayList<>(); ArrayList dateList = new ArrayList<>(); dateList.add("2015-03-02"); dateList.add("2015-03-01"); returnResults.add(HeartBeatResult.create("test-agent", dateList)); when(storage.getAllHeartBeats()).thenReturn(returnResults); - heartBeatController - .registerHeartBeat() - .addOnCompleteListener(executor, storeOnCompleteListener); - storeOnCompleteListener.await(); - heartBeatController - .registerHeartBeat() - .addOnCompleteListener(executor, storeOnCompleteListener); - storeOnCompleteListener.await(); + await(heartBeatController.registerHeartBeat()); + await(heartBeatController.registerHeartBeat()); verify(storage, times(2)).storeHeartBeat(anyLong(), anyString()); - heartBeatController - .getHeartBeatsHeader() - .addOnCompleteListener(executor, getOnCompleteListener); + String str = "{\"heartbeats\":[{\"agent\":\"test-agent\",\"dates\":[\"2015-03-02\",\"2015-03-01\"]}],\"version\":\"2\"}"; String expected = compress(str); - assertThat(getOnCompleteListener.await().replace("\n", "")).isEqualTo(expected); + assertThat(await(heartBeatController.getHeartBeatsHeader()).replace("\n", "")) + .isEqualTo(expected); } private static String base64Encode(byte[] input) { @@ -218,38 +192,28 @@ private static byte[] gzip(String input) { } } - private String compress(String str) throws IOException { + private String compress(String str) { return base64Encode(gzip(str)); } @Config(sdk = 29) @Test public void generateHeartBeat_twoHeartBeatstwoUserAgents() - throws ExecutionException, InterruptedException, JSONException, IOException { + throws InterruptedException, TimeoutException { ArrayList returnResults = new ArrayList<>(); returnResults.add( - HeartBeatResult.create( - "test-agent", new ArrayList(Collections.singleton("2015-03-02")))); + HeartBeatResult.create("test-agent", Collections.singletonList("2015-03-02"))); returnResults.add( - HeartBeatResult.create( - "test-agent-1", new ArrayList(Collections.singleton("2015-03-03")))); + HeartBeatResult.create("test-agent-1", Collections.singletonList("2015-03-03"))); when(storage.getAllHeartBeats()).thenReturn(returnResults); - heartBeatController - .registerHeartBeat() - .addOnCompleteListener(executor, storeOnCompleteListener); - storeOnCompleteListener.await(); - heartBeatController - .registerHeartBeat() - .addOnCompleteListener(executor, storeOnCompleteListener); - storeOnCompleteListener.await(); - Thread.sleep(1000); + await(heartBeatController.registerHeartBeat()); + await(heartBeatController.registerHeartBeat()); + verify(storage, times(2)).storeHeartBeat(anyLong(), anyString()); - heartBeatController - .getHeartBeatsHeader() - .addOnCompleteListener(executor, getOnCompleteListener); String str = "{\"heartbeats\":[{\"agent\":\"test-agent\",\"dates\":[\"2015-03-02\"]},{\"agent\":\"test-agent-1\",\"dates\":[\"2015-03-03\"]}],\"version\":\"2\"}"; String expected = compress(str); - assertThat(getOnCompleteListener.await().replace("\n", "")).isEqualTo(expected); + assertThat(await(heartBeatController.getHeartBeatsHeader()).replace("\n", "")) + .isEqualTo(expected); } } diff --git a/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/TaskWaiter.java b/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/TaskWaiter.java new file mode 100644 index 00000000000..3de11613829 --- /dev/null +++ b/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/TaskWaiter.java @@ -0,0 +1,56 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.heartbeatinfo; + +import androidx.annotation.NonNull; +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import com.google.errorprone.annotations.CanIgnoreReturnValue; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Helper listener that works around a limitation of the Tasks API where await() cannot be called on + * the main thread. + */ +public class TaskWaiter implements OnCompleteListener { + private static final long TIMEOUT_MS = 500000; + private final CountDownLatch latch = new CountDownLatch(1); + private final Task task; + + private TaskWaiter(Task task) { + this.task = task; + task.addOnCompleteListener(Runnable::run, this); + } + + @Override + public void onComplete(@NonNull Task task) { + latch.countDown(); + } + + public TResult await() throws InterruptedException, TimeoutException { + if (!task.isComplete() && !latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) { + throw new TimeoutException("timed out waiting for result"); + } + return task.getResult(); + } + + @CanIgnoreReturnValue + public static TResult await(Task task) + throws InterruptedException, TimeoutException { + return new TaskWaiter<>(task).await(); + } +} diff --git a/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/TestOnCompleteListener.java b/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/TestOnCompleteListener.java deleted file mode 100644 index 1f6d9078621..00000000000 --- a/firebase-common/src/test/java/com/google/firebase/heartbeatinfo/TestOnCompleteListener.java +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright 2020 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.google.firebase.heartbeatinfo; - -import androidx.annotation.NonNull; -import com.google.android.gms.tasks.OnCompleteListener; -import com.google.android.gms.tasks.Task; -import java.io.IOException; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; - -/** - * Helper listener that works around a limitation of the Tasks API where await() cannot be called on - * the main thread. This listener works around it by running itself on a different thread, thus - * allowing the main thread to be woken up when the Tasks complete. - */ -public class TestOnCompleteListener implements OnCompleteListener { - private static final long TIMEOUT_MS = 5000; - private final CountDownLatch latch = new CountDownLatch(1); - private Task task; - private volatile TResult result; - private volatile Exception exception; - private volatile boolean successful; - - @Override - public void onComplete(@NonNull Task task) { - this.task = task; - successful = task.isSuccessful(); - if (successful) { - result = task.getResult(); - } else { - exception = task.getException(); - } - latch.countDown(); - } - - /** Blocks until the {@link #onComplete} is called. */ - public TResult await() throws InterruptedException, ExecutionException { - if (!latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS)) { - throw new InterruptedException("timed out waiting for result"); - } - if (successful) { - return result; - } else { - if (exception instanceof InterruptedException) { - throw (InterruptedException) exception; - } - if (exception instanceof IOException) { - throw new ExecutionException(exception); - } - throw new IllegalStateException("got an unexpected exception type", exception); - } - } -}