Changed Robolectric's ShadowAccessibilityNodeInfo.equals() test to use a unique source ID.

This is simpler and more efficient, and more accurately mimics the behavior of AccessibilityNodeInfo.equals() which matches instances based upon their origins.
diff --git a/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java b/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java
index 487fd6e..2b42469 100644
--- a/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java
+++ b/robolectric/src/test/java/org/robolectric/shadows/ShadowAccessibilityNodeInfoTest.java
@@ -2,12 +2,12 @@
 
 import static android.os.Build.VERSION_CODES.LOLLIPOP;
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.assertEquals;
 import static org.robolectric.Shadows.shadowOf;
 
 import android.graphics.Rect;
 import android.os.Bundle;
 import android.os.Parcel;
+import android.view.View;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 import android.view.accessibility.AccessibilityWindowInfo;
@@ -16,6 +16,7 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.robolectric.RobolectricTestRunner;
+import org.robolectric.RuntimeEnvironment;
 import org.robolectric.annotation.Config;
 
 @RunWith(RobolectricTestRunner.class)
@@ -45,12 +46,13 @@
   }
 
   @Test
-  public void ShouldHaveClonedCorrectly() {
+  public void shouldHaveClonedCorrectly() {
     node.setAccessibilityFocused(true);
     node.setBoundsInParent(new Rect(0, 0, 100, 100));
     node.setContentDescription("test");
     AccessibilityNodeInfo anotherNode = AccessibilityNodeInfo.obtain(node);
-    assertEquals(node, anotherNode);
+    assertThat(anotherNode).isEqualTo(node);
+    assertThat(anotherNode.getContentDescription().toString()).isEqualTo("test");
   }
 
   @Test
@@ -71,7 +73,7 @@
     shadowOf(node).addChild(child);
     shadowOf(child).addChild(node);
     AccessibilityNodeInfo anotherNode = AccessibilityNodeInfo.obtain(node);
-    assertThat(node.equals(anotherNode)).isEqualTo(true);
+    assertThat(node).isEqualTo(anotherNode);
   }
 
   @Test
@@ -82,14 +84,14 @@
     shadow.addChild(child1);
     ShadowAccessibilityNodeInfo child1Shadow = shadowOf(child1);
     child1Shadow.addChild(node);
-    AccessibilityNodeInfo anotherNode = ShadowAccessibilityNodeInfo.obtain(node);
+    AccessibilityNodeInfo anotherNode = ShadowAccessibilityNodeInfo.obtain();
     AccessibilityNodeInfo child2 = ShadowAccessibilityNodeInfo.obtain();
     child2.setText("test");
     ShadowAccessibilityNodeInfo child2Shadow = shadowOf(child2);
     ShadowAccessibilityNodeInfo anotherNodeShadow = shadowOf(anotherNode);
     anotherNodeShadow.addChild(child2);
     child2Shadow.addChild(anotherNode);
-    assertThat(node.equals(anotherNode)).isEqualTo(false);
+    assertThat(node).isNotEqualTo(anotherNode);
   }
 
   @Test
@@ -156,28 +158,45 @@
   }
 
   @Test
-  public void equalsTest_avoidsNullPointerDuringParentComparison() {
-    AccessibilityNodeInfo grandparentInfo = AccessibilityNodeInfo.obtain();
-    AccessibilityNodeInfo childInfo = AccessibilityNodeInfo.obtain();
-    AccessibilityNodeInfo parentInfo = AccessibilityNodeInfo.obtain();
-    shadowOf(grandparentInfo).addChild(parentInfo);
-    shadowOf(parentInfo).addChild(childInfo);
+  public void equalsTest_unrelatedNodesAreUnequal() {
+    AccessibilityNodeInfo nodeA = AccessibilityNodeInfo.obtain();
+    AccessibilityNodeInfo nodeB = AccessibilityNodeInfo.obtain();
+    shadowOf(nodeA).setText("test");
+    shadowOf(nodeB).setText("test");
 
-    assertThat(parentInfo.equals(childInfo)).isFalse();
-    assertThat(childInfo.equals(parentInfo)).isFalse();
-    assertThat(grandparentInfo.equals(parentInfo)).isFalse();
-    assertThat(parentInfo.equals(grandparentInfo)).isFalse();
+    assertThat(nodeA).isNotEqualTo(nodeB);
   }
 
   @Test
-  public void equalsTest_avoidsNullPointerDuringChildrenComparison() {
-    node.setVisibleToUser(true);
-    AccessibilityNodeInfo child1 = AccessibilityNodeInfo.obtain();
-    AccessibilityNodeInfo child2 = AccessibilityNodeInfo.obtain();
-    shadowOf(node).addChild(child1);
-    shadowOf(node).addChild(child2);
+  public void equalsTest_nodesFromTheSameViewAreEqual() {
+    View view = new View(RuntimeEnvironment.application);
+    AccessibilityNodeInfo nodeA = AccessibilityNodeInfo.obtain(view);
+    AccessibilityNodeInfo nodeB = AccessibilityNodeInfo.obtain(view);
+    shadowOf(nodeA).setText("tomato");
+    shadowOf(nodeB).setText("tomatoe");
 
-    assertThat(node).isEqualTo(node);
+    assertThat(nodeA).isEqualTo(nodeB);
+  }
+
+  @Test
+  public void equalsTest_nodesFromDifferentViewsAreNotEqual() {
+    View viewA = new View(RuntimeEnvironment.application);
+    View viewB = new View(RuntimeEnvironment.application);
+    AccessibilityNodeInfo nodeA = AccessibilityNodeInfo.obtain(viewA);
+    AccessibilityNodeInfo nodeB = AccessibilityNodeInfo.obtain(viewB);
+    shadowOf(nodeA).setText("test");
+    shadowOf(nodeB).setText("test");
+
+    assertThat(nodeA).isNotEqualTo(nodeB);
+  }
+
+  @Test
+  public void equalsTest_nodeIsEqualToItsClone_evenWhenModified() {
+    node = AccessibilityNodeInfo.obtain();
+    AccessibilityNodeInfo clone = AccessibilityNodeInfo.obtain(node);
+    shadowOf(clone).setText("test");
+
+    assertThat(node).isEqualTo(clone);
   }
 
   @After
diff --git a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java
index e5f3ce1..82b2d7f 100644
--- a/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java
+++ b/shadows/framework/src/main/java/org/robolectric/shadows/ShadowAccessibilityNodeInfo.java
@@ -11,7 +11,6 @@
 import android.os.Bundle;
 import android.os.Parcel;
 import android.os.Parcelable;
-import android.text.TextUtils;
 import android.util.Pair;
 import android.util.SparseArray;
 import android.view.View;
@@ -104,6 +103,13 @@
 
   private static final int CAN_OPEN_POPUP_MASK = 0x00100000; //19
 
+  /**
+   * Uniquely identifies the origin of the AccessibilityNodeInfo for equality
+   * testing. Two instances that come from the same node info should have the
+   * same ID.
+   */
+  private long mOriginNodeId;
+
   private List<AccessibilityNodeInfo> children;
 
   private Rect boundsInScreen = new Rect();
@@ -167,8 +173,6 @@
   private AccessibilityNodeInfo traversalBefore; //22
 
   private OnPerformActionListener actionListener;
-  
-  private boolean visitedWhenCheckingChildren = false;
 
   @RealObject
   private AccessibilityNodeInfo realAccessibilityNodeInfo;
@@ -184,6 +188,9 @@
     final AccessibilityNodeInfo obtainedInstance = shadowInfo.getClone();
 
     sAllocationCount++;
+    if (shadowInfo.mOriginNodeId == 0) {
+      shadowInfo.mOriginNodeId = sAllocationCount;
+    }
     StrictEqualityNodeWrapper wrapper = new StrictEqualityNodeWrapper(obtainedInstance);
     obtainedInstances.put(wrapper, Thread.currentThread().getStackTrace());
     orderedInstances.put(sAllocationCount, wrapper);
@@ -212,6 +219,9 @@
 
     shadowObtained.view = view;
     sAllocationCount++;
+    if (shadowObtained.mOriginNodeId == 0) {
+      shadowObtained.mOriginNodeId = sAllocationCount;
+    }
     StrictEqualityNodeWrapper wrapper = new StrictEqualityNodeWrapper(obtainedInstance);
     obtainedInstances.put(wrapper, Thread.currentThread().getStackTrace());
     orderedInstances.put(sAllocationCount, wrapper);
@@ -924,35 +934,9 @@
     return actionListener == null || actionListener.onPerformAccessibilityAction(action, arguments);
   }
 
-  private boolean childrenEqualityCheck(
-      ShadowAccessibilityNodeInfo otherShadow,
-      LinkedList<ShadowAccessibilityNodeInfo> visitedNodes) {
-    if (children == null) {
-      return otherShadow.getChildCount() == 0;
-    } else if (getChildCount() != otherShadow.getChildCount()) {
-      return false;
-    }
-    boolean childrenEquality = true;
-    for (int i = 0; i < children.size(); i++) {
-      AccessibilityNodeInfo child = children.get(i);
-      ShadowAccessibilityNodeInfo childShadow = shadowOf(child);
-      if (!childShadow.visitedWhenCheckingChildren) {
-        AccessibilityNodeInfo otherChild = otherShadow.children.get(i);
-        visitedNodes.add(childShadow);
-        childShadow.visitedWhenCheckingChildren = true;
-        if (!child.equals(otherChild)) {
-           childrenEquality = false;
-           break;
-        }
-        childrenEquality = childShadow.childrenEqualityCheck(shadowOf(otherChild), visitedNodes);
-      }
-    }
-    return childrenEquality;
-  }
-
   /**
-   * Equality check based on reference equality for mParent and mView and
-   * value equality for other fields.
+   * Equality check based on reference equality of the Views from which these instances were
+   * created, or the equality of their assigned IDs.
    */
   @Implementation
   @Override
@@ -964,119 +948,13 @@
     final AccessibilityNodeInfo info = (AccessibilityNodeInfo) object;
     final ShadowAccessibilityNodeInfo otherShadow = shadowOf(info);
 
-    boolean areEqual = true;
-    if (children == null) {
-      areEqual &= (otherShadow.children == null);
-    } else {
-      LinkedList<ShadowAccessibilityNodeInfo> visitedNodes = new LinkedList<>();
-      areEqual &=
-          (otherShadow.children != null) && childrenEqualityCheck(otherShadow, visitedNodes);
-      if (parent == null) {
-        areEqual &= (otherShadow.parent == null);
-      } else if (!shadowOf(parent).visitedWhenCheckingChildren){
-        areEqual &=
-            ((otherShadow.parent != null) && shadowOf(parent).equals(shadowOf(otherShadow.parent)));
-      }
-
-      while (!visitedNodes.isEmpty()) {
-        ShadowAccessibilityNodeInfo visitedNode = visitedNodes.remove();
-        visitedNode.visitedWhenCheckingChildren = false;
-      }
+    if (this.view != null) {
+      return this.view == otherShadow.view;
     }
-    areEqual &= (propertyFlags == otherShadow.propertyFlags);
-
-    if (getApiLevel() >= LOLLIPOP) {
-      boolean actionsArrayEquality = false;
-      if (actionsArray == null && otherShadow.actionsArray == null) {
-        actionsArrayEquality = true;
-      } else if (actionsArray == null || otherShadow.actionsArray == null) {
-        actionsArrayEquality = false;
-      } else {
-        actionsArrayEquality = actionsArray.equals(otherShadow.actionsArray);
-      }
-      areEqual &= actionsArrayEquality;
-      if (accessibilityWindowInfo == null) {
-        areEqual &= (otherShadow.accessibilityWindowInfo == null);
-      } else {
-        areEqual &= (otherShadow.accessibilityWindowInfo != null)
-            && accessibilityWindowInfo.equals(otherShadow.accessibilityWindowInfo);
-      }
-    } else {
-      areEqual &= (actionsMask == otherShadow.actionsMask);
+    if (this.mOriginNodeId != 0) {
+      return this.mOriginNodeId == otherShadow.mOriginNodeId;
     }
-
-    /*
-     * These checks have the potential to become infinite loops if there are
-     * loops in the labelFor or labeledBy logic. Rather than deal with this
-     * complexity, allow the failure since it will indicate a problem that
-     * needs addressing.
-     */
-    if (labelFor == null) {
-      areEqual &= (otherShadow.labelFor == null);
-    } else {
-      areEqual &= (labelFor.equals(otherShadow.labelFor));
-    }
-
-    if (labeledBy == null) {
-      areEqual &= (otherShadow.labeledBy == null);
-    } else {
-      areEqual &= (labeledBy.equals(otherShadow.labeledBy));
-    }
-
-    areEqual &= boundsInScreen.equals(otherShadow.boundsInScreen);
-    areEqual &= (TextUtils.equals(contentDescription, otherShadow.contentDescription));
-    areEqual &= (TextUtils.equals(text, otherShadow.text));
-
-    areEqual &= TextUtils.equals(className, otherShadow.className);
-    areEqual &= (view == otherShadow.view);
-    areEqual &= (textSelectionStart == otherShadow.textSelectionStart);
-    areEqual &= (textSelectionEnd == otherShadow.textSelectionEnd);
-
-    areEqual &= (refreshReturnValue == otherShadow.refreshReturnValue);
-    areEqual &= (movementGranularities == otherShadow.movementGranularities);
-    areEqual &= (TextUtils.isEmpty(packageName) == TextUtils.isEmpty(otherShadow.packageName));
-    if (!TextUtils.isEmpty(packageName)) {
-      areEqual &= (packageName.toString().equals(otherShadow.packageName.toString()));
-    }
-    if (getApiLevel() >= JELLY_BEAN_MR2) {
-      areEqual &= TextUtils.equals(viewIdResourceName, otherShadow.viewIdResourceName);
-    }
-    if (getApiLevel() >= KITKAT) {
-      if (collectionInfo == null) {
-        areEqual &= (otherShadow.collectionInfo == null);
-      } else {
-        areEqual &= (collectionInfo.equals(otherShadow.collectionInfo));
-      }
-      if (collectionItemInfo == null) {
-        areEqual &= (otherShadow.collectionItemInfo == null);
-      } else {
-        areEqual &= (collectionItemInfo.equals(otherShadow.collectionItemInfo));
-      }
-      areEqual &= (inputType == otherShadow.inputType);
-      areEqual &= (liveRegion == otherShadow.liveRegion);
-      if (rangeInfo == null) {
-        areEqual &= (otherShadow.rangeInfo == null);
-      } else {
-        areEqual &= (rangeInfo.equals(otherShadow.rangeInfo));
-      }
-    }
-    if (getApiLevel() >= LOLLIPOP) {
-      areEqual &= (maxTextLength == otherShadow.maxTextLength);
-      areEqual &= TextUtils.equals(error, otherShadow.error);
-    }
-    if (getApiLevel() >= LOLLIPOP_MR1) {
-      if (traversalAfter == null) {
-        areEqual &= (otherShadow.traversalAfter == null);
-      } else {
-        areEqual &= (traversalAfter.equals(otherShadow.traversalAfter));
-      }
-      if (traversalBefore == null) {
-        areEqual &= (otherShadow.traversalBefore == null);
-      } else {
-        areEqual &= (traversalBefore.equals(otherShadow.traversalBefore));
-      }
-    }
-    return areEqual;
+    throw new IllegalStateException("Node has neither an ID nor View");
   }
 
   @Implementation
@@ -1155,6 +1033,7 @@
         ReflectionHelpers.callConstructor(AccessibilityNodeInfo.class);
     final ShadowAccessibilityNodeInfo newShadow = shadowOf(newInfo);
 
+    newShadow.mOriginNodeId = mOriginNodeId;
     newShadow.boundsInScreen = new Rect(boundsInScreen);
     newShadow.propertyFlags = propertyFlags;
     newShadow.contentDescription = contentDescription;
@@ -1162,6 +1041,7 @@
     newShadow.performedActionAndArgsList = performedActionAndArgsList;
     newShadow.parent = parent;
     newShadow.className = className;
+    newShadow.labelFor = labelFor;
     newShadow.labeledBy = labeledBy;
     newShadow.view = view;
     newShadow.textSelectionStart = textSelectionStart;