blob: e390f0f2d9739b31bdf736e0052925bec4b17d41 [file] [log] [blame]
/*
* Copyright (C) 2014 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.apps.common.testing.ui.espresso.base;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import com.google.android.apps.common.testing.ui.espresso.IdlingPolicies;
import com.google.android.apps.common.testing.ui.espresso.IdlingPolicy;
import com.google.android.apps.common.testing.ui.espresso.IdlingResource;
import com.google.android.apps.common.testing.ui.espresso.IdlingResource.ResourceCallback;
import com.google.common.collect.Lists;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import java.util.BitSet;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Keeps track of user-registered {@link IdlingResource}s.
*/
@Singleton
public final class IdlingResourceRegistry {
private static final String TAG = IdlingResourceRegistry.class.getSimpleName();
private static final int DYNAMIC_RESOURCE_HAS_IDLED = 1;
private static final int TIMEOUT_OCCURRED = 2;
private static final int IDLE_WARNING_REACHED = 3;
private static final int POSSIBLE_RACE_CONDITION_DETECTED = 4;
private static final Object TIMEOUT_MESSAGE_TAG = new Object();
private static final IdleNotificationCallback NO_OP_CALLBACK = new IdleNotificationCallback() {
@Override
public void allResourcesIdle() {}
@Override
public void resourcesStillBusyWarning(List<String> busys) {}
@Override
public void resourcesHaveTimedOut(List<String> busys) {}
};
// resources and idleState should only be accessed on main thread
private final List<IdlingResource> resources = Lists.newArrayList();
// idleState.get(i) == true indicates resources.get(i) is idle, false indicates it's busy
private final BitSet idleState = new BitSet();
private final Looper looper;
private final Handler handler;
private final Dispatcher dispatcher;
private IdleNotificationCallback idleNotificationCallback = NO_OP_CALLBACK;
@Inject
public IdlingResourceRegistry(Looper looper) {
this.looper = looper;
this.dispatcher = new Dispatcher();
this.handler = new Handler(looper, dispatcher);
}
/**
* Registers the given resource.
*/
public void register(final IdlingResource resource) {
checkNotNull(resource);
if (Looper.myLooper() != looper) {
handler.post(new Runnable() {
@Override
public void run() {
register(resource);
}
});
} else {
for (IdlingResource oldResource : resources) {
if (resource.getName().equals(oldResource.getName())) {
// This does not throw an error to avoid leaving tests that register resource in test
// setup in an undeterministic state (we cannot assume that everyone clears vm state
// between each test run)
Log.e(TAG, String.format("Attempted to register resource with same names:" +
" %s. R1: %s R2: %s.\nDuplicate resource registration will be ignored.",
resource.getName(), resource, oldResource));
return;
}
}
resources.add(resource);
final int position = resources.size() - 1;
registerToIdleCallback(resource, position);
idleState.set(position, resource.isIdleNow());
}
}
public void registerLooper(Looper looper, boolean considerWaitIdle) {
checkNotNull(looper);
checkArgument(Looper.getMainLooper() != looper, "Not intended for use with main looper!");
register(new LooperIdlingResource(looper, considerWaitIdle));
}
private void registerToIdleCallback(IdlingResource resource, final int position) {
resource.registerIdleTransitionCallback(new ResourceCallback() {
@Override
public void onTransitionToIdle() {
Message m = handler.obtainMessage(DYNAMIC_RESOURCE_HAS_IDLED);
m.arg1 = position;
handler.sendMessage(m);
}
});
}
boolean allResourcesAreIdle() {
checkState(Looper.myLooper() == looper);
for (int i = idleState.nextSetBit(0); i >= 0 && i < resources.size();
i = idleState.nextSetBit(i + 1)) {
idleState.set(i, resources.get(i).isIdleNow());
}
return idleState.cardinality() == resources.size();
}
interface IdleNotificationCallback {
public void allResourcesIdle();
public void resourcesStillBusyWarning(List<String> busyResourceNames);
public void resourcesHaveTimedOut(List<String> busyResourceNames);
}
void notifyWhenAllResourcesAreIdle(IdleNotificationCallback callback) {
checkNotNull(callback);
checkState(Looper.myLooper() == looper);
checkState(idleNotificationCallback == NO_OP_CALLBACK, "Callback has already been registered.");
if (allResourcesAreIdle()) {
callback.allResourcesIdle();
} else {
idleNotificationCallback = callback;
scheduleTimeoutMessages();
}
}
void cancelIdleMonitor() {
dispatcher.deregister();
}
private void scheduleTimeoutMessages() {
IdlingPolicy warning = IdlingPolicies.getDynamicIdlingResourceWarningPolicy();
Message timeoutWarning = handler.obtainMessage(IDLE_WARNING_REACHED, TIMEOUT_MESSAGE_TAG);
handler.sendMessageDelayed(timeoutWarning, warning.getIdleTimeoutUnit().toMillis(
warning.getIdleTimeout()));
Message timeoutError = handler.obtainMessage(TIMEOUT_OCCURRED, TIMEOUT_MESSAGE_TAG);
IdlingPolicy error = IdlingPolicies.getDynamicIdlingResourceErrorPolicy();
handler.sendMessageDelayed(timeoutError, error.getIdleTimeoutUnit().toMillis(
error.getIdleTimeout()));
}
private List<String> getBusyResources() {
List<String> busyResourceNames = Lists.newArrayList();
List<Integer> racyResources = Lists.newArrayList();
for (int i = 0; i < resources.size(); i++) {
IdlingResource resource = resources.get(i);
if (!idleState.get(i)) {
if (resource.isIdleNow()) {
// We have not been notified of a BUSY -> IDLE transition, but the resource is telling us
// its that its idle. Either it's a race condition or is this resource buggy.
racyResources.add(i);
} else {
busyResourceNames.add(resource.getName());
}
}
}
if (!racyResources.isEmpty()) {
Message raceBuster = handler.obtainMessage(POSSIBLE_RACE_CONDITION_DETECTED,
TIMEOUT_MESSAGE_TAG);
raceBuster.obj = racyResources;
handler.sendMessage(raceBuster);
return null;
} else {
return busyResourceNames;
}
}
private class Dispatcher implements Handler.Callback {
@Override
public boolean handleMessage(Message m) {
switch (m.what) {
case DYNAMIC_RESOURCE_HAS_IDLED:
handleResourceIdled(m);
break;
case IDLE_WARNING_REACHED:
handleTimeoutWarning();
break;
case TIMEOUT_OCCURRED:
handleTimeout();
break;
case POSSIBLE_RACE_CONDITION_DETECTED:
handleRaceCondition(m);
break;
default:
Log.w(TAG, "Unknown message type: " + m);
return false;
}
return true;
}
private void handleResourceIdled(Message m) {
idleState.set(m.arg1, true);
if (idleState.cardinality() == resources.size()) {
try {
idleNotificationCallback.allResourcesIdle();
} finally {
deregister();
}
}
}
private void handleTimeoutWarning() {
List<String> busyResources = getBusyResources();
if (busyResources == null) {
// null indicates that there is either a race or a programming error
// a race detector message has been inserted into the q.
// reinsert the idle_warning_reached message into the q directly after it
// so we generate warnings if the system is still sane.
handler.sendMessage(handler.obtainMessage(IDLE_WARNING_REACHED, TIMEOUT_MESSAGE_TAG));
} else {
IdlingPolicy warning = IdlingPolicies.getDynamicIdlingResourceWarningPolicy();
idleNotificationCallback.resourcesStillBusyWarning(busyResources);
handler.sendMessageDelayed(
handler.obtainMessage(IDLE_WARNING_REACHED, TIMEOUT_MESSAGE_TAG),
warning.getIdleTimeoutUnit().toMillis(warning.getIdleTimeout()));
}
}
private void handleTimeout() {
List<String> busyResources = getBusyResources();
if (busyResources == null) {
// detected a possible race... we've enqueued a race busting message
// so either that'll resolve the race or kill the app because it's buggy.
// if the race resolves, we need to timeout properly.
handler.sendMessage(handler.obtainMessage(TIMEOUT_OCCURRED, TIMEOUT_MESSAGE_TAG));
} else {
try {
idleNotificationCallback.resourcesHaveTimedOut(busyResources);
} finally {
deregister();
}
}
}
@SuppressWarnings("unchecked")
private void handleRaceCondition(Message m) {
for (Integer i : (List<Integer>) m.obj) {
if (idleState.get(i)) {
// it was a race... i is now idle, everything is fine...
} else {
throw new IllegalStateException(String.format(
"Resource %s isIdleNow() is returning true, but a message indicating that the "
+ "resource has transitioned from busy to idle was never sent.",
resources.get(i).getName()));
}
}
}
private void deregister() {
handler.removeCallbacksAndMessages(TIMEOUT_MESSAGE_TAG);
idleNotificationCallback = NO_OP_CALLBACK;
}
}
}