DefaultFailureHandler.java
/*
* 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 androidx.test.espresso.base;
import static androidx.test.internal.util.Checks.checkNotNull;
import android.content.Context;
import android.util.Log;
import android.view.View;
import androidx.test.core.app.DeviceCapture;
import androidx.test.core.graphics.BitmapStorage;
import androidx.test.espresso.AmbiguousViewMatcherException;
import androidx.test.espresso.EspressoException;
import androidx.test.espresso.FailureHandler;
import androidx.test.espresso.NoMatchingViewException;
import androidx.test.espresso.PerformException;
import androidx.test.espresso.base.ViewHierarchyExceptionHandler.Truncater;
import androidx.test.espresso.internal.inject.TargetContext;
import androidx.test.internal.platform.util.TestOutputEmitter;
import androidx.test.platform.io.PlatformTestStorage;
import androidx.test.platform.io.PlatformTestStorageRegistry;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import javax.inject.Inject;
import junit.framework.AssertionFailedError;
import org.hamcrest.Matcher;
/**
* Espresso's default {@link FailureHandler}. If this does not fit your needs, feel free to provide
* your own implementation via Espresso.setFailureHandler(FailureHandler).
*/
public final class DefaultFailureHandler implements FailureHandler {
private static final AtomicInteger failureCount = new AtomicInteger(0);
private final List<FailureHandler> handlers = new ArrayList<>();
private final PlatformTestStorage testStorage;
private final boolean captureScreenshotOnFailure;
public DefaultFailureHandler(@TargetContext Context appContext) {
this(appContext, PlatformTestStorageRegistry.getInstance(), true);
}
public DefaultFailureHandler(
@TargetContext Context appContext, boolean captureScreenshotOnFailure) {
this(appContext, PlatformTestStorageRegistry.getInstance(), captureScreenshotOnFailure);
}
@Inject
DefaultFailureHandler(
@TargetContext Context appContext,
PlatformTestStorage testStorage,
boolean captureScreenshotOnFailure) {
// Adds a chain of exception handlers.
// Order matters and a matching failure handler in the chain will throw after the exception is
// handled. Always adds the handler of the child class ahead of its superclasses to make sure
// the exception is handled by its corresponding handler.
//
// The hierarchy of the exception types handled is:
// NoMatchingViewException -->
// PerformException ---------> EspressoException
// ---------> Throwable
// AssertionError ----------->
this.testStorage = testStorage;
this.captureScreenshotOnFailure = captureScreenshotOnFailure;
handlers.add(
new ViewHierarchyExceptionHandler<>(
testStorage,
failureCount,
NoMatchingViewException.class,
getNoMatchingViewExceptionTruncater()));
handlers.add(
new ViewHierarchyExceptionHandler<>(
testStorage,
failureCount,
AmbiguousViewMatcherException.class,
getAmbiguousViewMatcherExceptionTruncater()));
handlers.add(new PerformExceptionHandler(checkNotNull(appContext), PerformException.class));
// On API 15, junit.framework.AssertionFailedError is not a subclass of AssertionError.
handlers.add(new AssertionErrorHandler(AssertionFailedError.class, AssertionError.class));
handlers.add(new EspressoExceptionHandler(EspressoException.class));
handlers.add(new ThrowableHandler());
}
static Truncater<NoMatchingViewException> getNoMatchingViewExceptionTruncater() {
return (exception, msgLen, viewHierarchyFile) ->
new NoMatchingViewException.Builder()
.from(exception)
.withMaxMsgLen(msgLen)
.withViewHierarchyFile(viewHierarchyFile)
.build();
}
static Truncater<AmbiguousViewMatcherException> getAmbiguousViewMatcherExceptionTruncater() {
return (exception, msgLen, viewHierarchyFile) ->
new AmbiguousViewMatcherException.Builder()
.from(exception)
.withMaxMsgLen(msgLen)
.withViewHierarchyFile(viewHierarchyFile)
.build();
}
@Override
public void handle(Throwable error, Matcher<View> viewMatcher) {
int count = failureCount.incrementAndGet();
try {
TestOutputEmitter.captureWindowHierarchy("explore-window-hierarchy-" + count + ".xml");
takeScreenshot("view-op-error-" + count);
} catch (RuntimeException screenshotException) {
// Ensure that the root cause exception is surfaced, not an auxiliary exception that may occur
// during the capture/screenshot process.
error.addSuppressed(screenshotException);
}
// Iterates through the list of handlers to handle the exception, but at most one handler will
// update the exception and throw at the end of the handling.
for (FailureHandler handler : handlers) {
handler.handle(error, viewMatcher);
}
}
private void takeScreenshot(String outputName) {
if (!captureScreenshotOnFailure) {
return;
}
try {
if (DeviceCapture.canTakeScreenshot()) {
BitmapStorage.writeToTestStorage(
DeviceCapture.takeScreenshotNoSync(), testStorage, outputName);
} else {
TestOutputEmitter.takeScreenshot(outputName + ".png");
}
} catch (RuntimeException | Error | IOException e) {
Log.w("DefaultFailureHandler", "Failed to take screenshot", e);
}
}
/** Handles failure for given types of exceptions. Does nothing if the types do not match. */
abstract static class TypedFailureHandler<T> implements FailureHandler {
private final List<Class<?>> acceptedTypes;
/**
* Constructor.
*
* @param acceptedTypes accepted types by this failure handler.
*/
public TypedFailureHandler(Class<?>... acceptedTypes) {
this.acceptedTypes = checkNotNull(Arrays.asList(acceptedTypes));
}
@Override
@SuppressWarnings({"unchecked"})
public void handle(Throwable error, Matcher<View> viewMatcher) {
if (error != null) {
for (Class<?> acceptedType : acceptedTypes) {
if (acceptedType.isInstance(error)) {
handleSafely((T) error, viewMatcher);
break;
}
}
}
}
abstract void handleSafely(T error, Matcher<View> viewMatcher);
}
}