Add PollingCheck utility to host-side test utils am: 1a981d20fd
am: c8e7562d75

Change-Id: I28b5a29b8b9dc1584ad07fb8699410a207ddf2ba
diff --git a/common/host-side/util/src/com/android/compatibility/common/util/PollingCheck.java b/common/host-side/util/src/com/android/compatibility/common/util/PollingCheck.java
new file mode 100644
index 0000000..aa0d22d
--- /dev/null
+++ b/common/host-side/util/src/com/android/compatibility/common/util/PollingCheck.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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.android.compatibility.common.util;
+
+import static org.junit.Assert.fail;
+
+import java.util.concurrent.Callable;
+
+/** Test utility to check for or wait for a condition by polling. */
+public abstract class PollingCheck {
+    public static final PollingCheckClock DEFAULT_CLOCK = new SystemClock();
+    private static final long TIME_SLICE = 50;
+
+    /** Clock for polling. */
+    public interface PollingCheckClock {
+        long currentTimeMillis();
+
+        default void sleep(long millis) throws InterruptedException {
+            Thread.sleep(millis);
+        }
+    }
+
+    private static class SystemClock implements PollingCheckClock {
+        @Override
+        public long currentTimeMillis() {
+            return System.currentTimeMillis();
+        }
+    }
+
+    /**
+     * Repeatedly check a condition.
+     *
+     * @param clock Clock used for checking time and sleeping between checks
+     * @param pollInterval Time interval to wait between checks
+     * @param timeout Timeout after which to stop checking
+     * @param condition Condition to check
+     * @return {@code true} if the condition became true within the timeout, {@code false} otherwise
+     * @throws Exception
+     */
+    public static boolean check(
+            PollingCheckClock clock, long pollInterval, long timeout, Callable<Boolean> condition)
+            throws Exception {
+        long start = clock.currentTimeMillis();
+
+        if (condition.call()) {
+            return true;
+        }
+
+        while ((clock.currentTimeMillis() - start) < timeout) {
+            clock.sleep(pollInterval);
+
+            if (condition.call()) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Repeatedly check a condition. Uses the default clock and polling interval.
+     *
+     * @param timeout Timeout after which to stop checking
+     * @param condition Condition to check
+     * @return {@code true} if the condition became true within the timeout, {@code false} otherwise
+     * @throws Exception
+     */
+    public static boolean check(long timeout, Callable<Boolean> condition) throws Exception {
+        return check(DEFAULT_CLOCK, TIME_SLICE, timeout, condition);
+    }
+
+    /**
+     * Repeatedly check a condition. Throws an {@link AssertionError} if the condition does not
+     * become {@code true} within the timeout.
+     *
+     * @param clock Clock used for checking time and sleeping between checks
+     * @param pollInterval Time interval to wait between checks
+     * @param timeout Timeout after which to stop checking
+     * @param condition Condition to check
+     * @throws Exception
+     */
+    public static void check(
+            PollingCheckClock clock,
+            String message,
+            long pollInterval,
+            long timeout,
+            Callable<Boolean> condition)
+            throws Exception {
+        if (!check(clock, pollInterval, timeout, condition)) {
+            fail(message);
+        }
+    }
+
+    /**
+     * Repeatedly check a condition. Uses the default clock and polling interval. Throws an {@link
+     * AssertionError} if the condition does not become {@code true} within the timeout.
+     *
+     * @param timeout Timeout after which to stop checking
+     * @param condition Condition to check
+     * @throws Exception
+     */
+    public static void check(String message, long timeout, Callable<Boolean> condition)
+            throws Exception {
+        check(DEFAULT_CLOCK, message, TIME_SLICE, timeout, condition);
+    }
+
+    /**
+     * Waits for a condition to become {@code true}. Throws an {@link AssertionError} if the
+     * condition does not become {@code true} within the timeout.
+     *
+     * @param clock Clock used for checking time and sleeping between checks
+     * @param pollInterval Time interval to wait between checks
+     * @param timeout Timeout after which to stop checking
+     * @param condition Condition to check
+     * @throws Exception
+     */
+    public static void waitFor(
+            PollingCheckClock clock,
+            long pollInterval,
+            long timeout,
+            final Callable<Boolean> condition)
+            throws Exception {
+        check(clock, "Unexpected timeout waiting for condition", pollInterval, timeout, condition);
+    }
+
+    /**
+     * Waits for a condition to become {@code true}. Uses the default clock and polling interval.
+     * Throws an {@link AssertionError} if the condition does not become {@code true} within the
+     * timeout.
+     *
+     * @param timeout Timeout after which to stop checking
+     * @param condition Condition to check
+     * @throws Exception
+     */
+    public static void waitFor(long timeout, final Callable<Boolean> condition) throws Exception {
+        waitFor(DEFAULT_CLOCK, TIME_SLICE, timeout, condition);
+    }
+}
diff --git a/common/host-side/util/tests/src/com/android/compatibility/common/util/HostUnitTests.java b/common/host-side/util/tests/src/com/android/compatibility/common/util/HostUnitTests.java
index 32408e4..2fb0868 100644
--- a/common/host-side/util/tests/src/com/android/compatibility/common/util/HostUnitTests.java
+++ b/common/host-side/util/tests/src/com/android/compatibility/common/util/HostUnitTests.java
@@ -20,8 +20,8 @@
 import org.junit.runners.Suite.SuiteClasses;
 /**
  * A test suite for all host util unit tests.
- * <p/>
- * All tests listed here should be self-contained, and do not require any external dependencies.
+ *
+ * <p>All tests listed here should be self-contained, and do not require any external dependencies.
  */
 @RunWith(Suite.class)
 @SuiteClasses({
@@ -29,6 +29,7 @@
     DynamicConfigHandlerTest.class,
     ModuleResultTest.class,
     TestFilterTest.class,
+    PollingCheckTest.class,
 })
 public class HostUnitTests {
     // empty on purpose
diff --git a/common/host-side/util/tests/src/com/android/compatibility/common/util/PollingCheckTest.java b/common/host-side/util/tests/src/com/android/compatibility/common/util/PollingCheckTest.java
new file mode 100644
index 0000000..cafd7ab
--- /dev/null
+++ b/common/host-side/util/tests/src/com/android/compatibility/common/util/PollingCheckTest.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * 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.android.compatibility.common.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/** Tests for {@link PollingCheck}. */
+@RunWith(JUnit4.class)
+public class PollingCheckTest {
+    private PollingCheck.PollingCheckClock testClock;
+
+    @Before
+    public void setUp() {
+        testClock = new TestClock();
+    }
+
+    @Test
+    public void testCheckSuccess() throws Exception {
+        assertTrue("check failed", PollingCheck.check(testClock, 10, 100, () -> true));
+    }
+
+    @Test
+    public void testCheckEventualSuccess() throws Exception {
+        final int[] i = {0};
+        assertTrue("check failed", PollingCheck.check(testClock, 10, 1000, () -> ++i[0] == 3));
+        assertEquals("Condition expected to be checked three times", 3, i[0]);
+        assertEquals("Time advanced unexpectedly", 20, testClock.currentTimeMillis());
+    }
+
+    @Test
+    public void testCheckTimeout() throws Exception {
+        final int[] i = {0};
+        assertFalse(
+                "Expected failure due to timeout",
+                PollingCheck.check(testClock, 10, 50, () -> i[0]++ == 9));
+    }
+
+    @Test
+    public void testCheckFailure() throws Exception {
+        assertFalse(
+                "Expected failure due to condition not being true",
+                PollingCheck.check(testClock, 10, 100, () -> false));
+    }
+
+    @Test
+    public void testCheckChecksConditionAtLeastOnce() throws Exception {
+        final boolean[] conditionChecked = {false};
+        PollingCheck.check(
+                testClock,
+                10,
+                0,
+                () -> {
+                    conditionChecked[0] = true;
+                    return true;
+                });
+
+        assertTrue("Expected condition to be checked", conditionChecked[0]);
+    }
+
+    @Test
+    public void testLongConditionCheck() throws Exception {
+        PollingCheck.check(
+                testClock,
+                10,
+                500,
+                () -> {
+                    testClock.sleep(5); // condition takes some time to evaluate
+                    return testClock.currentTimeMillis() >= 50;
+                });
+
+        assertEquals(50, testClock.currentTimeMillis());
+    }
+
+    @Test
+    public void testCheckMessage() throws Exception {
+        try {
+            PollingCheck.check(testClock, "Expected message", 10, 0, () -> false);
+        } catch (AssertionError e) {
+            assertEquals("Expected message", e.getMessage());
+        }
+    }
+
+    @Test
+    public void testWaitForTimeout() throws Exception {
+        try {
+            PollingCheck.waitFor(testClock, 10, 500, () -> false);
+        } catch (AssertionError e) {
+            assertEquals("Unexpected timeout waiting for condition", e.getMessage());
+        }
+
+        assertEquals(500, testClock.currentTimeMillis());
+    }
+
+    @Test
+    public void testWaitForSuccess() throws Exception {
+        PollingCheck.waitFor(testClock, 10, 500, () -> testClock.currentTimeMillis() >= 200);
+        assertEquals(200, testClock.currentTimeMillis());
+    }
+
+    private final class TestClock implements PollingCheck.PollingCheckClock {
+        private long currentTime = 0;
+
+        @Override
+        public long currentTimeMillis() {
+            return currentTime;
+        }
+
+        @Override
+        public void sleep(long millis) {
+            currentTime += millis;
+        }
+    }
+}