Skip to content

Commit e25a980

Browse files
Added a failure handler for the NoMatchingViewException. It dumps the full view hierarchy to a file, when the test storage service is installed on the device.
PiperOrigin-RevId: 375843010
1 parent 22c865c commit e25a980

File tree

6 files changed

+203
-7
lines changed

6 files changed

+203
-7
lines changed

Diff for: espresso/core/java/androidx/test/espresso/NoMatchingViewException.java

+5
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ public String getViewMatcherDescription() {
7070
return viewMatcherDescription;
7171
}
7272

73+
/** Returns the root view where this exception is thrown. */
74+
public View getRootView() {
75+
return rootView;
76+
}
77+
7378
private static String getErrorMessage(Builder builder) {
7479
String errorMessage = "";
7580
if (builder.includeViewHierarchy) {

Diff for: espresso/core/java/androidx/test/espresso/base/BUILD.bazel

+3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ android_library(
1818
"IdlingUiController.java",
1919
"IdlingResourceRegistry.java",
2020
"LooperIdlingResourceInterrogationHandler.java",
21+
"NoMatchingViewExceptionHandler.java",
2122
],
2223
),
2324
plugins = ["//opensource/dagger:dagger_plugin"],
@@ -80,13 +81,15 @@ android_library(
8081
"AssertionErrorHandler.java",
8182
"DefaultFailureHandler.java",
8283
"EspressoExceptionHandler.java",
84+
"NoMatchingViewExceptionHandler.java",
8385
"PerformExceptionHandler.java",
8486
"ThrowableHandler.java",
8587
],
8688
visibility = ["//visibility:public"],
8789
deps = [
8890
"//espresso/core/java/androidx/test/espresso:interface",
8991
"//espresso/core/java/androidx/test/espresso/internal/inject",
92+
"//espresso/core/java/androidx/test/espresso/util",
9093
"//runner/monitor/java/androidx/test:monitor",
9194
"@maven//:com_google_dagger_dagger",
9295
"@maven//:com_google_guava_guava",

Diff for: espresso/core/java/androidx/test/espresso/base/BaseLayerModule.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,9 @@ FailureHandler provideFailureHander(DefaultFailureHandler impl) {
189189
}
190190

191191
@Provides
192-
DefaultFailureHandler provideDefaultFailureHander(@TargetContext Context context) {
193-
return new DefaultFailureHandler(context);
192+
DefaultFailureHandler provideDefaultFailureHander(
193+
@TargetContext Context context, PlatformTestStorage testStorage) {
194+
return new DefaultFailureHandler(context, testStorage);
194195
}
195196

196197
@Provides

Diff for: espresso/core/java/androidx/test/espresso/base/DefaultFailureHandler.java

+10-5
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@
2222
import android.view.View;
2323
import androidx.test.espresso.EspressoException;
2424
import androidx.test.espresso.FailureHandler;
25+
import androidx.test.espresso.NoMatchingViewException;
2526
import androidx.test.espresso.PerformException;
2627
import androidx.test.espresso.internal.inject.TargetContext;
2728
import androidx.test.internal.platform.util.TestOutputEmitter;
29+
import androidx.test.platform.io.PlatformTestStorage;
2830
import java.util.ArrayList;
2931
import java.util.Arrays;
3032
import java.util.List;
@@ -43,17 +45,20 @@ public final class DefaultFailureHandler implements FailureHandler {
4345
private final List<FailureHandler> handlers = new ArrayList<>();
4446

4547
@Inject
46-
public DefaultFailureHandler(@TargetContext Context appContext) {
48+
public DefaultFailureHandler(@TargetContext Context appContext, PlatformTestStorage testStorage) {
4749
// Adds a chain of exception handlers.
4850
// Order matters and a matching failure handler in the chain will throw after the exception is
4951
// handled. Always adds the handler of the child class ahead of its superclasses to make sure
5052
// the exception is handled by its corresponding handler.
5153
//
5254
// The hierarchy of the exception types handled is:
53-
//
54-
// PerformException --> EspressoException
55-
// --> Throwable
56-
// AssertionError ---->
55+
// NoMatchingViewException -->
56+
// PerformException ---------> EspressoException
57+
// ---------> Throwable
58+
// AssertionError ----------->
59+
handlers.add(
60+
new NoMatchingViewExceptionHandler(
61+
testStorage, failureCount, NoMatchingViewException.class));
5762
handlers.add(new PerformExceptionHandler(checkNotNull(appContext), PerformException.class));
5863
// On API 15, junit.framework.AssertionFailedError is not a subclass of AssertionError.
5964
handlers.add(new AssertionErrorHandler(AssertionFailedError.class, AssertionError.class));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright (C) 2021 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package androidx.test.espresso.base;
17+
18+
import static androidx.test.internal.util.Checks.checkNotNull;
19+
import static com.google.common.base.Throwables.throwIfUnchecked;
20+
21+
import android.util.Log;
22+
import android.view.View;
23+
import androidx.test.espresso.NoMatchingViewException;
24+
import androidx.test.espresso.base.DefaultFailureHandler.TypedFailureHandler;
25+
import androidx.test.espresso.util.HumanReadables;
26+
import androidx.test.platform.io.PlatformTestStorage;
27+
import java.io.IOException;
28+
import java.io.OutputStream;
29+
import java.util.concurrent.atomic.AtomicInteger;
30+
import org.hamcrest.Matcher;
31+
32+
/** An Espresso failure handler that handles an {@link NoMatchingViewException}. */
33+
class NoMatchingViewExceptionHandler extends TypedFailureHandler<NoMatchingViewException> {
34+
private static final String TAG = NoMatchingViewExceptionHandler.class.getSimpleName();
35+
36+
private final PlatformTestStorage testStorage;
37+
private final AtomicInteger failureCount;
38+
39+
public NoMatchingViewExceptionHandler(
40+
PlatformTestStorage testStorage,
41+
AtomicInteger failureCount,
42+
Class<NoMatchingViewException> expectedType) {
43+
super(expectedType);
44+
this.testStorage = checkNotNull(testStorage);
45+
this.failureCount = failureCount;
46+
}
47+
48+
@Override
49+
public void handleSafely(NoMatchingViewException error, Matcher<View> viewMatcher) {
50+
dumpFullViewHierarchyToFile(error);
51+
error.setStackTrace(Thread.currentThread().getStackTrace());
52+
throwIfUnchecked(error);
53+
throw new RuntimeException(error);
54+
}
55+
56+
private void dumpFullViewHierarchyToFile(NoMatchingViewException error) {
57+
String viewHierarchyMsg =
58+
HumanReadables.getViewHierarchyErrorMessage(error.getRootView(), null, "", null);
59+
String viewHierarchyFile = "view-hierarchy-" + failureCount + ".txt";
60+
try {
61+
addOutputFile(viewHierarchyFile, viewHierarchyMsg);
62+
} catch (IOException e) {
63+
// Log and ignore.
64+
Log.w(TAG, "Failed to save the view hierarchy to file " + viewHierarchyFile, e);
65+
}
66+
}
67+
68+
private void addOutputFile(String filename, String content) throws IOException {
69+
try (OutputStream out = testStorage.openOutputFile(filename)) {
70+
out.write(content.getBytes());
71+
}
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
/*
2+
* Copyright (C) 2021 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package androidx.test.espresso.base;
17+
18+
import static com.google.common.truth.Truth.assertThat;
19+
import static org.junit.Assert.assertThrows;
20+
import static org.mockito.ArgumentMatchers.eq;
21+
import static org.mockito.Mockito.verify;
22+
import static org.mockito.Mockito.when;
23+
24+
import android.view.View;
25+
import android.widget.TextView;
26+
import androidx.test.espresso.NoMatchingViewException;
27+
import androidx.test.ext.junit.runners.AndroidJUnit4;
28+
import androidx.test.platform.app.InstrumentationRegistry;
29+
import androidx.test.platform.io.PlatformTestStorage;
30+
import java.io.ByteArrayOutputStream;
31+
import java.io.IOException;
32+
import java.util.concurrent.atomic.AtomicInteger;
33+
import org.hamcrest.BaseMatcher;
34+
import org.hamcrest.Description;
35+
import org.hamcrest.Matcher;
36+
import org.junit.Before;
37+
import org.junit.Rule;
38+
import org.junit.Test;
39+
import org.junit.runner.RunWith;
40+
import org.mockito.Mock;
41+
import org.mockito.junit.MockitoJUnit;
42+
import org.mockito.junit.MockitoRule;
43+
44+
@RunWith(AndroidJUnit4.class)
45+
public class NoMatchingViewExceptionHandlerTest {
46+
47+
@Rule public final MockitoRule mockito = MockitoJUnit.rule();
48+
49+
private final AtomicInteger failureCount = new AtomicInteger();
50+
private NoMatchingViewExceptionHandler handler;
51+
private Matcher<View> viewMatcher;
52+
@Mock private PlatformTestStorage testStorage;
53+
54+
@Before
55+
public void setUp() throws IOException {
56+
when(testStorage.openOutputFile("view-hierarchy-1.txt"))
57+
.thenReturn(new ByteArrayOutputStream());
58+
handler =
59+
new NoMatchingViewExceptionHandler(
60+
testStorage, failureCount, NoMatchingViewException.class);
61+
viewMatcher =
62+
new BaseMatcher<View>() {
63+
@Override
64+
public boolean matches(Object o) {
65+
return false;
66+
}
67+
68+
@Override
69+
public void describeTo(Description description) {
70+
description.appendText("A view matcher");
71+
}
72+
};
73+
}
74+
75+
@Test
76+
public void handle_noMatchingViewException() throws IOException {
77+
NoMatchingViewException noMatchingViewException =
78+
new NoMatchingViewException.Builder()
79+
.withViewMatcher(viewMatcher)
80+
.withRootView(
81+
new TextView(InstrumentationRegistry.getInstrumentation().getTargetContext()))
82+
.build();
83+
String viewHierarchyMsg =
84+
"\n\nView Hierarchy:\n"
85+
+ "+>TextView{id=-1, visibility=VISIBLE, width=0, height=0, has-focus=false,"
86+
+ " has-focusable=false, has-window-focus=false, is-clickable=false, is-enabled=true,"
87+
+ " is-focused=false, is-focusable=false, is-layout-requested=true, is-selected=false,"
88+
+ " layout-params=null, tag=null, root-is-layout-requested=true,"
89+
+ " has-input-connection=false, x=0.0, y=0.0, text=, input-type=0, ime-target=false,"
90+
+ " has-links=false} ";
91+
92+
failureCount.incrementAndGet();
93+
NoMatchingViewException thrown =
94+
assertThrows(
95+
NoMatchingViewException.class,
96+
() -> handler.handle(noMatchingViewException, viewMatcher));
97+
98+
assertThat(thrown)
99+
.hasMessageThat()
100+
.contains("No views in hierarchy found matching: A view matcher" + viewHierarchyMsg);
101+
verify(testStorage).openOutputFile(eq("view-hierarchy-1.txt"));
102+
}
103+
104+
@Test
105+
public void handle_nonNoMatchingViewException() {
106+
// No-op. No exception should be thrown.
107+
handler.handle(new Throwable("A random error"), viewMatcher);
108+
}
109+
}

0 commit comments

Comments
 (0)