Back to Repositories

Testing FakeClock Time Management Implementation in SmartTube

This test suite validates the FakeClock implementation in ExoPlayer, providing comprehensive testing of time manipulation and scheduling functionality. The suite ensures reliable timing control for media playback testing scenarios.

Test Coverage Overview

The test suite provides extensive coverage of FakeClock functionality including time advancement, sleep operations, and delayed post handling.

  • Tests time advancement with precise millisecond control
  • Validates multi-threaded sleep operations
  • Verifies delayed post execution timing
  • Covers concurrent operation scenarios

Implementation Analysis

The testing approach employs JUnit with AndroidJUnit4 runner and Robolectric shadows for Android environment simulation. The implementation uses synchronized blocks for thread safety and CountDownLatch mechanisms for precise timing control.

Key patterns include:
  • Thread synchronization testing
  • Time advancement verification
  • Handler message queue testing

Technical Details

Testing infrastructure includes:

  • JUnit test framework
  • Robolectric for Android runtime simulation
  • Custom shadow classes for Looper and MessageQueue
  • HandlerThread for async operations
  • ConditionVariable for thread coordination

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through comprehensive coverage and robust implementation.

  • Thorough edge case handling
  • Clear test method organization
  • Effective use of helper methods for state verification
  • Proper timeout handling
  • Thread-safe testing approaches

yuliskov/smarttube

exoplayer-amzn-2.10.6/testutils/src/test/java/com/google/android/exoplayer2/testutil/FakeClockTest.java

            
/*
 * Copyright (C) 2017 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.google.android.exoplayer2.testutil;

import static com.google.common.truth.Truth.assertThat;

import android.os.ConditionVariable;
import android.os.HandlerThread;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.exoplayer2.util.Clock;
import com.google.android.exoplayer2.util.HandlerWrapper;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;

/** Unit test for {@link FakeClock}. */
@RunWith(AndroidJUnit4.class)
@Config(shadows = {RobolectricUtil.CustomLooper.class, RobolectricUtil.CustomMessageQueue.class})
public final class FakeClockTest {

  private static final long TIMEOUT_MS = 10000;

  @Test
  public void testAdvanceTime() {
    FakeClock fakeClock = new FakeClock(2000);
    assertThat(fakeClock.elapsedRealtime()).isEqualTo(2000);
    fakeClock.advanceTime(500);
    assertThat(fakeClock.elapsedRealtime()).isEqualTo(2500);
    fakeClock.advanceTime(0);
    assertThat(fakeClock.elapsedRealtime()).isEqualTo(2500);
  }

  @Test
  public void testSleep() throws InterruptedException {
    FakeClock fakeClock = new FakeClock(0);
    SleeperThread sleeperThread = new SleeperThread(fakeClock, 1000);
    sleeperThread.start();
    assertThat(sleeperThread.waitUntilAsleep(TIMEOUT_MS)).isTrue();
    assertThat(sleeperThread.isSleeping()).isTrue();
    fakeClock.advanceTime(1000);
    sleeperThread.join(TIMEOUT_MS);
    assertThat(sleeperThread.isSleeping()).isFalse();

    sleeperThread = new SleeperThread(fakeClock, 0);
    sleeperThread.start();
    sleeperThread.join();
    assertThat(sleeperThread.isSleeping()).isFalse();

    SleeperThread[] sleeperThreads = new SleeperThread[5];
    sleeperThreads[0] = new SleeperThread(fakeClock, 1000);
    sleeperThreads[1] = new SleeperThread(fakeClock, 1000);
    sleeperThreads[2] = new SleeperThread(fakeClock, 2000);
    sleeperThreads[3] = new SleeperThread(fakeClock, 3000);
    sleeperThreads[4] = new SleeperThread(fakeClock, 4000);
    for (SleeperThread thread : sleeperThreads) {
      thread.start();
      assertThat(thread.waitUntilAsleep(TIMEOUT_MS)).isTrue();
    }
    assertSleepingStates(new boolean[] {true, true, true, true, true}, sleeperThreads);
    fakeClock.advanceTime(1500);
    assertThat(sleeperThreads[0].waitUntilAwake(TIMEOUT_MS)).isTrue();
    assertThat(sleeperThreads[1].waitUntilAwake(TIMEOUT_MS)).isTrue();
    assertSleepingStates(new boolean[] {false, false, true, true, true}, sleeperThreads);
    fakeClock.advanceTime(2000);
    assertThat(sleeperThreads[2].waitUntilAwake(TIMEOUT_MS)).isTrue();
    assertThat(sleeperThreads[3].waitUntilAwake(TIMEOUT_MS)).isTrue();
    assertSleepingStates(new boolean[] {false, false, false, false, true}, sleeperThreads);
    fakeClock.advanceTime(2000);
    for (SleeperThread thread : sleeperThreads) {
      thread.join(TIMEOUT_MS);
    }
    assertSleepingStates(new boolean[] {false, false, false, false, false}, sleeperThreads);
  }

  @Test
  public void testPostDelayed() {
    HandlerThread handlerThread = new HandlerThread("FakeClockTest thread");
    handlerThread.start();
    FakeClock fakeClock = new FakeClock(0);
    HandlerWrapper handler =
        fakeClock.createHandler(handlerThread.getLooper(), /* callback= */ null);

    TestRunnable[] testRunnables = {
      new TestRunnable(),
      new TestRunnable(),
      new TestRunnable(),
      new TestRunnable(),
      new TestRunnable()
    };
    handler.postDelayed(testRunnables[0], 0);
    handler.postDelayed(testRunnables[1], 100);
    handler.postDelayed(testRunnables[2], 200);
    waitForHandler(handler);
    assertTestRunnableStates(new boolean[] {true, false, false, false, false}, testRunnables);

    fakeClock.advanceTime(150);
    handler.postDelayed(testRunnables[3], 50);
    handler.postDelayed(testRunnables[4], 100);
    waitForHandler(handler);
    assertTestRunnableStates(new boolean[] {true, true, false, false, false}, testRunnables);

    fakeClock.advanceTime(50);
    waitForHandler(handler);
    assertTestRunnableStates(new boolean[] {true, true, true, true, false}, testRunnables);

    fakeClock.advanceTime(1000);
    waitForHandler(handler);
    assertTestRunnableStates(new boolean[] {true, true, true, true, true}, testRunnables);
  }

  private static void assertSleepingStates(boolean[] states, SleeperThread[] sleeperThreads) {
    for (int i = 0; i < sleeperThreads.length; i++) {
      assertThat(sleeperThreads[i].isSleeping()).isEqualTo(states[i]);
    }
  }

  private static void waitForHandler(HandlerWrapper handler) {
    final ConditionVariable handlerFinished = new ConditionVariable();
    handler.post(handlerFinished::open);
    handlerFinished.block();
  }

  private static void assertTestRunnableStates(boolean[] states, TestRunnable[] testRunnables) {
    for (int i = 0; i < testRunnables.length; i++) {
      assertThat(testRunnables[i].hasRun).isEqualTo(states[i]);
    }
  }

  private static final class SleeperThread extends Thread {

    private final Clock clock;
    private final long sleepDurationMs;
    private final CountDownLatch fallAsleepCountDownLatch;
    private final CountDownLatch wakeUpCountDownLatch;

    private volatile boolean isSleeping;

    public SleeperThread(Clock clock, long sleepDurationMs) {
      this.clock = clock;
      this.sleepDurationMs = sleepDurationMs;
      this.fallAsleepCountDownLatch = new CountDownLatch(1);
      this.wakeUpCountDownLatch = new CountDownLatch(1);
    }

    public boolean waitUntilAsleep(long timeoutMs) throws InterruptedException {
      return fallAsleepCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS);
    }

    public boolean waitUntilAwake(long timeoutMs) throws InterruptedException {
      return wakeUpCountDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS);
    }

    public boolean isSleeping() {
      return isSleeping;
    }

    @Override
    public void run() {
      // This relies on the FakeClock's methods synchronizing on its own monitor to ensure that
      // any interactions with it occur only after sleep() has called wait() or returned.
      synchronized (clock) {
        isSleeping = true;
        fallAsleepCountDownLatch.countDown();
        clock.sleep(sleepDurationMs);
        isSleeping = false;
        wakeUpCountDownLatch.countDown();
      }
    }
  }

  private static final class TestRunnable implements Runnable {

    public boolean hasRun;

    @Override
    public void run() {
      hasRun = true;
    }
  }
}