提交 4671c23c 编写于 作者: T tonihei 提交者: Oliver Woodman

Migrate ExoPlayerTest to Robolectric.

So far this wasn't possible because Robolectric's Looper and MessageQueue
implementations have multiple shortcomings:
 1. The message loop of new HandlerThreads is an not an actual loop and
    scheduled messages are executed on the thread the message is enqueued
    (not the handler thread).
 2. The scheduler used to replace the message queue is synchronizing all its
    methods. Thus, when a test attempts to add messages to a Handler from
    two different threads, it may easily run into a deadlock.
 3. The scheduler doesn't correctly emulate the order of messages as they
    would be in an actual MessageQueue:
   a. If the message is enqueued on the handler thread, it gets executed
      immediately (and not after all other messages at the same time).
   b. The list of messages is always re-sorted by time, meaning that the
      order of execution for messages at the same time is indeterminate.
 4. Robolectric's SystemClock implementation returns the current scheduler
    time of the main UI thread. So, unless this scheduler is used to add
    messages in the future, the SystemClock time never advances.

This CL adds two helper classes which extend and replace Robolectric's
ShadowLooper and ShadowMessageQueue.
 1. We intercept messages being enqueued or deleted in the message queue.
    Thus Robolectric's faulty scheduler gets never used. Instead, we keep
    a blocking priority queue of messages, sorted first by execution time
    and then by FIFO order to correctly emulate the real MessageQueue.
 2. We also keep a list of deleted messages to know which messages to ignore
    when they come up in the looper.
 3. When a new Looper is started, we override the dummy loop to an actual
    eternal while loop which waits for new messages, checks if they haven't
    been deleted, and runs the messages (similar to what Robolectric's
    MessageQueue would have done at this point).

Because we don't actually use the main UI thread in our tests, we can't rely
on the SystemClock to progress in any sensible manner. To overcome this issue,
we can use the auto-advancing FakeClock also used for the simulation tests.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=182912510
上级 e991a801
/*
* Copyright (C) 2018 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;
import static org.robolectric.Shadows.shadowOf;
import static org.robolectric.util.ReflectionHelpers.callInstanceMethod;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.google.android.exoplayer2.util.Util;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowLooper;
import org.robolectric.shadows.ShadowMessageQueue;
/** Collection of shadow classes used to run tests with Robolectric which require Loopers. */
public final class RobolectricUtil {
private static final AtomicLong sequenceNumberGenerator = new AtomicLong(0);
private RobolectricUtil() {}
/**
* A custom implementation of Robolectric's ShadowLooper which runs all scheduled messages in the
* loop method of the looper. Also ensures to correctly emulate the message order of the real
* message loop and to avoid blocking caused by Robolectric's default implementation.
*
* <p>Only works in conjunction with {@link CustomMessageQueue}. Note that the test's {@code
* SystemClock} is not advanced automatically.
*/
@Implements(Looper.class)
public static final class CustomLooper extends ShadowLooper {
private final PriorityBlockingQueue<PendingMessage> pendingMessages;
private final CopyOnWriteArraySet<RemovedMessage> removedMessages;
public CustomLooper() {
pendingMessages = new PriorityBlockingQueue<>();
removedMessages = new CopyOnWriteArraySet<>();
}
@Implementation
public static void loop() {
ShadowLooper looper = shadowOf(Looper.myLooper());
if (looper instanceof CustomLooper) {
((CustomLooper) looper).doLoop();
}
}
@Implementation
@Override
public void quitUnchecked() {
super.quitUnchecked();
// Insert message at the front of the queue to quit loop as soon as possible.
addPendingMessage(/* message= */ null, /* when= */ Long.MIN_VALUE);
}
private void addPendingMessage(@Nullable Message message, long when) {
pendingMessages.put(new PendingMessage(message, when));
}
private void removeMessages(Handler handler, int what, Object object) {
RemovedMessage newRemovedMessage = new RemovedMessage(handler, what, object);
removedMessages.add(newRemovedMessage);
for (RemovedMessage removedMessage : removedMessages) {
if (removedMessage != newRemovedMessage
&& removedMessage.handler == handler
&& removedMessage.what == what
&& removedMessage.object == object) {
removedMessages.remove(removedMessage);
}
}
}
private void doLoop() {
try {
while (true) {
PendingMessage pendingMessage = pendingMessages.take();
if (pendingMessage.message == null) {
// Null message is signal to end message loop.
return;
}
// Call through to real {@code Message.markInUse()} and {@code Message.recycle()} to
// ensure message recycling works. This is also done in Robolectric's own implementation
// of the message queue.
callInstanceMethod(pendingMessage.message, "markInUse");
Handler target = pendingMessage.message.getTarget();
if (target != null) {
boolean isRemoved = false;
for (RemovedMessage removedMessage : removedMessages) {
if (removedMessage.handler == target
&& removedMessage.what == pendingMessage.message.what
&& (removedMessage.object == null
|| removedMessage.object == pendingMessage.message.obj)
&& pendingMessage.sequenceNumber < removedMessage.sequenceNumber) {
isRemoved = true;
}
}
if (!isRemoved) {
target.dispatchMessage(pendingMessage.message);
}
}
if (Util.SDK_INT >= 21) {
callInstanceMethod(pendingMessage.message, "recycleUnchecked");
} else {
callInstanceMethod(pendingMessage.message, "recycle");
}
}
} catch (InterruptedException e) {
// Ignore.
}
}
}
/**
* Custom implementation of Robolectric's ShadowMessageQueue which is needed to let {@link
* CustomLooper} work as intended.
*/
@Implements(MessageQueue.class)
public static final class CustomMessageQueue extends ShadowMessageQueue {
private final Thread looperThread;
public CustomMessageQueue() {
looperThread = Thread.currentThread();
}
@Implementation
@Override
public boolean enqueueMessage(Message msg, long when) {
ShadowLooper looper = shadowOf(ShadowLooper.getLooperForThread(looperThread));
if (looper instanceof CustomLooper) {
((CustomLooper) looper).addPendingMessage(msg, when);
}
return true;
}
@Implementation
public void removeMessages(Handler handler, int what, Object object) {
ShadowLooper looper = shadowOf(ShadowLooper.getLooperForThread(looperThread));
if (looper instanceof CustomLooper) {
((CustomLooper) looper).removeMessages(handler, what, object);
}
}
}
private static final class PendingMessage implements Comparable<PendingMessage> {
public final @Nullable Message message;
public final long when;
public final long sequenceNumber;
public PendingMessage(@Nullable Message message, long when) {
this.message = message;
this.when = when;
sequenceNumber = sequenceNumberGenerator.getAndIncrement();
}
@Override
public int compareTo(@NonNull PendingMessage other) {
int res = Long.compare(this.when, other.when);
if (res == 0 && this != other) {
res = Long.compare(this.sequenceNumber, other.sequenceNumber);
}
return res;
}
}
private static final class RemovedMessage {
public final Handler handler;
public final int what;
public final Object object;
public final long sequenceNumber;
public RemovedMessage(Handler handler, int what, Object object) {
this.handler = handler;
this.what = what;
this.object = object;
this.sequenceNumber = sequenceNumberGenerator.get();
}
}
}
/*
* Copyright (C) 2018 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 com.google.android.exoplayer2.util.HandlerWrapper;
/**
* {@link FakeClock} extension which automatically advances time whenever an empty message is
* enqueued at a future time. The clock time is advanced to the time of the message. Only the first
* Handler sending messages at a future time will be allowed to advance time to ensure there is only
* one "time master". This should usually be the Handler of the internal playback loop.
*/
public final class AutoAdvancingFakeClock extends FakeClock {
private HandlerWrapper autoAdvancingHandler;
public AutoAdvancingFakeClock() {
super(/* initialTimeMs= */ 0);
}
@Override
protected synchronized boolean addHandlerMessageAtTime(
HandlerWrapper handler, int message, long timeMs) {
boolean result = super.addHandlerMessageAtTime(handler, message, timeMs);
if (autoAdvancingHandler == null || autoAdvancingHandler == handler) {
autoAdvancingHandler = handler;
long currentTimeMs = elapsedRealtime();
if (currentTimeMs < timeMs) {
advanceTime(timeMs - currentTimeMs);
}
}
return result;
}
}
......@@ -203,8 +203,8 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
}
/**
* Sets the {@link Clock} to be used by the test runner. The default value is {@link
* Clock#DEFAULT}.
* Sets the {@link Clock} to be used by the test runner. The default value is a {@link
* AutoAdvancingFakeClock}.
*
* @param clock A {@link Clock} to be used by the test runner.
* @return This builder.
......@@ -307,7 +307,7 @@ public final class ExoPlayerTestRunner extends Player.DefaultEventListener
loadControl = new DefaultLoadControl();
}
if (clock == null) {
clock = Clock.DEFAULT;
clock = new AutoAdvancingFakeClock();
}
if (mediaSource == null) {
if (timeline == null) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册