DiffTree reworked: smarter PSI trees merge to allow SmartPsiPointers work in XML

This commit is contained in:
Alexey Kudravtsev
2015-02-18 14:58:25 +03:00
parent ee287f4c1e
commit 5d3603f58d
7 changed files with 270 additions and 99 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@ package com.intellij.psi.impl.smartPointers;
import com.intellij.JavaTestUtil;
import com.intellij.codeInsight.CodeInsightTestCase;
import com.intellij.ide.highlighter.HtmlFileType;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.editor.Document;
@@ -37,6 +38,7 @@ import com.intellij.psi.stubs.StubTree;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.util.PsiUtilBase;
import com.intellij.psi.xml.XmlTag;
import com.intellij.testFramework.IdeaTestUtil;
import com.intellij.testFramework.PlatformTestCase;
import com.intellij.testFramework.PlatformTestUtil;
@@ -469,4 +471,64 @@ public class SmartPsiElementPointersTest extends CodeInsightTestCase {
assertEquals(range1.shiftRight(1), pointer1.getRange());
assertEquals(range2.shiftRight(1), pointer2.getRange());
}
public void testInXml() {
final PsiFile file = configureByText(HtmlFileType.INSTANCE,
"<!doctype html>\n" +
"<html>\n" +
" <fieldset></fieldset>\n" +
" <select></select>\n" +
"\n" +
" <caret>\n" +
"</html>"
);
final XmlTag fieldSet = PsiTreeUtil.getParentOfType(file.findElementAt(file.getText().indexOf("fieldset")), XmlTag.class);
assertNotNull(fieldSet);
assertEquals("fieldset", fieldSet.getName());
final XmlTag select = PsiTreeUtil.getParentOfType(file.findElementAt(file.getText().indexOf("select")), XmlTag.class);
assertNotNull(select);
assertEquals("select", select.getName());
final SmartPsiElementPointer<XmlTag> fieldSetPointer = SmartPointerManager.getInstance(getProject()).createSmartPsiElementPointer(
fieldSet);
final SmartPsiElementPointer<XmlTag> selectPointer = SmartPointerManager.getInstance(getProject()).createSmartPsiElementPointer(select);
WriteCommandAction.runWriteCommandAction(getProject(), new Runnable() {
@Override
public void run() {
getEditor().getDocument().insertString(getEditor().getCaretModel().getOffset(), "<a></a>");
}
});
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
final XmlTag newFieldSet = fieldSetPointer.getElement();
assertNotNull(newFieldSet);
assertEquals("fieldset", newFieldSet.getName());
final XmlTag newSelect = selectPointer.getElement();
assertNotNull(newSelect);
assertEquals("select", newSelect.getName());
}
public void testInsertImport() {
final PsiFile file = configureByText(JavaFileType.INSTANCE,
"class S {\n" +
"}");
PsiClass aClass = ((PsiJavaFile)file).getClasses()[0];
WriteCommandAction.runWriteCommandAction(getProject(), new Runnable() {
@Override
public void run() {
getEditor().getDocument().insertString(0, "import java.util.Map;\n");
}
});
PsiDocumentManager.getInstance(getProject()).commitAllDocuments();
PsiClass aClass2 = ((PsiJavaFile)file).getClasses()[0];
assertSame(aClass, aClass2);
}
}

View File

@@ -33,6 +33,7 @@ import com.intellij.util.diff.DiffTreeChangeBuilder;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
@@ -173,6 +174,8 @@ public class DiffLog implements DiffTreeChangeBuilder<ASTNode,ASTNode> {
private InsertEntry(@NotNull ASTNode oldParent, @NotNull ASTNode newNode, int pos) {
assert oldParent instanceof CompositeElement : oldParent;
assert pos>=0 : pos;
assert pos<=oldParent.getChildren(null).length : pos + " "+ Arrays.toString(oldParent.getChildren(null));
myOldParent = oldParent;
myNewNode = newNode;
myPos = pos;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2000-2009 JetBrains s.r.o.
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,8 +16,10 @@
package com.intellij.refactoring.classMembers;
import com.intellij.psi.PsiElement;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.psi.PsiElement;
import com.intellij.psi.SmartPointerManager;
import com.intellij.psi.SmartPsiElementPointer;
import com.intellij.psi.util.PsiUtilCore;
/**
@@ -25,7 +27,7 @@ import com.intellij.psi.util.PsiUtilCore;
*/
public abstract class MemberInfoBase<T extends PsiElement> {
protected static final Logger LOG = Logger.getInstance("#com.intellij.refactoring.extractSuperclass.MemberInfo");
protected T myMember;
private SmartPsiElementPointer<T> myMember;
protected boolean isStatic;
protected String displayName;
private boolean isChecked = false;
@@ -36,7 +38,7 @@ public abstract class MemberInfoBase<T extends PsiElement> {
private boolean toAbstract = false;
public MemberInfoBase(T member) {
myMember = member;
updateMember(member);
}
public boolean isStatic() {
@@ -67,16 +69,16 @@ public abstract class MemberInfoBase<T extends PsiElement> {
}
public T getMember() {
PsiUtilCore.ensureValid(myMember);
return myMember;
T element = myMember.getElement();
PsiUtilCore.ensureValid(element);
return element;
}
/**
* Use this method solely to update element from smart pointer and the likes
* @param element
*/
public void updateMember(T element) {
myMember = element;
myMember = SmartPointerManager.getInstance(element.getProject()).createSmartPsiElementPointer(element);
}
public boolean isToAbstract() {
@@ -87,7 +89,7 @@ public abstract class MemberInfoBase<T extends PsiElement> {
this.toAbstract = toAbstract;
}
public static interface Filter<T extends PsiElement> {
public interface Filter<T extends PsiElement> {
boolean includeMember(T member);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2000-2013 JetBrains s.r.o.
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -368,14 +368,16 @@ public abstract class InplaceRefactoring {
final int offset = myEditor.getCaretModel().getOffset();
Editor topLevelEditor = InjectedLanguageUtil.getTopLevelEditor(myEditor);
TextRange range = myScope.getTextRange();
assert range != null;
RangeMarker rangeMarker = topLevelEditor.getDocument().createRangeMarker(range);
Template template = builder.buildInlineTemplate();
template.setToShortenLongNames(false);
template.setToReformat(false);
TextRange range = myScope.getTextRange();
assert range != null;
myHighlighters = new ArrayList<RangeHighlighter>();
Editor topLevelEditor = InjectedLanguageUtil.getTopLevelEditor(myEditor);
topLevelEditor.getCaretModel().moveToOffset(range.getStartOffset());
topLevelEditor.getCaretModel().moveToOffset(rangeMarker.getStartOffset());
TemplateManager.getInstance(myProject).startTemplate(topLevelEditor, template, templateListener);
restoreOldCaretPositionAndSelection(offset);

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2000-2014 JetBrains s.r.o.
* Copyright 2000-2015 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -41,7 +41,6 @@ import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.psi.PsiElement;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.refactoring.rename.inplace.InplaceRefactoring;
import com.intellij.refactoring.rename.inplace.VariableInplaceRenameHandler;
import com.intellij.testFramework.TestDataFile;
import com.intellij.util.ui.UIUtil;
@@ -195,12 +194,12 @@ public class CodeInsightTestUtil {
}
@TestOnly
public static void doInlineRename(VariableInplaceRenameHandler handler, final String newName, Editor editor, PsiElement elementAtCaret) {
public static void doInlineRename(VariableInplaceRenameHandler handler, final String newName, @NotNull Editor editor, PsiElement elementAtCaret) {
Project project = editor.getProject();
TemplateManagerImpl templateManager = (TemplateManagerImpl)TemplateManager.getInstance(project);
try {
templateManager.setTemplateTesting(true);
InplaceRefactoring renamer = handler.doRename(elementAtCaret, editor, null);
handler.doRename(elementAtCaret, editor, DataManager.getInstance().getDataContext(editor.getComponent()));
if (editor instanceof EditorWindow) {
editor = ((EditorWindow)editor).getDelegate();
}

View File

@@ -31,25 +31,23 @@ public class DiffTree<OT, NT> {
private final FlyweightCapableTreeStructure<OT> myOldTree;
private final FlyweightCapableTreeStructure<NT> myNewTree;
private final ShallowNodeComparator<OT, NT> myComparator;
private final DiffTreeChangeBuilder<OT, NT> myConsumer;
private final List<Ref<OT[]>> myOldChildrenLists = new ArrayList<Ref<OT[]>>();
private final List<Ref<NT[]>> myNewChildrenLists = new ArrayList<Ref<NT[]>>();
private DiffTree(@NotNull FlyweightCapableTreeStructure<OT> oldTree,
@NotNull FlyweightCapableTreeStructure<NT> newTree,
@NotNull ShallowNodeComparator<OT, NT> comparator,
@NotNull DiffTreeChangeBuilder<OT, NT> consumer) {
@NotNull ShallowNodeComparator<OT, NT> comparator) {
myOldTree = oldTree;
myNewTree = newTree;
myComparator = comparator;
myConsumer = consumer;
}
public static <OT, NT> void diff(@NotNull FlyweightCapableTreeStructure<OT> oldTree,
@NotNull FlyweightCapableTreeStructure<NT> newTree,
@NotNull ShallowNodeComparator<OT, NT> comparator,
@NotNull DiffTreeChangeBuilder<OT, NT> consumer) {
new DiffTree<OT, NT>(oldTree, newTree, comparator, consumer).build(oldTree.getRoot(), newTree.getRoot(), 0);
final DiffTree<OT, NT> tree = new DiffTree<OT, NT>(oldTree, newTree, comparator);
tree.build(oldTree.getRoot(), newTree.getRoot(), 0, Integer.MAX_VALUE, consumer);
}
private enum CompareResult {
@@ -59,7 +57,32 @@ public class DiffTree<OT, NT> {
NOT_EQUAL, // 100% different
}
private void build(@NotNull OT oldN, @NotNull NT newN, int level) {
@NotNull
private static <OT, NT> DiffTreeChangeBuilder<OT, NT> emptyConsumer() {
//noinspection unchecked
return EMPTY_CONSUMER;
}
private static final DiffTreeChangeBuilder EMPTY_CONSUMER = new DiffTreeChangeBuilder() {
@Override
public void nodeReplaced(@NotNull Object oldChild, @NotNull Object newChild) {
}
@Override
public void nodeDeleted(@NotNull Object oldParent, @NotNull Object oldNode) {
}
@Override
public void nodeInserted(@NotNull Object oldParent, @NotNull Object newNode, int pos) {
}
};
@NotNull
private CompareResult build(@NotNull OT oldN, @NotNull NT newN, int level, int maxLevel, @NotNull DiffTreeChangeBuilder<OT, NT> consumer) {
if (level == maxLevel) return CompareResult.NOT_EQUAL; // too deep, abort
OT oldNode = myOldTree.prepareForGetChildren(oldN);
NT newNode = myNewTree.prepareForGetChildren(newN);
@@ -76,102 +99,180 @@ public class DiffTree<OT, NT> {
int newChildrenSize = myNewTree.getChildren(newNode, newChildrenR);
final NT[] newChildren = newChildrenR.get();
CompareResult result;
if (Math.abs(oldChildrenSize - newChildrenSize) > CHANGE_PARENT_VERSUS_CHILDREN_THRESHOLD) {
myConsumer.nodeReplaced(oldNode, newNode);
consumer.nodeReplaced(oldNode, newNode);
result = CompareResult.NOT_EQUAL;
}
else if (oldChildrenSize == 0 && newChildrenSize == 0) {
if (!myComparator.hashCodesEqual(oldNode, newNode) || !myComparator.typesEqual(oldNode, newNode)) {
myConsumer.nodeReplaced(oldNode, newNode);
consumer.nodeReplaced(oldNode, newNode);
result = CompareResult.NOT_EQUAL;
}
else {
result = CompareResult.EQUAL;
}
}
else {
int oldSize = oldChildrenSize;
int newSize = newChildrenSize;
final ShallowNodeComparator<OT, NT> comparator = myComparator;
while (oldSize > 0 && newSize > 0) {
OT oldChild1 = oldChildren[oldSize -1];
NT newChild1 = newChildren[newSize -1];
CompareResult c11 = looksEqual(comparator, oldChild1, newChild1);
int minSize = Math.min(oldChildrenSize, newChildrenSize);
int newMaxLevel = Math.min(maxLevel, level+4); // try not to descend recursively too deep
int suffixLength = match(oldChildren, oldChildrenSize - 1, newChildren, newChildrenSize - 1, level, -1, minSize, newMaxLevel);
int prefixLength = oldChildrenSize == 1 && newChildrenSize == 1 ? 0 : match(oldChildren, 0, newChildren, 0, level, 1, minSize-suffixLength, newMaxLevel);
if (c11 != CompareResult.EQUAL && c11 != CompareResult.DRILL_DOWN_NEEDED) {
break;
}
if (c11 == CompareResult.DRILL_DOWN_NEEDED) {
build(oldChild1, newChild1, level + 1);
}
oldSize--;
newSize--;
if (oldChildrenSize == newChildrenSize && suffixLength + prefixLength == oldChildrenSize) {
result = CompareResult.EQUAL;
}
else if (consumer == emptyConsumer()){
result = CompareResult.NOT_EQUAL;
}
else {
int oldIndex = oldChildrenSize - suffixLength - 1;
int newIndex = newChildrenSize - suffixLength - 1;
while (oldIndex >= prefixLength || newIndex >= prefixLength) {
OT oldChild1 = oldIndex >= prefixLength ? oldChildren[oldIndex] : null;
OT oldChild2 = oldIndex >= prefixLength + 1 ? oldChildren[oldIndex - 1] : null;
OT oldChild3 = oldIndex >= prefixLength + 2 ? oldChildren[oldIndex - 2] : null;
NT newChild1 = newIndex >= prefixLength ? newChildren[newIndex] : null;
NT newChild2 = newIndex >= prefixLength + 1 ? newChildren[newIndex - 1] : null;
NT newChild3 = newIndex >= prefixLength + 2 ? newChildren[newIndex - 2] : null;
int oldIndex = 0;
int newIndex = 0;
while (oldIndex < oldSize || newIndex < newSize) {
OT oldChild1 = oldIndex < oldSize ? oldChildren[oldIndex] : null;
OT oldChild2 = oldIndex < oldSize -1 ? oldChildren[oldIndex+1] : null;
NT newChild1 = newIndex < newSize ? newChildren[newIndex] : null;
NT newChild2 = newIndex < newSize -1 ? newChildren[newIndex+1] : null;
CompareResult c11 = looksEqual(comparator, oldChild1, newChild1);
if (c11 == CompareResult.EQUAL || c11 == CompareResult.DRILL_DOWN_NEEDED) {
if (c11 == CompareResult.DRILL_DOWN_NEEDED) {
build(oldChild1, newChild1, level +1);
}
oldIndex++;
newIndex++;
continue;
}
if (c11 == CompareResult.TYPE_ONLY) {
CompareResult c21 = looksEqual(comparator, oldChild2, newChild1);
if (c21 == CompareResult.EQUAL || c21 == CompareResult.DRILL_DOWN_NEEDED) {
myConsumer.nodeDeleted(oldNode, oldChild1);
oldIndex++;
CompareResult c11 = looksEqual(comparator, oldChild1, newChild1);
if (c11 == CompareResult.EQUAL || c11 == CompareResult.DRILL_DOWN_NEEDED) {
if (c11 == CompareResult.DRILL_DOWN_NEEDED) {
build(oldChild1, newChild1, level + 1, maxLevel, consumer);
}
oldIndex--;
newIndex--;
continue;
}
if (c11 == CompareResult.TYPE_ONLY) {
CompareResult c21 = looksEqual(comparator, oldChild2, newChild1);
if (c21 == CompareResult.EQUAL || c21 == CompareResult.DRILL_DOWN_NEEDED) {
consumer.nodeDeleted(oldNode, oldChild1);
oldIndex--;
continue;
}
CompareResult c12 = looksEqual(comparator, oldChild1, newChild2);
if (c12 == CompareResult.EQUAL || c12 == CompareResult.DRILL_DOWN_NEEDED) {
consumer.nodeInserted(oldNode, newChild1, oldIndex + 1);
newIndex--;
continue;
}
consumer.nodeReplaced(oldChild1, newChild1);
oldIndex--;
newIndex--;
continue;
}
CompareResult c12 = looksEqual(comparator, oldChild1, newChild2);
if (c12 == CompareResult.EQUAL || c12 == CompareResult.DRILL_DOWN_NEEDED) {
myConsumer.nodeInserted(oldNode, newChild1, newIndex);
newIndex++;
consumer.nodeInserted(oldNode, newChild1, oldIndex + 1);
newIndex--;
continue;
}
myConsumer.nodeReplaced(oldChild1, newChild1);
oldIndex++;
newIndex++;
continue;
}
CompareResult c12 = looksEqual(comparator, oldChild1, newChild2);
if (c12 == CompareResult.EQUAL || c12 == CompareResult.DRILL_DOWN_NEEDED || c12 == CompareResult.TYPE_ONLY) {
myConsumer.nodeInserted(oldNode, newChild1, newIndex);
newIndex++;
continue;
}
CompareResult c21 = looksEqual(comparator, oldChild2, newChild1);
if (c21 == CompareResult.EQUAL || c21 == CompareResult.DRILL_DOWN_NEEDED || c21 == CompareResult.TYPE_ONLY) {
consumer.nodeDeleted(oldNode, oldChild1);
oldIndex--;
continue;
}
CompareResult c21 = looksEqual(comparator, oldChild2, newChild1);
if (c21 == CompareResult.EQUAL || c21 == CompareResult.DRILL_DOWN_NEEDED || c21 == CompareResult.TYPE_ONLY) {
myConsumer.nodeDeleted(oldNode, oldChild1);
oldIndex++;
continue;
}
if (c12 == CompareResult.TYPE_ONLY) {
consumer.nodeInserted(oldNode, newChild1, oldIndex + 1);
newIndex--;
continue;
}
if (oldChild1 == null) {
myConsumer.nodeInserted(oldNode, newChild1, newIndex);
newIndex++;
continue;
}
if (newChild1 == null) {
myConsumer.nodeDeleted(oldNode, oldChild1);
oldIndex++;
continue;
}
if (oldChild1 == null) {
//consumer.nodeInserted(oldNode, newChild1, newIndex);
consumer.nodeInserted(oldNode, newChild1, oldIndex+1);
newIndex--;
continue;
}
if (newChild1 == null) {
consumer.nodeDeleted(oldNode, oldChild1);
oldIndex--;
continue;
}
myConsumer.nodeReplaced(oldChild1, newChild1);
oldIndex++;
newIndex++;
// check that maybe two children are inserted/deleted
// (which frequently is a case when e.g. a PsiMethod inserted, the trailing PsiWhiteSpace is appended too)
if (oldChild3 != null || newChild3 != null) {
CompareResult c13 = looksEqual(comparator, oldChild1, newChild3);
if (c13 == CompareResult.EQUAL || c13 == CompareResult.DRILL_DOWN_NEEDED || c13 == CompareResult.TYPE_ONLY) {
consumer.nodeInserted(oldNode, newChild1, oldIndex + 1);
consumer.nodeInserted(oldNode, newChild2, oldIndex+1);
newIndex--;
newIndex--;
continue;
}
CompareResult c31 = looksEqual(comparator, oldChild3, newChild1);
if (c31 == CompareResult.EQUAL || c31 == CompareResult.DRILL_DOWN_NEEDED || c31 == CompareResult.TYPE_ONLY) {
consumer.nodeDeleted(oldNode, oldChild1);
consumer.nodeDeleted(oldNode, oldChild2);
oldIndex--;
oldIndex--;
continue;
}
}
// last resort: maybe the first elements are more similar?
OT oldFirstChild = oldIndex >= prefixLength ? oldChildren[prefixLength] : null;
NT newFirstChild = newIndex >= prefixLength ? newChildren[prefixLength] : null;
CompareResult c = oldFirstChild == null || newFirstChild == null ? CompareResult.NOT_EQUAL : looksEqual(comparator, oldFirstChild, newFirstChild);
if (c == CompareResult.EQUAL || c == CompareResult.TYPE_ONLY || c == CompareResult.DRILL_DOWN_NEEDED) {
if (c == CompareResult.DRILL_DOWN_NEEDED) {
build(oldFirstChild, newFirstChild, level + 1, maxLevel, consumer);
}
else {
consumer.nodeReplaced(oldFirstChild, newFirstChild);
}
prefixLength++;
continue;
}
consumer.nodeReplaced(oldChild1, newChild1);
oldIndex--;
newIndex--;
}
result = CompareResult.NOT_EQUAL;
}
}
myOldTree.disposeChildren(oldChildren, oldChildrenSize);
myNewTree.disposeChildren(newChildren, newChildrenSize);
return result;
}
// tries to match as many nodes as possible from the beginning (if step=1) of from the end (if step =-1)
// returns number of nodes matched
private int match(OT[] oldChildren,
int oldIndex,
NT[] newChildren,
int newIndex,
int level,
int step, // 1 if we go from the start to the end; -1 if we go from the end to the start
int maxLength,
int maxLevel) {
int delta = 0;
while (delta != maxLength*step) {
OT oldChild = oldChildren[oldIndex + delta];
NT newChild = newChildren[newIndex + delta];
CompareResult c11 = looksEqual(myComparator, oldChild, newChild);
if (c11 == CompareResult.DRILL_DOWN_NEEDED) {
c11 = build(oldChild, newChild, level + 1, maxLevel, DiffTree.<OT, NT>emptyConsumer());
}
assert c11 != CompareResult.DRILL_DOWN_NEEDED;
if (c11 != CompareResult.EQUAL) {
break;
}
delta += step;
}
return delta*step;
}
@NotNull

View File

@@ -32,13 +32,14 @@ public class DiffTreeTest extends TestCase {
private static class Node {
@NotNull
private final Node[] myChildren;
int myId;
private final int myId;
public Node(final int id, @NotNull Node... children) {
myChildren = children;
myId = id;
}
@Override
public int hashCode() {
return myId + myChildren.length; // This is intentionally bad hashcode
}
@@ -52,6 +53,7 @@ public class DiffTreeTest extends TestCase {
return myId;
}
@Override
public String toString() {
return String.valueOf(myId);
}
@@ -60,7 +62,7 @@ public class DiffTreeTest extends TestCase {
private static class TreeStructure implements FlyweightCapableTreeStructure<Node> {
private final Node myRoot;
public TreeStructure(final Node root) {
private TreeStructure(final Node root) {
myRoot = root;
}
@@ -110,7 +112,7 @@ public class DiffTreeTest extends TestCase {
}
}
public static class DiffBuilder implements DiffTreeChangeBuilder<Node, Node> {
private static class DiffBuilder implements DiffTreeChangeBuilder<Node, Node> {
private final List<String> myResults = new ArrayList<String>();
@Override
@@ -202,7 +204,7 @@ public class DiffTreeTest extends TestCase {
Node r1 = new Node(0, new Node(1, new Node(21), new Node(22)));
Node r2 = new Node(0, new Node(1, new Node(21), new Node(22), new Node(23), new Node(24)));
performTest(r1, r2, "INSERTED to 1: 23 at 2", "INSERTED to 1: 24 at 3");
performTest(r1, r2, "INSERTED to 1: 24 at 2", "INSERTED to 1: 23 at 2");
}
public void testSubtreeAppears() throws Exception {